From cd4b8b92320cea5af6161eeb793d0732e8eab551 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Mon, 11 Aug 2025 16:56:36 +0200 Subject: [PATCH 01/60] remove the forced installation of pytest plugin --- {vsengine/tests => extra_tests}/__init__.py | 0 {vsengine/tests => extra_tests}/pytest.py | 0 {vsengine/tests => extra_tests}/unittest.py | 0 pyproject.toml | 8 +------- tests/test_video.py | 4 ++-- 5 files changed, 3 insertions(+), 9 deletions(-) rename {vsengine/tests => extra_tests}/__init__.py (100%) rename {vsengine/tests => extra_tests}/pytest.py (100%) rename {vsengine/tests => extra_tests}/unittest.py (100%) diff --git a/vsengine/tests/__init__.py b/extra_tests/__init__.py similarity index 100% rename from vsengine/tests/__init__.py rename to extra_tests/__init__.py diff --git a/vsengine/tests/pytest.py b/extra_tests/pytest.py similarity index 100% rename from vsengine/tests/pytest.py rename to extra_tests/pytest.py diff --git a/vsengine/tests/unittest.py b/extra_tests/unittest.py similarity index 100% rename from vsengine/tests/unittest.py rename to extra_tests/unittest.py diff --git a/pyproject.toml b/pyproject.toml index 52a631b..0de1abc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "vsengine" -version = "0.2.0+dirty" +version = "0.2.0+jet" license = { file = "COPYING" } readme = "README.md" authors = [ @@ -13,12 +13,6 @@ dependencies = [ "vapoursynth>=57" ] -[project.scripts] -vpy-unittest = "vsengine.tests.unittest:main" - -[project.entry-points.pytest11] -vsengine = "vsengine.tests.pytest" - [project.optional-dependencies] trio = [ "trio" diff --git a/tests/test_video.py b/tests/test_video.py index 97a21cb..ab7083e 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -7,13 +7,13 @@ from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy -from vapoursynth import core, PresetFormat, VideoFormat, GRAY8, RGB24 +from vapoursynth import core, PresetVideoFormat, VideoFormat, GRAY8, RGB24 from vapoursynth import VideoNode, VideoFrame from vsengine.video import frame, planes, frames, render -AnyFormat = t.Union[PresetFormat, VideoFormat] +AnyFormat = t.Union[PresetVideoFormat, VideoFormat] class TestVideo(unittest.TestCase): From c2bde81fbb1f7082b50db620c4ae6811dd40b9be Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Mon, 11 Aug 2025 17:04:10 +0200 Subject: [PATCH 02/60] bump deps --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0de1abc..97936fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,10 @@ authors = [ { name = "cid-chan", email ="cid+git@cid-chan.moe" } ] dynamic = ["description"] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ - "vapoursynth>=57" + "vapoursynth>=72" ] [project.optional-dependencies] From 34b234f4085abee31af62796aa7c88a1016ca508 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 29 Nov 2025 19:13:17 +0100 Subject: [PATCH 03/60] add missing typing --- pyproject.toml | 57 +- stubs/vapoursynth/__init__.pyi | 1560 ++++++++++++++++++++++++++++++++ tests/test_convert.py | 2 +- tests/test_video.py | 19 +- uv.lock | 346 +++++++ vsengine/_futures.py | 256 ++++-- vsengine/_helpers.py | 26 +- vsengine/_hospice.py | 37 +- vsengine/_nodes.py | 43 +- vsengine/_testutils.py | 78 +- vsengine/adapters/asyncio.py | 43 +- vsengine/adapters/trio.py | 73 +- vsengine/convert.py | 102 +-- vsengine/loops.py | 90 +- vsengine/policy.py | 141 +-- vsengine/py.typed | 0 vsengine/video.py | 102 +-- vsengine/vpy.py | 171 ++-- 18 files changed, 2543 insertions(+), 603 deletions(-) create mode 100644 stubs/vapoursynth/__init__.pyi create mode 100644 uv.lock create mode 100644 vsengine/py.typed diff --git a/pyproject.toml b/pyproject.toml index 97936fb..33c1b46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,24 +3,55 @@ name = "vsengine" version = "0.2.0+jet" license = { file = "COPYING" } readme = "README.md" -authors = [ - { name = "cid-chan", email ="cid+git@cid-chan.moe" } -] +authors = [{ name = "cid-chan", email = "cid+git@cid-chan.moe" }] dynamic = ["description"] -requires-python = ">=3.10" +requires-python = ">=3.12" -dependencies = [ - "vapoursynth>=72" -] +dependencies = ["vapoursynth>=69"] [project.optional-dependencies] -trio = [ - "trio" -] -test = [ - "pytest" -] +trio = ["trio"] +test = ["pytest"] + +[dependency-groups] +dev = ["mypy>=1.19.0", "ruff>=0.14.7", "trio"] [build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" + +[tool.mypy] +mypy_path = "$MYPY_CONFIG_FILE_DIR/stubs" + +# exclude = ["stubs/vapoursynth"] +exclude_gitignore = true + +# Flags changed by --strict +warn_return_any = false +extra_checks = false + +# Misc +warn_unreachable = true + +allow_redefinition_new = true +local_partial_types = true +implicit_reexport = false +strict = true + +show_column_numbers = true +pretty = true +color_output = true +error_summary = true + +[tool.ruff] +line-length = 120 +# extend-exclude = ["stubs/vapoursynth/__init__.pyi"] + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +extend-select = ["E", "C4", "I", "PYI", "Q", "SIM", "N", "W", "RUF"] + +[tool.ruff.lint.per-file-ignores] +"__init__.*" = ["F401", "F403"] diff --git a/stubs/vapoursynth/__init__.pyi b/stubs/vapoursynth/__init__.pyi new file mode 100644 index 0000000..e309dc8 --- /dev/null +++ b/stubs/vapoursynth/__init__.pyi @@ -0,0 +1,1560 @@ +# ruff: noqa: RUF100, E501, PYI002, PYI029, PYI046, PYI047, N801, N802, N803, N805, I001 +from collections.abc import Buffer +from concurrent.futures import Future +from ctypes import c_void_p +from enum import Enum, IntEnum, IntFlag +from fractions import Fraction +from inspect import Signature +from logging import Handler, LogRecord, StreamHandler +from types import MappingProxyType, TracebackType +from typing import Any, Callable, Concatenate, Final, IO, Iterable, Iterator, Literal, Mapping, MutableMapping, NamedTuple, Protocol, Self, SupportsFloat, SupportsIndex, SupportsInt, TextIO, TypedDict, final, overload +from warnings import deprecated +from weakref import ReferenceType + + +__all__ = [ + "CHROMA_BOTTOM", + "CHROMA_BOTTOM_LEFT", + "CHROMA_CENTER", + "CHROMA_LEFT", + "CHROMA_TOP", + "CHROMA_TOP_LEFT", + "FIELD_BOTTOM", + "FIELD_PROGRESSIVE", + "FIELD_TOP", + "FLOAT", + "GRAY", + "GRAY8", + "GRAY9", + "GRAY10", + "GRAY12", + "GRAY14", + "GRAY16", + "GRAY32", + "GRAYH", + "GRAYS", + "INTEGER", + "NONE", + "RANGE_FULL", + "RANGE_LIMITED", + "RGB", + "RGB24", + "RGB27", + "RGB30", + "RGB36", + "RGB42", + "RGB48", + "RGBH", + "RGBS", + "YUV", + "YUV410P8", + "YUV411P8", + "YUV420P8", + "YUV420P9", + "YUV420P10", + "YUV420P12", + "YUV420P14", + "YUV420P16", + "YUV420PH", + "YUV420PS", + "YUV422P8", + "YUV422P9", + "YUV422P10", + "YUV422P12", + "YUV422P14", + "YUV422P16", + "YUV422PH", + "YUV422PS", + "YUV440P8", + "YUV444P8", + "YUV444P9", + "YUV444P10", + "YUV444P12", + "YUV444P14", + "YUV444P16", + "YUV444PH", + "YUV444PS", + "clear_output", + "clear_outputs", + "core", + "get_output", + "get_outputs", +] + +type _AnyStr = str | bytes | bytearray +type _IntLike = SupportsInt | SupportsIndex | Buffer +type _FloatLike = SupportsFloat | SupportsIndex | Buffer + +type _VSValueSingle = ( + int | float | _AnyStr | RawFrame | VideoFrame | AudioFrame | RawNode | VideoNode | AudioNode | Callable[..., Any] +) + +type _VSValueIterable = ( + _SupportsIter[_IntLike] + | _SupportsIter[_FloatLike] + | _SupportsIter[_AnyStr] + | _SupportsIter[RawFrame] + | _SupportsIter[VideoFrame] + | _SupportsIter[AudioFrame] + | _SupportsIter[RawNode] + | _SupportsIter[VideoNode] + | _SupportsIter[AudioNode] + | _SupportsIter[Callable[..., Any]] + | _GetItemIterable[_IntLike] + | _GetItemIterable[_FloatLike] + | _GetItemIterable[_AnyStr] + | _GetItemIterable[RawFrame] + | _GetItemIterable[VideoFrame] + | _GetItemIterable[AudioFrame] + | _GetItemIterable[RawNode] + | _GetItemIterable[VideoNode] + | _GetItemIterable[AudioNode] + | _GetItemIterable[Callable[..., Any]] +) +type _VSValue = _VSValueSingle | _VSValueIterable + +class _SupportsIter[_T](Protocol): + def __iter__(self) -> Iterator[_T]: ... + +class _SequenceLike[_T](Protocol): + def __iter__(self) -> Iterator[_T]: ... + def __len__(self) -> int: ... + +class _GetItemIterable[_T](Protocol): + def __getitem__(self, i: SupportsIndex, /) -> _T: ... + +class _SupportsKeysAndGetItem[_KT, _VT](Protocol): + def __getitem__(self, key: _KT, /) -> _VT: ... + def keys(self) -> Iterable[_KT]: ... + +class _VSCallback(Protocol): + def __call__(self, *args: Any, **kwargs: Any) -> _VSValue: ... + +# Known callback signatures +# _VSCallback_{plugin_namespace}_{Function_name}_{parameter_name} +class _VSCallback_akarin_PropExpr_dict(Protocol): + def __call__( + self, + ) -> Mapping[ + str, + _IntLike + | _FloatLike + | _AnyStr + | _SupportsIter[_IntLike] + | _SupportsIter[_AnyStr] + | _SupportsIter[_FloatLike] + | _GetItemIterable[_IntLike] + | _GetItemIterable[_FloatLike] + | _GetItemIterable[_AnyStr], + ]: ... + +class _VSCallback_descale_Decustom_custom_kernel(Protocol): + def __call__(self, *, x: float) -> _FloatLike: ... + +class _VSCallback_descale_ScaleCustom_custom_kernel(Protocol): + def __call__(self, *, x: float) -> _FloatLike: ... + +class _VSCallback_std_FrameEval_eval_0(Protocol): + def __call__(self, *, n: int) -> VideoNode: ... + +class _VSCallback_std_FrameEval_eval_1(Protocol): + def __call__(self, *, n: int, f: VideoFrame) -> VideoNode: ... + +class _VSCallback_std_FrameEval_eval_2(Protocol): + def __call__(self, *, n: int, f: list[VideoFrame]) -> VideoNode: ... + +class _VSCallback_std_FrameEval_eval_3(Protocol): + def __call__(self, *, n: int, f: VideoFrame | list[VideoFrame]) -> VideoNode: ... + +type _VSCallback_std_FrameEval_eval = ( # noqa: PYI047 + _VSCallback_std_FrameEval_eval_0 + | _VSCallback_std_FrameEval_eval_1 + | _VSCallback_std_FrameEval_eval_2 + | _VSCallback_std_FrameEval_eval_3 +) + +class _VSCallback_std_Lut_function_0(Protocol): + def __call__(self, *, x: int) -> _IntLike: ... + +class _VSCallback_std_Lut_function_1(Protocol): + def __call__(self, *, x: float) -> _FloatLike: ... + +type _VSCallback_std_Lut_function = _VSCallback_std_Lut_function_0 | _VSCallback_std_Lut_function_1 # noqa: PYI047 + +class _VSCallback_std_Lut2_function_0(Protocol): + def __call__(self, *, x: int, y: int) -> _IntLike: ... + +class _VSCallback_std_Lut2_function_1(Protocol): + def __call__(self, *, x: float, y: float) -> _FloatLike: ... + +type _VSCallback_std_Lut2_function = _VSCallback_std_Lut2_function_0 | _VSCallback_std_Lut2_function_1 # noqa: PYI047 + +class _VSCallback_std_ModifyFrame_selector_0(Protocol): + def __call__(self, *, n: int, f: VideoFrame) -> VideoFrame: ... + +class _VSCallback_std_ModifyFrame_selector_1(Protocol): + def __call__(self, *, n: int, f: list[VideoFrame]) -> VideoFrame: ... + +class _VSCallback_std_ModifyFrame_selector_2(Protocol): + def __call__(self, *, n: int, f: VideoFrame | list[VideoFrame]) -> VideoFrame: ... + +type _VSCallback_std_ModifyFrame_selector = ( # noqa: PYI047 + _VSCallback_std_ModifyFrame_selector_0 + | _VSCallback_std_ModifyFrame_selector_1 + | _VSCallback_std_ModifyFrame_selector_2 +) + +class _VSCallback_resize2_Custom_custom_kernel(Protocol): + def __call__(self, *, x: float) -> _FloatLike: ... + +class LogHandle: ... + +class PythonVSScriptLoggingBridge(Handler): + def __init__(self, parent: StreamHandler[TextIO], level: int | str = ...) -> None: ... + def emit(self, record: LogRecord) -> None: ... + +class Error(Exception): + value: Any + def __init__(self, value: Any) -> None: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + +# Environment SubSystem +@final +class EnvironmentData: ... + +class EnvironmentPolicy: + def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None: ... + def on_policy_cleared(self) -> None: ... + def get_current_environment(self) -> EnvironmentData | None: ... + def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None: ... + def is_alive(self, environment: EnvironmentData) -> bool: ... + +@final +class StandaloneEnvironmentPolicy: + def on_policy_registered(self, api: EnvironmentPolicyAPI) -> None: ... + def on_policy_cleared(self) -> None: ... + def get_current_environment(self) -> EnvironmentData: ... + def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData: ... + def is_alive(self, environment: EnvironmentData) -> bool: ... + def _on_log_message(self, level: MessageType, msg: str) -> None: ... + +@final +class VSScriptEnvironmentPolicy: + def on_policy_registered(self, policy_api: EnvironmentPolicyAPI) -> None: ... + def on_policy_cleared(self) -> None: ... + def get_current_environment(self) -> EnvironmentData | None: ... + def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None: ... + def is_alive(self, environment: EnvironmentData) -> bool: ... + +@final +class EnvironmentPolicyAPI: + def wrap_environment(self, environment_data: EnvironmentData) -> Environment: ... + def create_environment(self, flags: _IntLike = 0) -> EnvironmentData: ... + def set_logger(self, env: EnvironmentData, logger: Callable[[int, str], None]) -> None: ... + def get_vapoursynth_api(self, version: int) -> c_void_p: ... + def get_core_ptr(self, environment_data: EnvironmentData) -> c_void_p: ... + def destroy_environment(self, env: EnvironmentData) -> None: ... + def unregister_policy(self) -> None: ... + +def register_policy(policy: EnvironmentPolicy) -> None: ... +def has_policy() -> bool: ... +def register_on_destroy(callback: Callable[..., None]) -> None: ... +def unregister_on_destroy(callback: Callable[..., None]) -> None: ... +def _try_enable_introspection(version: int | None = None) -> bool: ... +@final +class _FastManager: + def __enter__(self) -> None: ... + def __exit__(self, *_: object) -> None: ... + +class Environment: + env: Final[ReferenceType[EnvironmentData]] + def __repr__(self) -> str: ... + @overload + def __eq__(self, other: Environment) -> bool: ... + @overload + def __eq__(self, other: object) -> bool: ... + @property + def alive(self) -> bool: ... + @property + def single(self) -> bool: ... + @classmethod + def is_single(cls) -> bool: ... + @property + def env_id(self) -> int: ... + @property + def active(self) -> bool: ... + def copy(self) -> Self: ... + def use(self) -> _FastManager: ... + +class Local: + def __getattr__(self, key: str) -> Any: ... + def __setattr__(self, key: str, value: Any) -> None: ... + def __delattr__(self, key: str) -> None: ... + +def get_current_environment() -> Environment: ... + +class CoreTimings: + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + @property + def enabled(self) -> bool: ... + @enabled.setter + def enabled(self, enabled: bool) -> bool: ... + @property + def freed_nodes(self) -> bool: ... + @freed_nodes.setter + def freed_nodes(self, value: Literal[0]) -> bool: ... + +# VapourSynth & plugin versioning + +class VapourSynthVersion(NamedTuple): + release_major: int + release_minor: int + def __str__(self) -> str: ... + +class VapourSynthAPIVersion(NamedTuple): + api_major: int + api_minor: int + def __str__(self) -> str: ... + +__version__: VapourSynthVersion +__api_version__: VapourSynthAPIVersion + +# Vapoursynth constants from vapoursynth.pyx + +class MediaType(IntEnum): + VIDEO = ... + AUDIO = ... + +VIDEO: Literal[MediaType.VIDEO] +AUDIO: Literal[MediaType.AUDIO] + +class ColorFamily(IntEnum): + UNDEFINED = ... + GRAY = ... + RGB = ... + YUV = ... + +UNDEFINED: Literal[ColorFamily.UNDEFINED] +GRAY: Literal[ColorFamily.GRAY] +RGB: Literal[ColorFamily.RGB] +YUV: Literal[ColorFamily.YUV] + +class SampleType(IntEnum): + INTEGER = ... + FLOAT = ... + +INTEGER: Literal[SampleType.INTEGER] +FLOAT: Literal[SampleType.FLOAT] + +class PresetVideoFormat(IntEnum): + NONE = ... + + GRAY8 = ... + GRAY9 = ... + GRAY10 = ... + GRAY12 = ... + GRAY14 = ... + GRAY16 = ... + GRAY32 = ... + + GRAYH = ... + GRAYS = ... + + YUV420P8 = ... + YUV422P8 = ... + YUV444P8 = ... + YUV410P8 = ... + YUV411P8 = ... + YUV440P8 = ... + + YUV420P9 = ... + YUV422P9 = ... + YUV444P9 = ... + + YUV420P10 = ... + YUV422P10 = ... + YUV444P10 = ... + + YUV420P12 = ... + YUV422P12 = ... + YUV444P12 = ... + + YUV420P14 = ... + YUV422P14 = ... + YUV444P14 = ... + + YUV420P16 = ... + YUV422P16 = ... + YUV444P16 = ... + + YUV420PH = ... + YUV420PS = ... + + YUV422PH = ... + YUV422PS = ... + + YUV444PH = ... + YUV444PS = ... + + RGB24 = ... + RGB27 = ... + RGB30 = ... + RGB36 = ... + RGB42 = ... + RGB48 = ... + + RGBH = ... + RGBS = ... + +NONE: Literal[PresetVideoFormat.NONE] + +GRAY8: Literal[PresetVideoFormat.GRAY8] +GRAY9: Literal[PresetVideoFormat.GRAY9] +GRAY10: Literal[PresetVideoFormat.GRAY10] +GRAY12: Literal[PresetVideoFormat.GRAY12] +GRAY14: Literal[PresetVideoFormat.GRAY14] +GRAY16: Literal[PresetVideoFormat.GRAY16] +GRAY32: Literal[PresetVideoFormat.GRAY32] + +GRAYH: Literal[PresetVideoFormat.GRAYH] +GRAYS: Literal[PresetVideoFormat.GRAYS] + +YUV420P8: Literal[PresetVideoFormat.YUV420P8] +YUV422P8: Literal[PresetVideoFormat.YUV422P8] +YUV444P8: Literal[PresetVideoFormat.YUV444P8] +YUV410P8: Literal[PresetVideoFormat.YUV410P8] +YUV411P8: Literal[PresetVideoFormat.YUV411P8] +YUV440P8: Literal[PresetVideoFormat.YUV440P8] + +YUV420P9: Literal[PresetVideoFormat.YUV420P9] +YUV422P9: Literal[PresetVideoFormat.YUV422P9] +YUV444P9: Literal[PresetVideoFormat.YUV444P9] + +YUV420P10: Literal[PresetVideoFormat.YUV420P10] +YUV422P10: Literal[PresetVideoFormat.YUV422P10] +YUV444P10: Literal[PresetVideoFormat.YUV444P10] + +YUV420P12: Literal[PresetVideoFormat.YUV420P12] +YUV422P12: Literal[PresetVideoFormat.YUV422P12] +YUV444P12: Literal[PresetVideoFormat.YUV444P12] + +YUV420P14: Literal[PresetVideoFormat.YUV420P14] +YUV422P14: Literal[PresetVideoFormat.YUV422P14] +YUV444P14: Literal[PresetVideoFormat.YUV444P14] + +YUV420P16: Literal[PresetVideoFormat.YUV420P16] +YUV422P16: Literal[PresetVideoFormat.YUV422P16] +YUV444P16: Literal[PresetVideoFormat.YUV444P16] + +YUV420PH: Literal[PresetVideoFormat.YUV420PH] +YUV420PS: Literal[PresetVideoFormat.YUV420PS] + +YUV422PH: Literal[PresetVideoFormat.YUV422PH] +YUV422PS: Literal[PresetVideoFormat.YUV422PS] + +YUV444PH: Literal[PresetVideoFormat.YUV444PH] +YUV444PS: Literal[PresetVideoFormat.YUV444PS] + +RGB24: Literal[PresetVideoFormat.RGB24] +RGB27: Literal[PresetVideoFormat.RGB27] +RGB30: Literal[PresetVideoFormat.RGB30] +RGB36: Literal[PresetVideoFormat.RGB36] +RGB42: Literal[PresetVideoFormat.RGB42] +RGB48: Literal[PresetVideoFormat.RGB48] + +RGBH: Literal[PresetVideoFormat.RGBH] +RGBS: Literal[PresetVideoFormat.RGBS] + +class FilterMode(IntEnum): + PARALLEL = ... + PARALLEL_REQUESTS = ... + UNORDERED = ... + FRAME_STATE = ... + +PARALLEL: Literal[FilterMode.PARALLEL] +PARALLEL_REQUESTS: Literal[FilterMode.PARALLEL_REQUESTS] +UNORDERED: Literal[FilterMode.UNORDERED] +FRAME_STATE: Literal[FilterMode.FRAME_STATE] + +class AudioChannels(IntEnum): + FRONT_LEFT = ... + FRONT_RIGHT = ... + FRONT_CENTER = ... + LOW_FREQUENCY = ... + BACK_LEFT = ... + BACK_RIGHT = ... + FRONT_LEFT_OF_CENTER = ... + FRONT_RIGHT_OF_CENTER = ... + BACK_CENTER = ... + SIDE_LEFT = ... + SIDE_RIGHT = ... + TOP_CENTER = ... + TOP_FRONT_LEFT = ... + TOP_FRONT_CENTER = ... + TOP_FRONT_RIGHT = ... + TOP_BACK_LEFT = ... + TOP_BACK_CENTER = ... + TOP_BACK_RIGHT = ... + STEREO_LEFT = ... + STEREO_RIGHT = ... + WIDE_LEFT = ... + WIDE_RIGHT = ... + SURROUND_DIRECT_LEFT = ... + SURROUND_DIRECT_RIGHT = ... + LOW_FREQUENCY2 = ... + +FRONT_LEFT: Literal[AudioChannels.FRONT_LEFT] +FRONT_RIGHT: Literal[AudioChannels.FRONT_RIGHT] +FRONT_CENTER: Literal[AudioChannels.FRONT_CENTER] +LOW_FREQUENCY: Literal[AudioChannels.LOW_FREQUENCY] +BACK_LEFT: Literal[AudioChannels.BACK_LEFT] +BACK_RIGHT: Literal[AudioChannels.BACK_RIGHT] +FRONT_LEFT_OF_CENTER: Literal[AudioChannels.FRONT_LEFT_OF_CENTER] +FRONT_RIGHT_OF_CENTER: Literal[AudioChannels.FRONT_RIGHT_OF_CENTER] +BACK_CENTER: Literal[AudioChannels.BACK_CENTER] +SIDE_LEFT: Literal[AudioChannels.SIDE_LEFT] +SIDE_RIGHT: Literal[AudioChannels.SIDE_RIGHT] +TOP_CENTER: Literal[AudioChannels.TOP_CENTER] +TOP_FRONT_LEFT: Literal[AudioChannels.TOP_FRONT_LEFT] +TOP_FRONT_CENTER: Literal[AudioChannels.TOP_FRONT_CENTER] +TOP_FRONT_RIGHT: Literal[AudioChannels.TOP_FRONT_RIGHT] +TOP_BACK_LEFT: Literal[AudioChannels.TOP_BACK_LEFT] +TOP_BACK_CENTER: Literal[AudioChannels.TOP_BACK_CENTER] +TOP_BACK_RIGHT: Literal[AudioChannels.TOP_BACK_RIGHT] +STEREO_LEFT: Literal[AudioChannels.STEREO_LEFT] +STEREO_RIGHT: Literal[AudioChannels.STEREO_RIGHT] +WIDE_LEFT: Literal[AudioChannels.WIDE_LEFT] +WIDE_RIGHT: Literal[AudioChannels.WIDE_RIGHT] +SURROUND_DIRECT_LEFT: Literal[AudioChannels.SURROUND_DIRECT_LEFT] +SURROUND_DIRECT_RIGHT: Literal[AudioChannels.SURROUND_DIRECT_RIGHT] +LOW_FREQUENCY2: Literal[AudioChannels.LOW_FREQUENCY2] + +class MessageType(IntFlag): + MESSAGE_TYPE_DEBUG = ... + MESSAGE_TYPE_INFORMATION = ... + MESSAGE_TYPE_WARNING = ... + MESSAGE_TYPE_CRITICAL = ... + MESSAGE_TYPE_FATAL = ... + +MESSAGE_TYPE_DEBUG: Literal[MessageType.MESSAGE_TYPE_DEBUG] +MESSAGE_TYPE_INFORMATION: Literal[MessageType.MESSAGE_TYPE_INFORMATION] +MESSAGE_TYPE_WARNING: Literal[MessageType.MESSAGE_TYPE_WARNING] +MESSAGE_TYPE_CRITICAL: Literal[MessageType.MESSAGE_TYPE_CRITICAL] +MESSAGE_TYPE_FATAL: Literal[MessageType.MESSAGE_TYPE_FATAL] + +class CoreCreationFlags(IntFlag): + ENABLE_GRAPH_INSPECTION = ... + DISABLE_AUTO_LOADING = ... + DISABLE_LIBRARY_UNLOADING = ... + +ENABLE_GRAPH_INSPECTION: Literal[CoreCreationFlags.ENABLE_GRAPH_INSPECTION] +DISABLE_AUTO_LOADING: Literal[CoreCreationFlags.DISABLE_AUTO_LOADING] +DISABLE_LIBRARY_UNLOADING: Literal[CoreCreationFlags.DISABLE_LIBRARY_UNLOADING] + +# Vapoursynth constants from vsconstants.pyd + +class ColorRange(IntEnum): + RANGE_FULL = ... + RANGE_LIMITED = ... + +RANGE_FULL: Literal[ColorRange.RANGE_FULL] +RANGE_LIMITED: Literal[ColorRange.RANGE_LIMITED] + +class ChromaLocation(IntEnum): + CHROMA_LEFT = ... + CHROMA_CENTER = ... + CHROMA_TOP_LEFT = ... + CHROMA_TOP = ... + CHROMA_BOTTOM_LEFT = ... + CHROMA_BOTTOM = ... + +CHROMA_LEFT: Literal[ChromaLocation.CHROMA_LEFT] +CHROMA_CENTER: Literal[ChromaLocation.CHROMA_CENTER] +CHROMA_TOP_LEFT: Literal[ChromaLocation.CHROMA_TOP_LEFT] +CHROMA_TOP: Literal[ChromaLocation.CHROMA_TOP] +CHROMA_BOTTOM_LEFT: Literal[ChromaLocation.CHROMA_BOTTOM_LEFT] +CHROMA_BOTTOM: Literal[ChromaLocation.CHROMA_BOTTOM] + +class FieldBased(IntEnum): + FIELD_PROGRESSIVE = ... + FIELD_TOP = ... + FIELD_BOTTOM = ... + +FIELD_PROGRESSIVE: Literal[FieldBased.FIELD_PROGRESSIVE] +FIELD_TOP: Literal[FieldBased.FIELD_TOP] +FIELD_BOTTOM: Literal[FieldBased.FIELD_BOTTOM] + +class MatrixCoefficients(IntEnum): + MATRIX_RGB = ... + MATRIX_BT709 = ... + MATRIX_UNSPECIFIED = ... + MATRIX_FCC = ... + MATRIX_BT470_BG = ... + MATRIX_ST170_M = ... + MATRIX_ST240_M = ... + MATRIX_YCGCO = ... + MATRIX_BT2020_NCL = ... + MATRIX_BT2020_CL = ... + MATRIX_CHROMATICITY_DERIVED_NCL = ... + MATRIX_CHROMATICITY_DERIVED_CL = ... + MATRIX_ICTCP = ... + +MATRIX_RGB: Literal[MatrixCoefficients.MATRIX_RGB] +MATRIX_BT709: Literal[MatrixCoefficients.MATRIX_BT709] +MATRIX_UNSPECIFIED: Literal[MatrixCoefficients.MATRIX_UNSPECIFIED] +MATRIX_FCC: Literal[MatrixCoefficients.MATRIX_FCC] +MATRIX_BT470_BG: Literal[MatrixCoefficients.MATRIX_BT470_BG] +MATRIX_ST170_M: Literal[MatrixCoefficients.MATRIX_ST170_M] +MATRIX_ST240_M: Literal[MatrixCoefficients.MATRIX_ST240_M] +MATRIX_YCGCO: Literal[MatrixCoefficients.MATRIX_YCGCO] +MATRIX_BT2020_NCL: Literal[MatrixCoefficients.MATRIX_BT2020_NCL] +MATRIX_BT2020_CL: Literal[MatrixCoefficients.MATRIX_BT2020_CL] +MATRIX_CHROMATICITY_DERIVED_NCL: Literal[MatrixCoefficients.MATRIX_CHROMATICITY_DERIVED_NCL] +MATRIX_CHROMATICITY_DERIVED_CL: Literal[MatrixCoefficients.MATRIX_CHROMATICITY_DERIVED_CL] +MATRIX_ICTCP: Literal[MatrixCoefficients.MATRIX_ICTCP] + +class TransferCharacteristics(IntEnum): + TRANSFER_BT709 = ... + TRANSFER_UNSPECIFIED = ... + TRANSFER_BT470_M = ... + TRANSFER_BT470_BG = ... + TRANSFER_BT601 = ... + TRANSFER_ST240_M = ... + TRANSFER_LINEAR = ... + TRANSFER_LOG_100 = ... + TRANSFER_LOG_316 = ... + TRANSFER_IEC_61966_2_4 = ... + TRANSFER_IEC_61966_2_1 = ... + TRANSFER_BT2020_10 = ... + TRANSFER_BT2020_12 = ... + TRANSFER_ST2084 = ... + TRANSFER_ST428 = ... + TRANSFER_ARIB_B67 = ... + +TRANSFER_BT709: Literal[TransferCharacteristics.TRANSFER_BT709] +TRANSFER_UNSPECIFIED: Literal[TransferCharacteristics.TRANSFER_UNSPECIFIED] +TRANSFER_BT470_M: Literal[TransferCharacteristics.TRANSFER_BT470_M] +TRANSFER_BT470_BG: Literal[TransferCharacteristics.TRANSFER_BT470_BG] +TRANSFER_BT601: Literal[TransferCharacteristics.TRANSFER_BT601] +TRANSFER_ST240_M: Literal[TransferCharacteristics.TRANSFER_ST240_M] +TRANSFER_LINEAR: Literal[TransferCharacteristics.TRANSFER_LINEAR] +TRANSFER_LOG_100: Literal[TransferCharacteristics.TRANSFER_LOG_100] +TRANSFER_LOG_316: Literal[TransferCharacteristics.TRANSFER_LOG_316] +TRANSFER_IEC_61966_2_4: Literal[TransferCharacteristics.TRANSFER_IEC_61966_2_4] +TRANSFER_IEC_61966_2_1: Literal[TransferCharacteristics.TRANSFER_IEC_61966_2_1] +TRANSFER_BT2020_10: Literal[TransferCharacteristics.TRANSFER_BT2020_10] +TRANSFER_BT2020_12: Literal[TransferCharacteristics.TRANSFER_BT2020_12] +TRANSFER_ST2084: Literal[TransferCharacteristics.TRANSFER_ST2084] +TRANSFER_ST428: Literal[TransferCharacteristics.TRANSFER_ST428] +TRANSFER_ARIB_B67: Literal[TransferCharacteristics.TRANSFER_ARIB_B67] + +class ColorPrimaries(IntEnum): + PRIMARIES_BT709 = ... + PRIMARIES_UNSPECIFIED = ... + PRIMARIES_BT470_M = ... + PRIMARIES_BT470_BG = ... + PRIMARIES_ST170_M = ... + PRIMARIES_ST240_M = ... + PRIMARIES_FILM = ... + PRIMARIES_BT2020 = ... + PRIMARIES_ST428 = ... + PRIMARIES_ST431_2 = ... + PRIMARIES_ST432_1 = ... + PRIMARIES_EBU3213_E = ... + +PRIMARIES_BT709: Literal[ColorPrimaries.PRIMARIES_BT709] +PRIMARIES_UNSPECIFIED: Literal[ColorPrimaries.PRIMARIES_UNSPECIFIED] +PRIMARIES_BT470_M: Literal[ColorPrimaries.PRIMARIES_BT470_M] +PRIMARIES_BT470_BG: Literal[ColorPrimaries.PRIMARIES_BT470_BG] +PRIMARIES_ST170_M: Literal[ColorPrimaries.PRIMARIES_ST170_M] +PRIMARIES_ST240_M: Literal[ColorPrimaries.PRIMARIES_ST240_M] +PRIMARIES_FILM: Literal[ColorPrimaries.PRIMARIES_FILM] +PRIMARIES_BT2020: Literal[ColorPrimaries.PRIMARIES_BT2020] +PRIMARIES_ST428: Literal[ColorPrimaries.PRIMARIES_ST428] +PRIMARIES_ST431_2: Literal[ColorPrimaries.PRIMARIES_ST431_2] +PRIMARIES_ST432_1: Literal[ColorPrimaries.PRIMARIES_ST432_1] +PRIMARIES_EBU3213_E: Literal[ColorPrimaries.PRIMARIES_EBU3213_E] + +class _VideoFormatDict(TypedDict): + id: int + name: str + color_family: ColorFamily + sample_type: SampleType + bits_per_sample: Literal[ + 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 + ] + bytes_per_sample: int + subsampling_w: Literal[0, 1, 2, 3, 4] + subsampling_h: Literal[0, 1, 2, 3, 4] + num_planes: Literal[1, 3] + +class VideoFormat: + id: Final[int] + name: Final[str] + color_family: Final[ColorFamily] + sample_type: Final[SampleType] + bits_per_sample: Final[ + Literal[8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] + ] + bytes_per_sample: Final[int] + subsampling_w: Final[Literal[0, 1, 2, 3, 4]] + subsampling_h: Final[Literal[0, 1, 2, 3, 4]] + num_planes: Final[Literal[1, 3]] + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def __int__(self) -> int: ... + def replace( + self, + *, + color_family: ColorFamily = ..., + sample_type: SampleType = ..., + bits_per_sample: _IntLike = ..., + subsampling_w: _IntLike = ..., + subsampling_h: _IntLike = ..., + ) -> Self: ... + def _as_dict(self) -> _VideoFormatDict: ... + +# Behave like a Collection +class ChannelLayout(int): + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __contains__(self, layout: AudioChannels) -> bool: ... + def __iter__(self) -> Iterator[AudioChannels]: ... + def __len__(self) -> int: ... + +type _PropValue = ( + int + | float + | str + | bytes + | RawFrame + | VideoFrame + | AudioFrame + | RawNode + | VideoNode + | AudioNode + | Callable[..., Any] + | list[int] + | list[float] + | list[str] + | list[bytes] + | list[RawFrame] + | list[VideoFrame] + | list[AudioFrame] + | list[RawNode] + | list[VideoNode] + | list[AudioNode] + | list[Callable[..., Any]] +) + +# Only the _PropValue types are allowed in FrameProps but passing _VSValue is allowed. +# Just keep in mind that _SupportsIter and _GetItemIterable will only yield their keys if they're Mapping-like. +# Consider storing Mapping-likes as two separate props. One for the keys and one for the values as list. +class FrameProps(MutableMapping[str, _PropValue]): + def __repr__(self) -> str: ... + def __dir__(self) -> list[str]: ... + def __getitem__(self, name: str) -> _PropValue: ... + def __setitem__(self, name: str, value: _VSValue) -> None: ... + def __delitem__(self, name: str) -> None: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + def __setattr__(self, name: str, value: _VSValue) -> None: ... + def __delattr__(self, name: str) -> None: ... + def __getattr__(self, name: str) -> _PropValue: ... + @overload + def setdefault(self, key: str, default: Literal[0] = 0, /) -> _PropValue | Literal[0]: ... + @overload + def setdefault(self, key: str, default: _VSValue, /) -> _PropValue: ... # pyright: ignore[reportIncompatibleMethodOverride] + def copy(self) -> dict[str, _PropValue]: ... + +class FuncData: + def __call__(self, **kwargs: Any) -> Any: ... + +class Func: + def __call__(self, **kwargs: Any) -> Any: ... + +class Function: + plugin: Final[Plugin] + name: Final[str] + signature: Final[str] + return_signature: Final[str] + def __repr__(self) -> str: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + @property + def __signature__(self) -> Signature: ... + +class PluginVersion(NamedTuple): + major: int + minor: int + +class Plugin: + identifier: Final[str] + namespace: Final[str] + name: Final[str] + + def __repr__(self) -> str: ... + def __dir__(self) -> list[str]: ... + def __getattr__(self, name: str) -> Function: ... + @property + def version(self) -> PluginVersion: ... + @property + def plugin_path(self) -> str: ... + def functions(self) -> Iterator[Function]: ... + +_VSPlugin = Plugin +_VSFunction = Function + +class _Wrapper: + class Function[**_P, _R](_VSFunction): + def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, _P], _R]) -> None: ... + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... + +class _Wrapper_Core_bound_FrameEval: + class Function(_VSFunction): + def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, ...], VideoNode]) -> None: ... + @overload + def __call__( + self, + clip: VideoNode, + eval: _VSCallback_std_FrameEval_eval_0, + prop_src: None = None, + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + @overload + def __call__( + self, + clip: VideoNode, + eval: _VSCallback_std_FrameEval_eval_1, + prop_src: VideoNode, + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + @overload + def __call__( + self, + clip: VideoNode, + eval: _VSCallback_std_FrameEval_eval_2, + prop_src: _SequenceLike[VideoNode], + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + @overload + def __call__( + self, + clip: VideoNode, + eval: _VSCallback_std_FrameEval_eval_3, + prop_src: VideoNode | _SequenceLike[VideoNode], + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + @overload + def __call__( + self, + clip: VideoNode, + eval: _VSCallback_std_FrameEval_eval, + prop_src: VideoNode | _SequenceLike[VideoNode] | None, + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + +class _Wrapper_VideoNode_bound_FrameEval: + class Function(_VSFunction): + def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, ...], VideoNode]) -> None: ... + @overload + def __call__( + self, + eval: _VSCallback_std_FrameEval_eval_0, + prop_src: None = None, + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + @overload + def __call__( + self, + eval: _VSCallback_std_FrameEval_eval_1, + prop_src: VideoNode, + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + @overload + def __call__( + self, + eval: _VSCallback_std_FrameEval_eval_2, + prop_src: _SequenceLike[VideoNode], + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + @overload + def __call__( + self, + eval: _VSCallback_std_FrameEval_eval_3, + prop_src: VideoNode | _SequenceLike[VideoNode], + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + @overload + def __call__( + self, + eval: _VSCallback_std_FrameEval_eval, + prop_src: VideoNode | _SequenceLike[VideoNode] | None, + clip_src: VideoNode | _SequenceLike[VideoNode] | None = None, + ) -> VideoNode: ... + +class _Wrapper_Core_bound_ModifyFrame: + class Function(_VSFunction): + def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, ...], VideoNode]) -> None: ... + @overload + def __call__( + self, clip: VideoNode, clips: VideoNode, selector: _VSCallback_std_ModifyFrame_selector_0 + ) -> VideoNode: ... + @overload + def __call__( + self, clip: VideoNode, clips: _SequenceLike[VideoNode], selector: _VSCallback_std_ModifyFrame_selector_1 + ) -> VideoNode: ... + @overload + def __call__( + self, + clip: VideoNode, + clips: VideoNode | _SequenceLike[VideoNode], + selector: _VSCallback_std_ModifyFrame_selector, + ) -> VideoNode: ... + +class _Wrapper_VideoNode_bound_ModifyFrame: + class Function(_VSFunction): + def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, ...], VideoNode]) -> None: ... + @overload + def __call__(self, clips: VideoNode, selector: _VSCallback_std_ModifyFrame_selector_0) -> VideoNode: ... + @overload + def __call__( + self, clips: _SequenceLike[VideoNode], selector: _VSCallback_std_ModifyFrame_selector_1 + ) -> VideoNode: ... + @overload + def __call__( + self, clips: VideoNode | _SequenceLike[VideoNode], selector: _VSCallback_std_ModifyFrame_selector + ) -> VideoNode: ... + +class FramePtr: ... + +# These memoryview-likes don't exist at runtime. +class _video_view(memoryview): # type: ignore[misc] + def __getitem__(self, index: tuple[int, int]) -> float: ... # type: ignore[override] + def __setitem__(self, index: tuple[int, int], other: float) -> None: ... # type: ignore[override] + @property + def shape(self) -> tuple[int, int]: ... + @property + def strides(self) -> tuple[int, int]: ... + @property + def ndim(self) -> Literal[2]: ... + @property + def obj(self) -> FramePtr: ... # type: ignore[override] + def tolist(self) -> list[float]: ... # type: ignore[override] + +class _audio_view(memoryview): # type: ignore[misc] + def __getitem__(self, index: int) -> float: ... # type: ignore[override] + def __setitem__(self, index: int, other: float) -> None: ... # type: ignore[override] + @property + def shape(self) -> tuple[int]: ... + @property + def strides(self) -> tuple[int]: ... + @property + def ndim(self) -> Literal[1]: ... + @property + def obj(self) -> FramePtr: ... # type: ignore[override] + def tolist(self) -> list[float]: ... # type: ignore[override] + +class RawFrame: + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __enter__(self) -> Self: ... + def __exit__( + self, exc: type[BaseException] | None = None, val: BaseException | None = None, tb: TracebackType | None = None + ) -> bool | None: ... + def __getitem__(self, index: SupportsIndex) -> memoryview: ... + def __len__(self) -> int: ... + @property + def closed(self) -> bool: ... + @property + def props(self) -> FrameProps: ... + @props.setter + def props(self, new_props: _SupportsKeysAndGetItem[str, _VSValue]) -> None: ... + @property + def readonly(self) -> bool: ... + def copy(self) -> Self: ... + def close(self) -> None: ... + def get_write_ptr(self, plane: _IntLike) -> c_void_p: ... + def get_read_ptr(self, plane: _IntLike) -> c_void_p: ... + def get_stride(self, plane: _IntLike) -> int: ... + +# Behave like a Sequence +class VideoFrame(RawFrame): + format: Final[VideoFormat] + width: Final[int] + height: Final[int] + + def __getitem__(self, index: SupportsIndex) -> _video_view: ... + def readchunks(self) -> Iterator[_video_view]: ... + +# Behave like a Sequence +class AudioFrame(RawFrame): + sample_type: Final[SampleType] + bits_per_sample: Final[int] + bytes_per_sample: Final[int] + channel_layout: Final[int] + num_channels: Final[int] + + def __getitem__(self, index: SupportsIndex) -> _audio_view: ... + @property + def channels(self) -> ChannelLayout: ... + +class RawNode: + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def __dir__(self) -> list[str]: ... + def __getitem__(self, index: int | slice[int | None, int | None, int | None]) -> Self: ... + def __len__(self) -> int: ... + def __add__(self, other: Self) -> Self: ... + def __mul__(self, other: int) -> Self: ... + def __getattr__(self, name: str) -> Plugin: ... + @property + def node_name(self) -> str: ... + @property + def timings(self) -> int: ... + @timings.setter + def timings(self, value: Literal[0]) -> None: ... + @property + def mode(self) -> FilterMode: ... + @property + def dependencies(self) -> tuple[Self, ...]: ... + @property + def _name(self) -> str: ... + @property + def _inputs(self) -> dict[str, _VSValue]: ... + def get_frame(self, n: _IntLike) -> RawFrame: ... + @overload + def get_frame_async(self, n: _IntLike) -> Future[RawFrame]: ... + @overload + def get_frame_async(self, n: _IntLike, cb: Callable[[RawFrame | None, Exception | None], None]) -> None: ... + def frames( + self, prefetch: int | None = None, backlog: int | None = None, close: bool = False + ) -> Iterator[RawFrame]: ... + def set_output(self, index: _IntLike = 0) -> None: ... + def clear_cache(self) -> None: ... + def is_inspectable(self, version: int | None = None) -> bool: ... + +type _CurrentFrame = int +type _TotalFrames = int + +# Behave like a Sequence +class VideoNode(RawNode): + format: Final[VideoFormat] + width: Final[int] + height: Final[int] + num_frames: Final[int] + fps_num: Final[int] + fps_den: Final[int] + fps: Final[Fraction] + def get_frame(self, n: _IntLike) -> VideoFrame: ... + @overload # type: ignore[override] + def get_frame_async(self, n: _IntLike) -> Future[VideoFrame]: ... + @overload + def get_frame_async( # pyright: ignore[reportIncompatibleMethodOverride] + self, n: _IntLike, cb: Callable[[VideoFrame | None, Exception | None], None] + ) -> None: ... + def frames( + self, prefetch: int | None = None, backlog: int | None = None, close: bool = False + ) -> Iterator[VideoFrame]: ... + def set_output(self, index: _IntLike = 0, alpha: Self | None = None, alt_output: Literal[0, 1, 2] = 0) -> None: ... + def output( + self, + fileobj: IO[bytes], + y4m: bool = False, + progress_update: Callable[[_CurrentFrame, _TotalFrames], None] | None = None, + prefetch: int = 0, + backlog: int = -1, + ) -> None: ... + +# +# + resize: Final[_resize._VideoNode_bound.Plugin] + """VapourSynth Resize""" +# +# + std: Final[_std._VideoNode_bound.Plugin] + """VapourSynth Core Functions""" +# +# + +# Behave like a Sequence +class AudioNode(RawNode): + sample_type: Final[SampleType] + bits_per_sample: Final[int] + bytes_per_sample: Final[int] + channel_layout: Final[int] + num_channels: Final[int] + sample_rate: Final[int] + num_samples: Final[int] + num_frames: Final[int] + @property + def channels(self) -> ChannelLayout: ... + def get_frame(self, n: _IntLike) -> AudioFrame: ... + @overload # type: ignore[override] + def get_frame_async(self, n: _IntLike) -> Future[AudioFrame]: ... + @overload + def get_frame_async( # pyright: ignore[reportIncompatibleMethodOverride] + self, n: _IntLike, cb: Callable[[AudioFrame | None, Exception | None], None] + ) -> None: ... + def frames( + self, prefetch: int | None = None, backlog: int | None = None, close: bool = False + ) -> Iterator[AudioFrame]: ... + +# +# + std: Final[_std._AudioNode_bound.Plugin] + """VapourSynth Core Functions""" +# +# + +class Core: + timings: Final[CoreTimings] + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __dir__(self) -> list[str]: ... + def __getattr__(self, name: str) -> Plugin: ... + @property + def api_version(self) -> VapourSynthAPIVersion: ... + @property + def core_version(self) -> VapourSynthVersion: ... + @property + def num_threads(self) -> int: ... + @num_threads.setter + def num_threads(self, value: _IntLike) -> None: ... + @property + def max_cache_size(self) -> int: ... + @max_cache_size.setter + def max_cache_size(self, mb: _IntLike) -> None: ... + @property + def used_cache_size(self) -> int: ... + @property + def flags(self) -> int: ... + def plugins(self) -> Iterator[Plugin]: ... + def query_video_format( + self, + color_family: _IntLike, + sample_type: _IntLike, + bits_per_sample: _IntLike, + subsampling_w: _IntLike = 0, + subsampling_h: _IntLike = 0, + ) -> VideoFormat: ... + def get_video_format(self, id: _IntLike) -> VideoFormat: ... + def create_video_frame(self, format: VideoFormat, width: _IntLike, height: _IntLike) -> VideoFrame: ... + def log_message(self, message_type: _IntLike, message: str) -> None: ... + def add_log_handler(self, handler_func: Callable[[MessageType, str], None]) -> LogHandle: ... + def remove_log_handler(self, handle: LogHandle) -> None: ... + def clear_cache(self) -> None: ... + @deprecated("core.version() is deprecated, use str(core)!", category=DeprecationWarning) + def version(self) -> str: ... + @deprecated( + "core.version_number() is deprecated, use core.core_version.release_major!", category=DeprecationWarning + ) + def version_number(self) -> int: ... + +# +# + resize: Final[_resize._Core_bound.Plugin] + """VapourSynth Resize""" +# +# + std: Final[_std._Core_bound.Plugin] + """VapourSynth Core Functions""" +# +# + +# _CoreProxy doesn't inherit from Core but __getattr__ returns the attribute from the actual core +class _CoreProxy(Core): + def __setattr__(self, name: str, value: Any) -> None: ... + @property + def core(self) -> Core: ... + +core: _CoreProxy + +# +# +class _resize: + class _Core_bound: + class Plugin(_VSPlugin): + @_Wrapper.Function + def Bicubic(self, clip: VideoNode, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Bilinear(self, clip: VideoNode, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Bob(self, clip: VideoNode, filter: _AnyStr | None = None, tff: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Lanczos(self, clip: VideoNode, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Point(self, clip: VideoNode, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Spline16(self, clip: VideoNode, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Spline36(self, clip: VideoNode, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Spline64(self, clip: VideoNode, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + + class _VideoNode_bound: + class Plugin(_VSPlugin): + @_Wrapper.Function + def Bicubic(self, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Bilinear(self, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Bob(self, filter: _AnyStr | None = None, tff: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Lanczos(self, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Point(self, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Spline16(self, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Spline36(self, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Spline64(self, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, matrix: _IntLike | None = None, matrix_s: _AnyStr | None = None, transfer: _IntLike | None = None, transfer_s: _AnyStr | None = None, primaries: _IntLike | None = None, primaries_s: _AnyStr | None = None, range: _IntLike | None = None, range_s: _AnyStr | None = None, chromaloc: _IntLike | None = None, chromaloc_s: _AnyStr | None = None, matrix_in: _IntLike | None = None, matrix_in_s: _AnyStr | None = None, transfer_in: _IntLike | None = None, transfer_in_s: _AnyStr | None = None, primaries_in: _IntLike | None = None, primaries_in_s: _AnyStr | None = None, range_in: _IntLike | None = None, range_in_s: _AnyStr | None = None, chromaloc_in: _IntLike | None = None, chromaloc_in_s: _AnyStr | None = None, filter_param_a: _FloatLike | None = None, filter_param_b: _FloatLike | None = None, resample_filter_uv: _AnyStr | None = None, filter_param_a_uv: _FloatLike | None = None, filter_param_b_uv: _FloatLike | None = None, dither_type: _AnyStr | None = None, cpu_type: _AnyStr | None = None, prefer_props: _IntLike | None = None, src_left: _FloatLike | None = None, src_top: _FloatLike | None = None, src_width: _FloatLike | None = None, src_height: _FloatLike | None = None, nominal_luminance: _FloatLike | None = None, approximate_gamma: _IntLike | None = None) -> VideoNode: ... + +# + +# +class _std: + class _Core_bound: + class Plugin(_VSPlugin): + @_Wrapper.Function + def AddBorders(self, clip: VideoNode, left: _IntLike | None = None, right: _IntLike | None = None, top: _IntLike | None = None, bottom: _IntLike | None = None, color: _FloatLike | _SequenceLike[_FloatLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def AssumeFPS(self, clip: VideoNode, src: VideoNode | None = None, fpsnum: _IntLike | None = None, fpsden: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def AssumeSampleRate(self, clip: AudioNode, src: AudioNode | None = None, samplerate: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AudioGain(self, clip: AudioNode, gain: _FloatLike | _SequenceLike[_FloatLike] | None = None, overflow_error: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AudioLoop(self, clip: AudioNode, times: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AudioMix(self, clips: AudioNode | _SequenceLike[AudioNode], matrix: _FloatLike | _SequenceLike[_FloatLike], channels_out: _IntLike | _SequenceLike[_IntLike], overflow_error: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AudioReverse(self, clip: AudioNode) -> AudioNode: ... + @_Wrapper.Function + def AudioSplice(self, clips: AudioNode | _SequenceLike[AudioNode]) -> AudioNode: ... + @_Wrapper.Function + def AudioTrim(self, clip: AudioNode, first: _IntLike | None = None, last: _IntLike | None = None, length: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AverageFrames(self, clips: VideoNode | _SequenceLike[VideoNode], weights: _FloatLike | _SequenceLike[_FloatLike], scale: _FloatLike | None = None, scenechange: _IntLike | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Binarize(self, clip: VideoNode, threshold: _FloatLike | _SequenceLike[_FloatLike] | None = None, v0: _FloatLike | _SequenceLike[_FloatLike] | None = None, v1: _FloatLike | _SequenceLike[_FloatLike] | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def BinarizeMask(self, clip: VideoNode, threshold: _FloatLike | _SequenceLike[_FloatLike] | None = None, v0: _FloatLike | _SequenceLike[_FloatLike] | None = None, v1: _FloatLike | _SequenceLike[_FloatLike] | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def BlankAudio(self, clip: AudioNode | None = None, channels: _IntLike | _SequenceLike[_IntLike] | None = None, bits: _IntLike | None = None, sampletype: _IntLike | None = None, samplerate: _IntLike | None = None, length: _IntLike | None = None, keep: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def BlankClip(self, clip: VideoNode | None = None, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, length: _IntLike | None = None, fpsnum: _IntLike | None = None, fpsden: _IntLike | None = None, color: _FloatLike | _SequenceLike[_FloatLike] | None = None, keep: _IntLike | None = None, varsize: _IntLike | None = None, varformat: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def BoxBlur(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, hradius: _IntLike | None = None, hpasses: _IntLike | None = None, vradius: _IntLike | None = None, vpasses: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Cache(self, clip: VideoNode, size: _IntLike | None = None, fixed: _IntLike | None = None, make_linear: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def ClipToProp(self, clip: VideoNode, mclip: VideoNode, prop: _AnyStr | None = None) -> VideoNode: ... + @_Wrapper.Function + def Convolution(self, clip: VideoNode, matrix: _FloatLike | _SequenceLike[_FloatLike], bias: _FloatLike | None = None, divisor: _FloatLike | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None, saturate: _IntLike | None = None, mode: _AnyStr | None = None) -> VideoNode: ... + @_Wrapper.Function + def CopyFrameProps(self, clip: VideoNode, prop_src: VideoNode, props: _AnyStr | _SequenceLike[_AnyStr] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Crop(self, clip: VideoNode, left: _IntLike | None = None, right: _IntLike | None = None, top: _IntLike | None = None, bottom: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def CropAbs(self, clip: VideoNode, width: _IntLike, height: _IntLike, left: _IntLike | None = None, top: _IntLike | None = None, x: _IntLike | None = None, y: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def CropRel(self, clip: VideoNode, left: _IntLike | None = None, right: _IntLike | None = None, top: _IntLike | None = None, bottom: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Deflate(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, threshold: _FloatLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def DeleteFrames(self, clip: VideoNode, frames: _IntLike | _SequenceLike[_IntLike]) -> VideoNode: ... + @_Wrapper.Function + def DoubleWeave(self, clip: VideoNode, tff: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def DuplicateFrames(self, clip: VideoNode, frames: _IntLike | _SequenceLike[_IntLike]) -> VideoNode: ... + @_Wrapper.Function + def Expr(self, clips: VideoNode | _SequenceLike[VideoNode], expr: _AnyStr | _SequenceLike[_AnyStr], format: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def FlipHorizontal(self, clip: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def FlipVertical(self, clip: VideoNode) -> VideoNode: ... + @_Wrapper_Core_bound_FrameEval.Function + def FrameEval(self, clip: VideoNode, eval: _VSCallback_std_FrameEval_eval, prop_src: VideoNode | _SequenceLike[VideoNode] | None = None, clip_src: VideoNode | _SequenceLike[VideoNode] | None = None) -> VideoNode: ... + @_Wrapper.Function + def FreezeFrames(self, clip: VideoNode, first: _IntLike | _SequenceLike[_IntLike] | None = None, last: _IntLike | _SequenceLike[_IntLike] | None = None, replacement: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Inflate(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, threshold: _FloatLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Interleave(self, clips: VideoNode | _SequenceLike[VideoNode], extend: _IntLike | None = None, mismatch: _IntLike | None = None, modify_duration: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Invert(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def InvertMask(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Levels(self, clip: VideoNode, min_in: _FloatLike | _SequenceLike[_FloatLike] | None = None, max_in: _FloatLike | _SequenceLike[_FloatLike] | None = None, gamma: _FloatLike | _SequenceLike[_FloatLike] | None = None, min_out: _FloatLike | _SequenceLike[_FloatLike] | None = None, max_out: _FloatLike | _SequenceLike[_FloatLike] | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Limiter(self, clip: VideoNode, min: _FloatLike | _SequenceLike[_FloatLike] | None = None, max: _FloatLike | _SequenceLike[_FloatLike] | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def LoadAllPlugins(self, path: _AnyStr) -> None: ... + @_Wrapper.Function + def LoadPlugin(self, path: _AnyStr, altsearchpath: _IntLike | None = None, forcens: _AnyStr | None = None, forceid: _AnyStr | None = None) -> None: ... + @_Wrapper.Function + def Loop(self, clip: VideoNode, times: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Lut(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, lut: _IntLike | _SequenceLike[_IntLike] | None = None, lutf: _FloatLike | _SequenceLike[_FloatLike] | None = None, function: _VSCallback_std_Lut_function | None = None, bits: _IntLike | None = None, floatout: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Lut2(self, clipa: VideoNode, clipb: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, lut: _IntLike | _SequenceLike[_IntLike] | None = None, lutf: _FloatLike | _SequenceLike[_FloatLike] | None = None, function: _VSCallback_std_Lut2_function | None = None, bits: _IntLike | None = None, floatout: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def MakeDiff(self, clipa: VideoNode, clipb: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def MakeFullDiff(self, clipa: VideoNode, clipb: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def MaskedMerge(self, clipa: VideoNode, clipb: VideoNode, mask: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, first_plane: _IntLike | None = None, premultiplied: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Maximum(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, threshold: _FloatLike | None = None, coordinates: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Median(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Merge(self, clipa: VideoNode, clipb: VideoNode, weight: _FloatLike | _SequenceLike[_FloatLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def MergeDiff(self, clipa: VideoNode, clipb: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def MergeFullDiff(self, clipa: VideoNode, clipb: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def Minimum(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, threshold: _FloatLike | None = None, coordinates: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper_Core_bound_ModifyFrame.Function + def ModifyFrame(self, clip: VideoNode, clips: VideoNode | _SequenceLike[VideoNode], selector: _VSCallback_std_ModifyFrame_selector) -> VideoNode: ... + @_Wrapper.Function + def PEMVerifier(self, clip: VideoNode, upper: _FloatLike | _SequenceLike[_FloatLike] | None = None, lower: _FloatLike | _SequenceLike[_FloatLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def PlaneStats(self, clipa: VideoNode, clipb: VideoNode | None = None, plane: _IntLike | None = None, prop: _AnyStr | None = None) -> VideoNode: ... + @_Wrapper.Function + def PreMultiply(self, clip: VideoNode, alpha: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def Prewitt(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, scale: _FloatLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def PropToClip(self, clip: VideoNode, prop: _AnyStr | None = None) -> VideoNode: ... + @_Wrapper.Function + def RemoveFrameProps(self, clip: VideoNode, props: _AnyStr | _SequenceLike[_AnyStr] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Reverse(self, clip: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def SelectEvery(self, clip: VideoNode, cycle: _IntLike, offsets: _IntLike | _SequenceLike[_IntLike], modify_duration: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def SeparateFields(self, clip: VideoNode, tff: _IntLike | None = None, modify_duration: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def SetAudioCache(self, clip: AudioNode, mode: _IntLike | None = None, fixedsize: _IntLike | None = None, maxsize: _IntLike | None = None, maxhistory: _IntLike | None = None) -> None: ... + @_Wrapper.Function + def SetFieldBased(self, clip: VideoNode, value: _IntLike) -> VideoNode: ... + @_Wrapper.Function + def SetFrameProp(self, clip: VideoNode, prop: _AnyStr, intval: _IntLike | _SequenceLike[_IntLike] | None = None, floatval: _FloatLike | _SequenceLike[_FloatLike] | None = None, data: _AnyStr | _SequenceLike[_AnyStr] | None = None) -> VideoNode: ... + @_Wrapper.Function + def SetFrameProps(self, clip: VideoNode, **kwargs: Any) -> VideoNode: ... + @_Wrapper.Function + def SetMaxCPU(self, cpu: _AnyStr) -> _AnyStr: ... + @_Wrapper.Function + def SetVideoCache(self, clip: VideoNode, mode: _IntLike | None = None, fixedsize: _IntLike | None = None, maxsize: _IntLike | None = None, maxhistory: _IntLike | None = None) -> None: ... + @_Wrapper.Function + def ShuffleChannels(self, clips: AudioNode | _SequenceLike[AudioNode], channels_in: _IntLike | _SequenceLike[_IntLike], channels_out: _IntLike | _SequenceLike[_IntLike]) -> AudioNode: ... + @_Wrapper.Function + def ShufflePlanes(self, clips: VideoNode | _SequenceLike[VideoNode], planes: _IntLike | _SequenceLike[_IntLike], colorfamily: _IntLike, prop_src: VideoNode | None = None) -> VideoNode: ... + @_Wrapper.Function + def Sobel(self, clip: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, scale: _FloatLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Splice(self, clips: VideoNode | _SequenceLike[VideoNode], mismatch: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def SplitChannels(self, clip: AudioNode) -> AudioNode | list[AudioNode]: ... + @_Wrapper.Function + def SplitPlanes(self, clip: VideoNode) -> VideoNode | list[VideoNode]: ... + @_Wrapper.Function + def StackHorizontal(self, clips: VideoNode | _SequenceLike[VideoNode]) -> VideoNode: ... + @_Wrapper.Function + def StackVertical(self, clips: VideoNode | _SequenceLike[VideoNode]) -> VideoNode: ... + @_Wrapper.Function + def TestAudio(self, channels: _IntLike | _SequenceLike[_IntLike] | None = None, bits: _IntLike | None = None, isfloat: _IntLike | None = None, samplerate: _IntLike | None = None, length: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def Transpose(self, clip: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def Trim(self, clip: VideoNode, first: _IntLike | None = None, last: _IntLike | None = None, length: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Turn180(self, clip: VideoNode) -> VideoNode: ... + + class _VideoNode_bound: + class Plugin(_VSPlugin): + @_Wrapper.Function + def AddBorders(self, left: _IntLike | None = None, right: _IntLike | None = None, top: _IntLike | None = None, bottom: _IntLike | None = None, color: _FloatLike | _SequenceLike[_FloatLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def AssumeFPS(self, src: VideoNode | None = None, fpsnum: _IntLike | None = None, fpsden: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def AverageFrames(self, weights: _FloatLike | _SequenceLike[_FloatLike], scale: _FloatLike | None = None, scenechange: _IntLike | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Binarize(self, threshold: _FloatLike | _SequenceLike[_FloatLike] | None = None, v0: _FloatLike | _SequenceLike[_FloatLike] | None = None, v1: _FloatLike | _SequenceLike[_FloatLike] | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def BinarizeMask(self, threshold: _FloatLike | _SequenceLike[_FloatLike] | None = None, v0: _FloatLike | _SequenceLike[_FloatLike] | None = None, v1: _FloatLike | _SequenceLike[_FloatLike] | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def BlankClip(self, width: _IntLike | None = None, height: _IntLike | None = None, format: _IntLike | None = None, length: _IntLike | None = None, fpsnum: _IntLike | None = None, fpsden: _IntLike | None = None, color: _FloatLike | _SequenceLike[_FloatLike] | None = None, keep: _IntLike | None = None, varsize: _IntLike | None = None, varformat: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def BoxBlur(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None, hradius: _IntLike | None = None, hpasses: _IntLike | None = None, vradius: _IntLike | None = None, vpasses: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Cache(self, size: _IntLike | None = None, fixed: _IntLike | None = None, make_linear: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def ClipToProp(self, mclip: VideoNode, prop: _AnyStr | None = None) -> VideoNode: ... + @_Wrapper.Function + def Convolution(self, matrix: _FloatLike | _SequenceLike[_FloatLike], bias: _FloatLike | None = None, divisor: _FloatLike | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None, saturate: _IntLike | None = None, mode: _AnyStr | None = None) -> VideoNode: ... + @_Wrapper.Function + def CopyFrameProps(self, prop_src: VideoNode, props: _AnyStr | _SequenceLike[_AnyStr] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Crop(self, left: _IntLike | None = None, right: _IntLike | None = None, top: _IntLike | None = None, bottom: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def CropAbs(self, width: _IntLike, height: _IntLike, left: _IntLike | None = None, top: _IntLike | None = None, x: _IntLike | None = None, y: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def CropRel(self, left: _IntLike | None = None, right: _IntLike | None = None, top: _IntLike | None = None, bottom: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Deflate(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None, threshold: _FloatLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def DeleteFrames(self, frames: _IntLike | _SequenceLike[_IntLike]) -> VideoNode: ... + @_Wrapper.Function + def DoubleWeave(self, tff: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def DuplicateFrames(self, frames: _IntLike | _SequenceLike[_IntLike]) -> VideoNode: ... + @_Wrapper.Function + def Expr(self, expr: _AnyStr | _SequenceLike[_AnyStr], format: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def FlipHorizontal(self) -> VideoNode: ... + @_Wrapper.Function + def FlipVertical(self) -> VideoNode: ... + @_Wrapper_VideoNode_bound_FrameEval.Function + def FrameEval(self, eval: _VSCallback_std_FrameEval_eval, prop_src: VideoNode | _SequenceLike[VideoNode] | None = None, clip_src: VideoNode | _SequenceLike[VideoNode] | None = None) -> VideoNode: ... + @_Wrapper.Function + def FreezeFrames(self, first: _IntLike | _SequenceLike[_IntLike] | None = None, last: _IntLike | _SequenceLike[_IntLike] | None = None, replacement: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Inflate(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None, threshold: _FloatLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Interleave(self, extend: _IntLike | None = None, mismatch: _IntLike | None = None, modify_duration: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Invert(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def InvertMask(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Levels(self, min_in: _FloatLike | _SequenceLike[_FloatLike] | None = None, max_in: _FloatLike | _SequenceLike[_FloatLike] | None = None, gamma: _FloatLike | _SequenceLike[_FloatLike] | None = None, min_out: _FloatLike | _SequenceLike[_FloatLike] | None = None, max_out: _FloatLike | _SequenceLike[_FloatLike] | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Limiter(self, min: _FloatLike | _SequenceLike[_FloatLike] | None = None, max: _FloatLike | _SequenceLike[_FloatLike] | None = None, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Loop(self, times: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Lut(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None, lut: _IntLike | _SequenceLike[_IntLike] | None = None, lutf: _FloatLike | _SequenceLike[_FloatLike] | None = None, function: _VSCallback_std_Lut_function | None = None, bits: _IntLike | None = None, floatout: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Lut2(self, clipb: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, lut: _IntLike | _SequenceLike[_IntLike] | None = None, lutf: _FloatLike | _SequenceLike[_FloatLike] | None = None, function: _VSCallback_std_Lut2_function | None = None, bits: _IntLike | None = None, floatout: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def MakeDiff(self, clipb: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def MakeFullDiff(self, clipb: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def MaskedMerge(self, clipb: VideoNode, mask: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None, first_plane: _IntLike | None = None, premultiplied: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Maximum(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None, threshold: _FloatLike | None = None, coordinates: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Median(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Merge(self, clipb: VideoNode, weight: _FloatLike | _SequenceLike[_FloatLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def MergeDiff(self, clipb: VideoNode, planes: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def MergeFullDiff(self, clipb: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def Minimum(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None, threshold: _FloatLike | None = None, coordinates: _IntLike | _SequenceLike[_IntLike] | None = None) -> VideoNode: ... + @_Wrapper_VideoNode_bound_ModifyFrame.Function + def ModifyFrame(self, clips: VideoNode | _SequenceLike[VideoNode], selector: _VSCallback_std_ModifyFrame_selector) -> VideoNode: ... + @_Wrapper.Function + def PEMVerifier(self, upper: _FloatLike | _SequenceLike[_FloatLike] | None = None, lower: _FloatLike | _SequenceLike[_FloatLike] | None = None) -> VideoNode: ... + @_Wrapper.Function + def PlaneStats(self, clipb: VideoNode | None = None, plane: _IntLike | None = None, prop: _AnyStr | None = None) -> VideoNode: ... + @_Wrapper.Function + def PreMultiply(self, alpha: VideoNode) -> VideoNode: ... + @_Wrapper.Function + def Prewitt(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None, scale: _FloatLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def PropToClip(self, prop: _AnyStr | None = None) -> VideoNode: ... + @_Wrapper.Function + def RemoveFrameProps(self, props: _AnyStr | _SequenceLike[_AnyStr] | None = None) -> VideoNode: ... + @_Wrapper.Function + def Reverse(self) -> VideoNode: ... + @_Wrapper.Function + def SelectEvery(self, cycle: _IntLike, offsets: _IntLike | _SequenceLike[_IntLike], modify_duration: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def SeparateFields(self, tff: _IntLike | None = None, modify_duration: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def SetFieldBased(self, value: _IntLike) -> VideoNode: ... + @_Wrapper.Function + def SetFrameProp(self, prop: _AnyStr, intval: _IntLike | _SequenceLike[_IntLike] | None = None, floatval: _FloatLike | _SequenceLike[_FloatLike] | None = None, data: _AnyStr | _SequenceLike[_AnyStr] | None = None) -> VideoNode: ... + @_Wrapper.Function + def SetFrameProps(self, **kwargs: Any) -> VideoNode: ... + @_Wrapper.Function + def SetVideoCache(self, mode: _IntLike | None = None, fixedsize: _IntLike | None = None, maxsize: _IntLike | None = None, maxhistory: _IntLike | None = None) -> None: ... + @_Wrapper.Function + def ShufflePlanes(self, planes: _IntLike | _SequenceLike[_IntLike], colorfamily: _IntLike, prop_src: VideoNode | None = None) -> VideoNode: ... + @_Wrapper.Function + def Sobel(self, planes: _IntLike | _SequenceLike[_IntLike] | None = None, scale: _FloatLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Splice(self, mismatch: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def SplitPlanes(self) -> VideoNode | list[VideoNode]: ... + @_Wrapper.Function + def StackHorizontal(self) -> VideoNode: ... + @_Wrapper.Function + def StackVertical(self) -> VideoNode: ... + @_Wrapper.Function + def Transpose(self) -> VideoNode: ... + @_Wrapper.Function + def Trim(self, first: _IntLike | None = None, last: _IntLike | None = None, length: _IntLike | None = None) -> VideoNode: ... + @_Wrapper.Function + def Turn180(self) -> VideoNode: ... + + class _AudioNode_bound: + class Plugin(_VSPlugin): + @_Wrapper.Function + def AssumeSampleRate(self, src: AudioNode | None = None, samplerate: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AudioGain(self, gain: _FloatLike | _SequenceLike[_FloatLike] | None = None, overflow_error: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AudioLoop(self, times: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AudioMix(self, matrix: _FloatLike | _SequenceLike[_FloatLike], channels_out: _IntLike | _SequenceLike[_IntLike], overflow_error: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def AudioReverse(self) -> AudioNode: ... + @_Wrapper.Function + def AudioSplice(self) -> AudioNode: ... + @_Wrapper.Function + def AudioTrim(self, first: _IntLike | None = None, last: _IntLike | None = None, length: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def BlankAudio(self, channels: _IntLike | _SequenceLike[_IntLike] | None = None, bits: _IntLike | None = None, sampletype: _IntLike | None = None, samplerate: _IntLike | None = None, length: _IntLike | None = None, keep: _IntLike | None = None) -> AudioNode: ... + @_Wrapper.Function + def SetAudioCache(self, mode: _IntLike | None = None, fixedsize: _IntLike | None = None, maxsize: _IntLike | None = None, maxhistory: _IntLike | None = None) -> None: ... + @_Wrapper.Function + def ShuffleChannels(self, channels_in: _IntLike | _SequenceLike[_IntLike], channels_out: _IntLike | _SequenceLike[_IntLike]) -> AudioNode: ... + @_Wrapper.Function + def SplitChannels(self) -> AudioNode | list[AudioNode]: ... + +# + +# + +class VideoOutputTuple(NamedTuple): + clip: VideoNode + alpha: VideoNode | None + alt_output: Literal[0, 1, 2] + +def clear_output(index: _IntLike = 0) -> None: ... +def clear_outputs() -> None: ... +def get_outputs() -> MappingProxyType[int, VideoOutputTuple | AudioNode]: ... +def get_output(index: _IntLike = 0) -> VideoOutputTuple | AudioNode: ... + +def construct_signature( + signature: str | Function, return_signature: str, injected: bool | None = None, name: str | None = None +) -> Signature: ... +def _construct_type(signature: str) -> Any: ... +def _construct_parameter(signature: str) -> Any: ... +def _construct_repr_wrap(value: str | Enum | VideoFormat | Iterator[str]) -> str: ... +def _construct_repr(obj: Any, **kwargs: Any) -> str: ... diff --git a/tests/test_convert.py b/tests/test_convert.py index 7391216..324b0d1 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -19,7 +19,7 @@ # mediainfo -Output=JOSN -Full [Filenames] # | jq '.[].media.track[] | select(."@type" == "Video") | {matrix: .matrix_coefficients, width: .Width, height: .Height, primaries: .colour_primaries, transfer: .transfer_characteristics, chromaloc: .ChromaSubsampling_Position} | select(.matrix)' | jq -s PATH = os.path.join(DIR, "fixtures", "heuristic_examples.json") -with open(PATH, "r") as h: +with open(PATH) as h: HEURISTIC_EXAMPLES = json.load(h) MATRIX_MAPPING = { diff --git a/tests/test_video.py b/tests/test_video.py index ab7083e..08cef9d 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -2,7 +2,7 @@ # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import typing as t +from typing import * import unittest from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy @@ -13,7 +13,7 @@ from vsengine.video import frame, planes, frames, render -AnyFormat = t.Union[PresetVideoFormat, VideoFormat] +AnyFormat = Union[PresetVideoFormat, VideoFormat] class TestVideo(unittest.TestCase): @@ -27,17 +27,19 @@ def tearDown(self) -> None: @staticmethod def generate_video(length: int = 3, width: int = 1, height: int = 1, format: AnyFormat = GRAY8) -> VideoNode: clip = core.std.BlankClip(length=length, width=width, height=height, format=format, fpsden=1001, fpsnum=24000) + def _add_frameno(n: int, f: VideoFrame) -> VideoFrame: fout = f.copy() fout.props["FrameNumber"] = n return fout + clip = core.std.ModifyFrame(clip=clip, clips=clip, selector=_add_frameno) return clip def test_planes(self): clipA = core.std.BlankClip(length=1, color=[0, 1, 2], width=1, height=1, format=RGB24) clipB = core.std.BlankClip(length=1, color=[3, 4, 5], width=1, height=1, format=RGB24) - + clip = core.std.Splice([clipA, clipB]) self.assertEqual(planes(clip, 0).result(), [b"\x00", b"\x01", b"\x02"]) @@ -55,7 +57,7 @@ def test_planes(self): def test_planes_default_supports_multiformat_clips(self): clipA = core.std.BlankClip(length=1, color=[0, 1, 2], width=1, height=1, format=RGB24) clipB = core.std.BlankClip(length=1, color=[3], width=1, height=1, format=GRAY8) - + clip = core.std.Splice([clipA, clipB], mismatch=True) self.assertEqual(planes(clip, 0).result(), [b"\x00", b"\x01", b"\x02"]) self.assertEqual(planes(clip, 1).result(), [b"\x03"]) @@ -67,7 +69,7 @@ def test_single_frame(self): with frame(clip, 1).result(timeout=0.1) as f: self.assertEqual(f.props["FrameNumber"], 1) - + with frame(clip, 2).result(timeout=0.1) as f: self.assertEqual(f.props["FrameNumber"], 2) @@ -94,7 +96,7 @@ def test_multiple_frames_closes_after_iteration(self): finally: f2.close() next(it).close() - + def test_multiple_frames_without_closing(self): clip = self.generate_video() for nf, f in enumerate(frames(clip, close=False)): @@ -103,11 +105,10 @@ def test_multiple_frames_without_closing(self): def test_render(self): clip = self.generate_video() - data = b"".join((f[1] for f in render(clip))) + data = b"".join(f[1] for f in render(clip)) self.assertEqual(data, b"\0\0\0") def test_render_y4m(self): clip = self.generate_video() - data = b"".join((f[1] for f in render(clip, y4m=True))) + data = b"".join(f[1] for f in render(clip, y4m=True)) self.assertEqual(data, b"YUV4MPEG2 Cmono W1 H1 F24000:1001 Ip A0:0 XLENGTH=3\nFRAME\n\0FRAME\n\0FRAME\n\0") - diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d2fca56 --- /dev/null +++ b/uv.lock @@ -0,0 +1,346 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/c3/cdff3c10e2e608490dc0a310ccf11ba777b3943ad4fcead2a2ade98c21e1/librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae", size = 54209, upload-time = "2025-11-29T14:01:56.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/2c/b59249c566f98fe90e178baf59e83f628d6c38fb8bc78319301fccda0b5e/librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176", size = 27841, upload-time = "2025-11-29T14:00:58.925Z" }, + { url = "https://files.pythonhosted.org/packages/40/e8/9db01cafcd1a2872b76114c858f81cc29ce7ad606bc102020d6dabf470fb/librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057", size = 27844, upload-time = "2025-11-29T14:01:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/59/4d/da449d3a7d83cc853af539dee42adc37b755d7eea4ad3880bacfd84b651d/librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610", size = 84091, upload-time = "2025-11-29T14:01:01.118Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6c/f90306906fb6cc6eaf4725870f0347115de05431e1f96d35114392d31fda/librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c", size = 88239, upload-time = "2025-11-29T14:01:02.11Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ae/473ce7b423cfac2cb503851a89d9d2195bf615f534d5912bf86feeebbee7/librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9", size = 88815, upload-time = "2025-11-29T14:01:03.114Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6d/934df738c87fb9617cabefe4891eece585a06abe6def25b4bca3b174429d/librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c", size = 90598, upload-time = "2025-11-29T14:01:04.071Z" }, + { url = "https://files.pythonhosted.org/packages/72/89/eeaa124f5e0f431c2b39119550378ae817a4b1a3c93fd7122f0639336fff/librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c", size = 88603, upload-time = "2025-11-29T14:01:05.02Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ed/c60b3c1cfc27d709bc0288af428ce58543fcb5053cf3eadbc773c24257f5/librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf", size = 92112, upload-time = "2025-11-29T14:01:06.304Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/f56169be5f716ef4ab0277be70bcb1874b4effc262e655d85b505af4884d/librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d", size = 20127, upload-time = "2025-11-29T14:01:07.283Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/222750ce82bf95125529eaab585ac7e2829df252f3cfc05d68792fb1dd2c/librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01", size = 21545, upload-time = "2025-11-29T14:01:08.184Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/f731ddcfb72f446a92a8674c6b8e1e2242773cce43a04f41549bd8b958ff/librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55", size = 20946, upload-time = "2025-11-29T14:01:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/3055dd440f8b8b3b7e8624539a0749dd8e1913e978993bcca9ce7e306231/librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3", size = 27874, upload-time = "2025-11-29T14:01:10.615Z" }, + { url = "https://files.pythonhosted.org/packages/ef/93/226d7dd455eaa4c26712b5ccb2dfcca12831baa7f898c8ffd3a831e29fda/librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5", size = 27852, upload-time = "2025-11-29T14:01:11.535Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8b/db9d51191aef4e4cc06285250affe0bb0ad8b2ed815f7ca77951655e6f02/librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42", size = 84264, upload-time = "2025-11-29T14:01:12.461Z" }, + { url = "https://files.pythonhosted.org/packages/8d/53/297c96bda3b5a73bdaf748f1e3ae757edd29a0a41a956b9c10379f193417/librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70", size = 88432, upload-time = "2025-11-29T14:01:13.405Z" }, + { url = "https://files.pythonhosted.org/packages/54/3a/c005516071123278e340f22de72fa53d51e259d49215295c212da16c4dc2/librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522", size = 89014, upload-time = "2025-11-29T14:01:14.373Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9b/ea715f818d926d17b94c80a12d81a79e95c44f52848e61e8ca1ff29bb9a9/librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30", size = 90807, upload-time = "2025-11-29T14:01:15.377Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fc/4e2e4c87e002fa60917a8e474fd13c4bac9a759df82be3778573bb1ab954/librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917", size = 88890, upload-time = "2025-11-29T14:01:16.633Z" }, + { url = "https://files.pythonhosted.org/packages/70/7f/c7428734fbdfd4db3d5b9237fc3a857880b2ace66492836f6529fef25d92/librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530", size = 92300, upload-time = "2025-11-29T14:01:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0c/738c4824fdfe74dc0f95d5e90ef9e759d4ecf7fd5ba964d54a7703322251/librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3", size = 20159, upload-time = "2025-11-29T14:01:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/f2/95/93d0e61bc617306ecf4c54636b5cbde4947d872563565c4abdd9d07a39d3/librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88", size = 21484, upload-time = "2025-11-29T14:01:19.506Z" }, + { url = "https://files.pythonhosted.org/packages/10/23/abd7ace79ab54d1dbee265f13529266f686a7ce2d21ab59a992f989009b6/librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9", size = 20935, upload-time = "2025-11-29T14:01:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/83/14/c06cb31152182798ed98be73f54932ab984894f5a8fccf9b73130897a938/librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a", size = 27566, upload-time = "2025-11-29T14:01:21.609Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/ce83ca7b057b06150519152f53a0b302d7c33c8692ce2f01f669b5a819d9/librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd", size = 27753, upload-time = "2025-11-29T14:01:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ec/739a885ef0a2839b6c25f1b01c99149d2cb6a34e933ffc8c051fcd22012e/librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b", size = 83178, upload-time = "2025-11-29T14:01:23.555Z" }, + { url = "https://files.pythonhosted.org/packages/db/bd/dc18bb1489d48c0911b9f4d72eae2d304ea264e215ba80f1e6ba4a9fc41d/librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc", size = 87266, upload-time = "2025-11-29T14:01:24.532Z" }, + { url = "https://files.pythonhosted.org/packages/94/f3/d0c5431b39eef15e48088b2d739ad84b17c2f1a22c0345c6d4c4a42b135e/librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3", size = 87623, upload-time = "2025-11-29T14:01:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/9a52e90834e4bd6ee16cdbaf551cb32227cbaad27398391a189c489318bc/librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751", size = 89436, upload-time = "2025-11-29T14:01:26.769Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8a/a7e78e46e8486e023c50f21758930ef4793999115229afd65de69e94c9cc/librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8", size = 87540, upload-time = "2025-11-29T14:01:27.756Z" }, + { url = "https://files.pythonhosted.org/packages/49/01/93799044a1cccac31f1074b07c583e181829d240539657e7f305ae63ae2a/librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e", size = 90597, upload-time = "2025-11-29T14:01:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/a7/29/00c7f58b8f8eb1bad6529ffb6c9cdcc0890a27dac59ecda04f817ead5277/librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf", size = 18955, upload-time = "2025-11-29T14:01:30.325Z" }, + { url = "https://files.pythonhosted.org/packages/d7/13/2739e6e197a9f751375a37908a6a5b0bff637b81338497a1bcb5817394da/librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2", size = 20263, upload-time = "2025-11-29T14:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/e1/73/393868fc2158705ea003114a24e73bb10b03bda31e9ad7b5c5ec6575338b/librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78", size = 19575, upload-time = "2025-11-29T14:01:32.229Z" }, + { url = "https://files.pythonhosted.org/packages/48/6d/3c8ff3dec21bf804a205286dd63fd28dcdbe00b8dd7eb7ccf2e21a40a0b0/librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179", size = 28732, upload-time = "2025-11-29T14:01:33.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/e214b8b4aa34ed3d3f1040719c06c4d22472c40c5ef81a922d5af7876eb4/librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7", size = 29065, upload-time = "2025-11-29T14:01:34.088Z" }, + { url = "https://files.pythonhosted.org/packages/ab/90/ef61ed51f0a7770cc703422d907a757bbd8811ce820c333d3db2fd13542a/librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916", size = 93703, upload-time = "2025-11-29T14:01:35.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/c30bb119c35962cbe9a908a71da99c168056fc3f6e9bbcbc157d0b724d89/librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8", size = 98890, upload-time = "2025-11-29T14:01:36.031Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/47a4a78d252d36f072b79d592df10600d379a895c3880c8cbd2ac699f0ad/librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc", size = 98255, upload-time = "2025-11-29T14:01:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/e5/28/779b5cc3cd9987683884eb5f5672e3251676bebaaae6b7da1cf366eb1da1/librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07", size = 100769, upload-time = "2025-11-29T14:01:38.413Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/771755e57c375cb9d25a4e106f570607fd856e2cb91b02418db1db954796/librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f", size = 98580, upload-time = "2025-11-29T14:01:39.459Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ec/8b157eb8fbc066339a2f34b0aceb2028097d0ed6150a52e23284a311eafe/librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0", size = 101706, upload-time = "2025-11-29T14:01:40.474Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/4aaead9a06c795a318282aebf7d3e3e578fa889ff396e1b640c3be4c7806/librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a", size = 19465, upload-time = "2025-11-29T14:01:41.77Z" }, + { url = "https://files.pythonhosted.org/packages/3a/61/b7e6a02746c1731670c19ba07d86da90b1ae45d29e405c0b5615abf97cde/librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf", size = 21042, upload-time = "2025-11-29T14:01:42.865Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246, upload-time = "2025-11-29T14:01:44.13Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, + { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" }, + { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" }, + { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" }, + { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" }, + { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "trio" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "vapoursynth" +version = "73" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/29/85866b3c00b9f7e4b34007da02e2db286cc39370a0823108ce204ea5cc6b/vapoursynth-73.tar.gz", hash = "sha256:d52b29f37617a594c5dd1580e7831a08800d5fb5ae8fc8e833fa70bdf1178cc3", size = 67402, upload-time = "2025-11-24T16:58:32.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/e2/d621d4d4076277b98a244c38eb4bc3790fda3a3c66e5165fca4f6c2317a3/vapoursynth-73-cp312-abi3-win_amd64.whl", hash = "sha256:6f6543b2e3b62be14d23145ad8d14319b1eaf56cb9babd8fae2068411d31bc72", size = 1089499, upload-time = "2025-11-24T16:58:28.9Z" }, +] + +[[package]] +name = "vsengine" +version = "0.2.0+jet" +source = { editable = "." } +dependencies = [ + { name = "vapoursynth" }, +] + +[package.optional-dependencies] +test = [ + { name = "pytest" }, +] +trio = [ + { name = "trio" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "ruff" }, + { name = "trio" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'test'" }, + { name = "trio", marker = "extra == 'trio'" }, + { name = "vapoursynth", specifier = ">=69" }, +] +provides-extras = ["trio", "test"] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.19.0" }, + { name = "ruff", specifier = ">=0.14.7" }, + { name = "trio" }, +] diff --git a/vsengine/_futures.py b/vsengine/_futures.py index 3587c32..375a658 100644 --- a/vsengine/_futures.py +++ b/vsengine/_futures.py @@ -2,76 +2,80 @@ # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import inspect -import functools -import typing as t +from __future__ import annotations + +from collections.abc import AsyncIterator, Awaitable, Callable, Generator, Iterator from concurrent.futures import Future +from contextlib import AbstractAsyncContextManager, AbstractContextManager +from functools import wraps +from inspect import isgeneratorfunction +from types import TracebackType +from typing import Any, Self from vsengine.loops import Cancelled, get_loop, keep_environment +type UnifiedRunner[T, **P] = Callable[P, Future[T] | Iterator[Future[T]]] +type UnifiedCallable[T] = Callable[..., UnifiedFuture[T] | UnifiedIterator[T]] -T = t.TypeVar("T") -V = t.TypeVar("V") - -UnifiedRunner = t.Callable[..., t.Union[Future[T],t.Iterator[Future[T]]]] -UnifiedCallable = t.Callable[..., t.Union['UnifiedFuture', 'UnifiedIterator']] - - -class UnifiedFuture(Future[T]): +class UnifiedFuture[T]( + Future[T], AbstractContextManager[Any, Any], AbstractAsyncContextManager[Any, Any], Awaitable[T] +): @classmethod - def from_call(cls, func: UnifiedRunner[T], *args: t.Any, **kwargs: t.Any) -> 'UnifiedFuture[T]': + def from_call[**P](cls, func: UnifiedRunner[T, P], *args: P.args, **kwargs: P.kwargs) -> UnifiedFuture[T]: try: future = func(*args, **kwargs) except Exception as e: return cls.reject(e) - else: - return cls.from_future(t.cast(Future[T], future)) + + return cls.from_future(future) # type: ignore @classmethod - def from_future(cls, future: Future[T]) -> 'UnifiedFuture[T]': + def from_future(cls, future: Future[T]) -> UnifiedFuture[T]: if isinstance(future, cls): return future result = cls() - def _receive(_): + + def _receive(fn: Future[T]) -> None: if (exc := future.exception()) is not None: result.set_exception(exc) else: result.set_result(future.result()) + future.add_done_callback(_receive) return result @classmethod - def resolve(cls, value: T) -> 'UnifiedFuture[T]': + def resolve(cls, value: T) -> UnifiedFuture[T]: future = cls() future.set_result(value) return future @classmethod - def reject(cls, error: BaseException) -> 'UnifiedFuture[t.Any]': + def reject(cls, error: BaseException) -> UnifiedFuture[Any]: future = cls() future.set_exception(error) return future # Adding callbacks - def add_done_callback(self, fn: t.Callable[[Future[T]], t.Any]) -> None: + def add_done_callback(self, fn: Callable[[Future[T]], Any]) -> None: # The done_callback should inherit the environment of the current call. super().add_done_callback(keep_environment(fn)) - def add_loop_callback(self, func: t.Callable[['UnifiedFuture[T]'], None]) -> None: - def _wrapper(future): + def add_loop_callback(self, func: Callable[[UnifiedFuture[T]], None]) -> None: + def _wrapper(future: Future[T]) -> None: get_loop().from_thread(func, future) + self.add_done_callback(_wrapper) # Manipulating futures - def then( - self, - success_cb: t.Optional[t.Callable[[T], V]], - err_cb: t.Optional[t.Callable[[BaseException], V]] - ) -> 'UnifiedFuture[V]': - result = UnifiedFuture() - def _run_cb(cb, v): + def then[V]( + self, success_cb: Callable[[T], V] | None, err_cb: Callable[[BaseException], V] | None + ) -> UnifiedFuture[V]: + result = UnifiedFuture[V]() + + def _run_cb[T0](cb: Callable[[T0], V], v: T0) -> None: try: r = cb(v) except BaseException as e: @@ -79,7 +83,7 @@ def _run_cb(cb, v): else: result.set_result(r) - def _done(_): + def _done(fn: Future[T]) -> None: if (exc := self.exception()) is not None: if err_cb is not None: _run_cb(err_cb, exc) @@ -89,83 +93,87 @@ def _done(_): if success_cb is not None: _run_cb(success_cb, self.result()) else: - result.set_result(self.result()) + result.set_result(self.result()) # type: ignore self.add_done_callback(_done) return result - def map(self, cb: t.Callable[[T], V]) -> 'UnifiedFuture[V]': + def map[V](self, cb: Callable[[T], V]) -> UnifiedFuture[V]: return self.then(cb, None) - def catch(self, cb: t.Callable[[BaseException], V]) -> 'UnifiedFuture[V]': + def catch[V](self, cb: Callable[[BaseException], V]) -> UnifiedFuture[V]: return self.then(None, cb) # Nicer Syntax - def __enter__(self): + def __enter__(self) -> None: obj = self.result() - if hasattr(obj, "__enter__"): - return t.cast(t.ContextManager[t.Any], obj).__enter__() - else: - raise NotImplementedError("(async) with is not implemented for this object.") - def __exit__(self, exc, val, tb): + if isinstance(obj, AbstractContextManager): + return obj.__enter__() + + raise NotImplementedError("(async) with is not implemented for this objec") + + def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: obj = self.result() - if hasattr(obj, "__exit__"): - return t.cast(t.ContextManager[t.Any], obj).__exit__(exc, val, tb) - else: - raise NotImplementedError("(async) with is not implemented for this object.") - async def awaitable(self): + if isinstance(obj, AbstractContextManager): + return obj.__exit__(exc, val, tb) + + raise NotImplementedError("(async) with is not implemented for this objec") + + async def awaitable(self) -> T: return await get_loop().await_future(self) - def __await__(self): + def __await__(self) -> Generator[Any, None, T]: return self.awaitable().__await__() - async def __aenter__(self): + async def __aenter__(self) -> T: result = await self.awaitable() - if hasattr(result, "__aenter__"): - return await t.cast(t.AsyncContextManager[t.Any], result).__aenter__() - elif hasattr(result, "__enter__"): - return t.cast(t.ContextManager[t.Any], result).__enter__() - else: - raise NotImplementedError("(async) with is not implemented for this object.") - - async def __aexit__(self, exc, val, tb): + + if isinstance(result, AbstractAsyncContextManager): + return await result.__aenter__() + if isinstance(result, AbstractContextManager): + return result.__enter__() + + raise NotImplementedError("(async) with is not implemented for this objec") + + async def __aexit__( + self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None + ) -> None: result = await self.awaitable() - if hasattr(result, "__aexit__"): - return await t.cast(t.AsyncContextManager[t.Any], result).__aexit__(exc, val, tb) - elif hasattr(result, "__exit__"): - return t.cast(t.ContextManager[t.Any], result).__exit__(exc, val, tb) - else: - raise NotImplementedError("(async) with is not implemented for this object.") + if isinstance(result, AbstractAsyncContextManager): + return await result.__aexit__(exc, val, tb) + if isinstance(result, AbstractContextManager): + return result.__exit__(exc, val, tb) + + raise NotImplementedError("(async) with is not implemented for this objec") -class UnifiedIterator(t.Generic[T]): - def __init__(self, future_iterable: t.Iterator[Future[T]]) -> None: +class UnifiedIterator[T](Iterator[T], AsyncIterator[T]): + def __init__(self, future_iterable: Iterator[Future[T]]) -> None: self.future_iterable = future_iterable @classmethod - def from_call(cls, func: UnifiedRunner[T], *args: t.Any, **kwargs: t.Any) -> 'UnifiedIterator[T]': - return cls(t.cast(t.Iterator[Future[T]], func(*args, **kwargs))) + def from_call[**P](cls, func: UnifiedRunner[T, P], *args: P.args, **kwargs: P.kwargs) -> UnifiedIterator[T]: + return cls(func(*args, **kwargs)) # type: ignore @property - def futures(self): + def futures(self) -> Iterator[Future[T]]: return self.future_iterable - def run_as_completed(self, callback: t.Callable[[Future[T]], t.Any]) -> UnifiedFuture[None]: - state = UnifiedFuture() + def run_as_completed(self, callback: Callable[[Future[T]], Any]) -> UnifiedFuture[None]: + state = UnifiedFuture[None]() def _is_done_or_cancelled() -> bool: if state.done(): return True - elif state.cancelled(): + if state.cancelled(): state.set_exception(Cancelled()) return True - else: - return False + return False - def _get_next_future() -> t.Optional[Future[T]]: + def _get_next_future() -> Future[T] | None: if _is_done_or_cancelled(): return None @@ -177,10 +185,9 @@ def _get_next_future() -> t.Optional[Future[T]]: except BaseException as e: state.set_exception(e) return None - else: - return next_future + return next_future - def _run_callbacks(): + def _run_callbacks() -> None: try: while (future := _get_next_future()) is not None: # Wait for the future to finish. @@ -205,20 +212,21 @@ def _run_callbacks(): return except Exception as e: import traceback + traceback.print_exception(e) state.set_exception(e) - def _continuation_from_next_cycle(fut): + def _continuation_from_next_cycle(fut: Future[None]) -> None: if fut.exception() is not None: state.set_exception(fut.exception()) else: _run_callbacks() - def _continuation_in_foreign_thread(fut: Future[T]): + def _continuation_in_foreign_thread(fut: Future[T]) -> None: # Optimization, see below. get_loop().from_thread(_continuation, fut) - def _continuation(fut: Future[T]): + def _continuation(fut: Future[T]) -> None: if _run_single_callback(fut): _run_callbacks() @@ -247,14 +255,14 @@ def _run_single_callback(fut: Future[T]) -> bool: get_loop().from_thread(_run_callbacks) return state - def __iter__(self): + def __iter__(self) -> Self: return self def __next__(self) -> T: fut = self.future_iterable.__next__() return fut.result() - def __aiter__(self): + def __aiter__(self) -> Self: return self async def __anext__(self) -> T: @@ -265,33 +273,89 @@ async def __anext__(self) -> T: return await get_loop().await_future(fut) -def unified( - type: t.Literal["auto","generator","future"] = "auto", - future_class: t.Type[UnifiedFuture[T]] = UnifiedFuture, - iterable_class: t.Type[UnifiedIterator[T]] = UnifiedIterator, -) -> t.Callable[[UnifiedRunner[T]], UnifiedCallable]: - def _wrap_generator(func: UnifiedRunner[T]) -> UnifiedCallable: - @functools.wraps(func) - def _wrapped(*args, **kwargs): +# TODO: Probably needs overloads + +# type UnifiedRunner[T, **P] = Callable[P, Future[T] | Iterator[Future[T]]] +# type UnifiedCallable[T] = Callable[..., UnifiedFuture[T] | UnifiedIterator[T]] + + +# @overload +# def unified[T, **P]( +# type : s t r = ..., +# future_class: type[UnifiedFuture[T]] = UnifiedFuture[Any], +# iterable_class: type[UnifiedIterator[T]] = UnifiedIterator[Any], +# ) -> ( +# Callable[ +# [Runner[P, Future[T]]], +# Callable[..., UnifiedFuture[T]], +# ] +# | Callable[ +# [Runner[P, Iterator[Future[T]]]], +# Callable[..., UnifiedIterator[T]], +# ] +# ): ... + + +# @overload +# def unified[T, **P]( +# typee: L iteral["future"] = "future", future_class: type[UnifiedFuture[T]] = UnifiedFuture[Any] +# ) -> Callable[ +# [Runner[P, Future[T]]], +# Callable[..., UnifiedFuture[T]], +# ]: ... + + +# @overload +# def unified[T, **P]( +# typee: Literal["generator"] = "generator", +# *, +# iterable_class: type[UnifiedIterator[T]] = UnifiedIterator[Any], +# ) -> Callable[ +# [Runner[P, Iterator[Future[T]]]], +# Callable[..., UnifiedIterator[T]], +# ]: ... + + +def unified[T, **P]( + type: str = "auto", + future_class: type[UnifiedFuture[T]] = UnifiedFuture[Any], + iterable_class: type[UnifiedIterator[T]] = UnifiedIterator[Any], +) -> ( + Callable[ + [Callable[P, Future[T]]], + Callable[..., UnifiedFuture[T]], + ] + | Callable[ + [Callable[P, Iterator[Future[T]]]], + Callable[..., UnifiedIterator[T]], + ] +): + def _wrap_generator(func: UnifiedRunner[T, P]) -> UnifiedCallable[T]: + @wraps(func) + def _wrapped(*args: Any, **kwargs: Any) -> UnifiedIterator[T]: return iterable_class.from_call(func, *args, **kwargs) + return _wrapped - def _wrap_future(func: UnifiedRunner[T]) -> UnifiedCallable: - @functools.wraps(func) - def _wrapped(*args, **kwargs): + def _wrap_future(func: UnifiedRunner[T, P]) -> UnifiedCallable[T]: + @wraps(func) + def _wrapped(*args: Any, **kwargs: Any) -> UnifiedFuture[T]: return future_class.from_call(func, *args, **kwargs) + return _wrapped - def _wrapper(func: UnifiedRunner[T]) -> UnifiedCallable: + def _wrapper(func: UnifiedRunner[T, P]) -> UnifiedCallable[T]: if type == "auto": - if inspect.isgeneratorfunction(func): + if isgeneratorfunction(func): return _wrap_generator(func) - else: - return _wrap_future(func) - elif type == "generator": + return _wrap_future(func) + + if type == "generator": return _wrap_generator(func) - else: + + if type == "future": return _wrap_future(func) - return _wrapper + raise NotImplementedError + return _wrapper diff --git a/vsengine/_helpers.py b/vsengine/_helpers.py index 50e86c1..075d17e 100644 --- a/vsengine/_helpers.py +++ b/vsengine/_helpers.py @@ -3,27 +3,24 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 import contextlib -import typing as t +from collections.abc import Callable, Iterator + import vapoursynth as vs from vsengine.policy import ManagedEnvironment - -T = t.TypeVar("T") - - -EnvironmentTypes = t.Union[vs.Environment, ManagedEnvironment] +EnvironmentTypes = vs.Environment | ManagedEnvironment # Automatically set the environment within that block. @contextlib.contextmanager -def use_inline(function_name: str, env: t.Optional[EnvironmentTypes]) -> t.Generator[None, None, None]: +def use_inline(function_name: str, env: EnvironmentTypes | None) -> Iterator[None]: if env is None: # Ensure there is actually an environment set in this block. try: vs.get_current_environment() except Exception as e: - raise EnvironmentError( + raise OSError( f"You are currently not running within an environment. " f"Pass the environment directly to {function_name}." ) from e @@ -40,9 +37,7 @@ def use_inline(function_name: str, env: t.Optional[EnvironmentTypes]) -> t.Gener # Variable size and format clips may require different handling depending on the actual frame size. def wrap_variable_size( - node: vs.VideoNode, - force_assumed_format: vs.VideoFormat, - func: t.Callable[[vs.VideoNode], vs.VideoNode] + node: vs.VideoNode, force_assumed_format: vs.VideoFormat, func: Callable[[vs.VideoNode], vs.VideoNode] ) -> vs.VideoNode: # Check: This is not a variable format clip. # Nothing needs to be done. @@ -54,7 +49,8 @@ def _do_resize(f: vs.VideoFrame) -> vs.VideoNode: # As the node should aready have this format, this should be a no-op. return func(node.resize.Point(format=f.format, width=f.width, height=f.height)) - _node_cache = {} + _node_cache: dict[tuple[int, int, int], vs.VideoNode] | None = {} + def _assume_format(n: int, f: vs.VideoFrame) -> vs.VideoNode: nonlocal _node_cache selector = (int(f.format), f.width, f.height) @@ -77,8 +73,4 @@ def _assume_format(n: int, f: vs.VideoFrame) -> vs.VideoNode: # This clip must not become part of the closure, # or otherwise we risk cyclic references. - return ( - node.std.FrameEval(_assume_format, [node], [node]) - .resize.Point(format=force_assumed_format) - ) - + return node.std.FrameEval(_assume_format, [node], [node]).resize.Point(format=force_assumed_format) diff --git a/vsengine/_hospice.py b/vsengine/_hospice.py index f890575..6f73d74 100644 --- a/vsengine/_hospice.py +++ b/vsengine/_hospice.py @@ -3,10 +3,12 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 import gc -import sys import logging -import weakref +import sys import threading +import weakref +from typing import Literal + from vapoursynth import Core, EnvironmentData logger = logging.getLogger(__name__) @@ -14,22 +16,22 @@ lock = threading.Lock() refctr = 0 -refnanny = {} -cores = {} +refnanny = dict[int, weakref.ReferenceType[EnvironmentData]]() +cores = dict[int, Core]() -stage2_to_add = set() -stage2 = set() -stage1 = set() +stage2_to_add = set[int]() +stage2 = set[int]() +stage1 = set[int]() -hold = set() +hold = set[int]() -def admit_environment(environment: EnvironmentData, core: Core): +def admit_environment(environment: EnvironmentData, core: Core) -> None: global refctr with lock: ident = refctr - refctr+=1 + refctr += 1 ref = weakref.ref(environment, lambda _: _add_tostage1(ident)) cores[ident] = core @@ -37,7 +39,8 @@ def admit_environment(environment: EnvironmentData, core: Core): logger.info(f"Admitted environment {environment!r} and {core!r} as with ID:{ident}.") -def any_alive(): + +def any_alive() -> bool: if bool(stage1) or bool(stage2) or bool(stage2_to_add): gc.collect() if bool(stage1) or bool(stage2) or bool(stage2_to_add): @@ -47,8 +50,8 @@ def any_alive(): return bool(stage1) or bool(stage2) or bool(stage2_to_add) -def freeze(): - logger.debug(f"Freezing the hospice. Cores won't be collected anyore.") +def freeze() -> None: + logger.debug("Freezing the hospice. Cores won't be collected anyore.") hold.update(stage1) hold.update(stage2) @@ -57,7 +60,8 @@ def freeze(): stage2.clear() stage2_to_add.clear() -def unfreeze(): + +def unfreeze() -> None: stage1.update(hold) hold.clear() @@ -72,7 +76,7 @@ def _add_tostage1(ident: int) -> None: stage1.add(ident) -def _collectstage1(phase, __): +def _collectstage1(phase: Literal["start", "stop"], _: dict[str, int]) -> None: if phase != "stop": return @@ -86,7 +90,7 @@ def _collectstage1(phase, __): stage2_to_add.add(ident) -def _collectstage2(phase, __): +def _collectstage2(phase: Literal["start", "stop"], _: dict[str, int]) -> None: global stage2_to_add if phase != "stop": @@ -111,4 +115,3 @@ def _collectstage2(phase, __): gc.callbacks.append(_collectstage2) gc.callbacks.append(_collectstage1) - diff --git a/vsengine/_nodes.py b/vsengine/_nodes.py index 6ec3e23..b1274d5 100644 --- a/vsengine/_nodes.py +++ b/vsengine/_nodes.py @@ -2,21 +2,21 @@ # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import typing as t +from collections.abc import Iterable from concurrent.futures import Future +from contextlib import AbstractContextManager from threading import RLock -from vapoursynth import core - -T = t.TypeVar("T") -T_co = t.TypeVar("T_co", covariant=True) +from vapoursynth import core -def buffer_futures(futures: t.Iterable[Future[T_co]], prefetch: int=0, backlog: t.Optional[int]=None) -> t.Iterable[Future[T_co]]: +def buffer_futures[T_co]( + futures: Iterable[Future[T_co]], prefetch: int = 0, backlog: int | None = None +) -> Iterable[Future[T_co]]: if prefetch == 0: prefetch = core.num_threads if backlog is None: - backlog = prefetch*3 + backlog = prefetch * 3 if backlog < prefetch: backlog = prefetch @@ -25,9 +25,9 @@ def buffer_futures(futures: t.Iterable[Future[T_co]], prefetch: int=0, backlog: finished = False running = 0 lock = RLock() - reorder: t.MutableMapping[int, Future[T_co]] = {} + reorder = dict[int, Future[T_co]]() - def _request_next(): + def _request_next() -> None: nonlocal finished, running with lock: if finished: @@ -44,7 +44,7 @@ def _request_next(): reorder[idx] = fut fut.add_done_callback(_finished) - def _finished(f): + def _finished(f: Future[T_co]) -> None: nonlocal finished, running with lock: running -= 1 @@ -54,24 +54,25 @@ def _finished(f): if f.exception() is not None: finished = True return - + _refill() - def _refill(): + def _refill() -> None: if finished: return with lock: # Two rules: 1. Don't exceed the concurrency barrier. # 2. Don't exceed unused-frames-backlog - while (not finished) and (running < prefetch) and len(reorder)0) or running>0: + while (not finished) or (len(reorder) > 0) or running > 0: if sidx not in reorder: # Spin. Reorder being empty should never happen. continue @@ -88,10 +89,11 @@ def _refill(): finished = True -def close_when_needed(future_iterable: t.Iterable[Future[t.ContextManager[T]]]) -> t.Iterable[Future[T]]: - def copy_future_and_run_cb_before(fut): - f = Future() - def _as_completed(_): +def close_when_needed[T](future_iterable: Iterable[Future[AbstractContextManager[T]]]) -> Iterable[Future[T]]: + def copy_future_and_run_cb_before(fut: Future[AbstractContextManager[T]]) -> Future[T]: + f = Future[T]() + + def _as_completed(_: Future[AbstractContextManager[T]]) -> None: try: r = fut.result() except Exception as e: @@ -103,10 +105,11 @@ def _as_completed(_): fut.add_done_callback(_as_completed) return f - def close_fut(f: Future[t.ContextManager[T]]): - def _do_close(_): + def close_fut(f: Future[AbstractContextManager[T]]) -> None: + def _do_close(_: Future[AbstractContextManager[T]]) -> None: if f.exception() is None: f.result().__exit__(None, None, None) + f.add_done_callback(_do_close) for fut in future_iterable: diff --git a/vsengine/_testutils.py b/vsengine/_testutils.py index 71fc7da..84a9047 100644 --- a/vsengine/_testutils.py +++ b/vsengine/_testutils.py @@ -8,7 +8,7 @@ This should ensure that failing tests can safely clean up the current policy. -It works by implementing a proxy policy +It works by implementing a proxy policy and monkey-patching vapoursynth.register_policy. This policy is transparent to subsequent policies registering themselves. @@ -24,33 +24,22 @@ This function will build a policy which only ever uses one environment. """ -import typing as t - -from vsengine._hospice import admit_environment - -from vapoursynth import EnvironmentPolicyAPI, EnvironmentPolicy -from vapoursynth import EnvironmentData -from vapoursynth import Core, core +from typing import Any import vapoursynth as vs +from vapoursynth import Core, EnvironmentData, EnvironmentPolicy, EnvironmentPolicyAPI, core +from vsengine._hospice import admit_environment -__all__ = [ - "forcefully_unregister_policy", - "use_standalone_policy", - - "BLACKBOARD", - - "wrap_test_for_asyncio" -] +__all__ = ["BLACKBOARD", "forcefully_unregister_policy", "use_standalone_policy", "wrap_test_for_asyncio"] -BLACKBOARD = {} +BLACKBOARD = dict[Any, Any]() class ProxyPolicy(EnvironmentPolicy): - _api: t.Optional[EnvironmentPolicyAPI] - _policy: t.Optional[EnvironmentPolicy] + _api: EnvironmentPolicyAPI | None + _policy: EnvironmentPolicy | None __slots__ = ("_api", "_policy") @@ -58,9 +47,10 @@ def __init__(self) -> None: self._api = None self._policy = None - def attach_policy_to_proxy(self, policy: EnvironmentPolicy): + def attach_policy_to_proxy(self, policy: EnvironmentPolicy) -> None: if self._api is None: raise RuntimeError("This proxy is not active") + if self._policy is not None: orig_register_policy(policy) raise SystemError("Unreachable code") @@ -72,7 +62,7 @@ def attach_policy_to_proxy(self, policy: EnvironmentPolicy): self._policy = None raise - def forcefully_unregister_policy(self): + def forcefully_unregister_policy(self) -> None: if self._policy is None: return if self._api is None: @@ -84,7 +74,6 @@ def forcefully_unregister_policy(self): self._api.unregister_policy() orig_register_policy(self) - def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None: self._api = special_api vs.register_policy = self.attach_policy_to_proxy @@ -98,12 +87,12 @@ def on_policy_cleared(self) -> None: self._api = None vs.register_policy = orig_register_policy - def get_current_environment(self) -> t.Optional[EnvironmentData]: + def get_current_environment(self) -> EnvironmentData | None: if self._policy is None: raise RuntimeError("This proxy is not attached to a policy.") return self._policy.get_current_environment() - def set_environment(self, environment: t.Optional[EnvironmentData]) -> None: + def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None: if self._policy is None: raise RuntimeError("This proxy is not attached to a policy.") return self._policy.set_environment(environment) @@ -115,10 +104,10 @@ def is_alive(self, environment: EnvironmentData) -> bool: class StandalonePolicy: - _current: t.Optional[EnvironmentData] - _api: t.Optional[EnvironmentPolicyAPI] - _core: t.Optional[Core] - __slots__ = ("_current", "_api", "_core") + _current: EnvironmentData | None + _api: EnvironmentPolicyAPI | None + _core: Core | None + __slots__ = ("_api", "_core", "_current") def __init__(self) -> None: self._current = None @@ -129,7 +118,7 @@ def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None: self._current = special_api.create_environment() self._core = core.core - def on_policy_cleared(self): + def on_policy_cleared(self) -> None: assert self._api is not None admit_environment(self._current, self._core) @@ -137,14 +126,15 @@ def on_policy_cleared(self): self._current = None self._core = None - def get_current_environment(self): + def get_current_environment(self) -> EnvironmentData | None: return self._current - def set_environment(self, environment: t.Optional[EnvironmentData]): + def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None: if environment is not None and environment is not self._current: raise RuntimeError("No other environments should exist.") + return None - def is_alive(self, environment: EnvironmentData): + def is_alive(self, environment: EnvironmentData) -> bool: return self._current is environment @@ -157,14 +147,14 @@ class EnvironmentPolicyAPIWrapper: __slots__ = ("_api", "_proxy") - def __init__(self, api, proxy) -> None: + def __init__(self, api: EnvironmentPolicyAPI, proxy: ProxyPolicy) -> None: self._api = api self._proxy = proxy - def __getattr__(self, __name: str) -> t.Any: - return getattr(self._api, __name) + def __getattr__(self, name: str) -> Any: + return getattr(self._api, name) - def unregister_policy(self): + def unregister_policy(self) -> None: self._proxy.forcefully_unregister_policy() @@ -174,17 +164,21 @@ def unregister_policy(self): forcefully_unregister_policy = _policy.forcefully_unregister_policy -def use_standalone_policy(): - _policy.attach_policy_to_proxy(StandalonePolicy()) +def use_standalone_policy() -> None: + _policy.attach_policy_to_proxy(StandalonePolicy()) # type: ignore -def wrap_test_for_asyncio(func): +def wrap_test_for_asyncio(func): # type: ignore import asyncio - from vsengine.loops import set_loop + from vsengine.adapters.asyncio import AsyncIOLoop - def test_case(self): - async def _run(): + from vsengine.loops import set_loop + + def test_case(self) -> None: # type: ignore + async def _run() -> None: set_loop(AsyncIOLoop()) await func(self) + asyncio.run(_run()) + return test_case diff --git a/vsengine/adapters/asyncio.py b/vsengine/adapters/asyncio.py index b26bb98..96ca233 100644 --- a/vsengine/adapters/asyncio.py +++ b/vsengine/adapters/asyncio.py @@ -3,45 +3,40 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import typing as t import asyncio import contextlib import contextvars +from collections.abc import Callable, Coroutine, Iterator from concurrent.futures import Future +from typing import Any -from vsengine.loops import EventLoop, Cancelled - - -T = t.TypeVar("T") +from vsengine.loops import Cancelled, EventLoop class AsyncIOLoop(EventLoop): """ Bridges vs-engine to AsyncIO. """ + loop: asyncio.AbstractEventLoop - def __init__(self, loop: t.Optional[asyncio.AbstractEventLoop] = None) -> None: + def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None: if loop is None: loop = asyncio.get_event_loop() self.loop = loop - def attach(self): + def attach(self) -> None: pass - def detach(self): + def detach(self) -> None: pass - def from_thread( - self, - func: t.Callable[..., T], - *args: t.Any, - **kwargs: t.Any - ) -> Future[T]: - future = Future() + def from_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: + future = Future[T]() ctx = contextvars.copy_context() - def _wrap(): + + def _wrap() -> None: if not future.set_running_or_notify_cancel(): return @@ -55,32 +50,34 @@ def _wrap(): self.loop.call_soon_threadsafe(_wrap) return future - def to_thread(self, func, *args, **kwargs): + def to_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Coroutine[Any, Any, T]: # type: ignore ctx = contextvars.copy_context() - def _wrap(): + + def _wrap() -> T: return ctx.run(func, *args, **kwargs) return asyncio.to_thread(_wrap) - async def await_future(self, future: Future[T]) -> T: + async def await_future[T](self, future: Future[T]) -> T: with self.wrap_cancelled(): return await asyncio.wrap_future(future, loop=self.loop) def next_cycle(self) -> Future[None]: - future = Future() + future = Future[None]() task = asyncio.current_task() - def continuation(): + + def continuation() -> None: if task is None or not task.cancelled(): future.set_result(None) else: future.set_exception(Cancelled()) + self.loop.call_soon(continuation) return future @contextlib.contextmanager - def wrap_cancelled(self): + def wrap_cancelled(self) -> Iterator[None]: try: yield except Cancelled: raise asyncio.CancelledError() from None - diff --git a/vsengine/adapters/trio.py b/vsengine/adapters/trio.py index 007e6eb..1abebc4 100644 --- a/vsengine/adapters/trio.py +++ b/vsengine/adapters/trio.py @@ -3,38 +3,28 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -from concurrent.futures import Future -import typing as t import contextlib +from collections.abc import Callable, Iterator +from concurrent.futures import Future +from typing import Any from trio import Cancelled as TrioCancelled -from trio import CapacityLimiter -from trio import CancelScope -from trio import Nursery -from trio import to_thread -from trio import Event -from trio.lowlevel import current_trio_token +from trio import CancelScope, CapacityLimiter, Event, Nursery, to_thread +from trio.lowlevel import TrioToken, current_trio_token from vsengine.loops import Cancelled, EventLoop -T = t.TypeVar("T") - - class TrioEventLoop(EventLoop): _scope: Nursery - def __init__( - self, - nursery: Nursery, - limiter: t.Optional[CapacityLimiter]=None - ) -> None: + def __init__(self, nursery: Nursery, limiter: CapacityLimiter | None = None) -> None: if limiter is None: - limiter = t.cast(CapacityLimiter, to_thread.current_default_thread_limiter()) + limiter = to_thread.current_default_thread_limiter() self.nursery = nursery self.limiter = limiter - self._token = None + self._token: TrioToken | None = None def attach(self) -> None: """ @@ -48,19 +38,15 @@ def detach(self) -> None: """ self.nursery.cancel_scope.cancel() - def from_thread( - self, - func: t.Callable[..., T], - *args: t.Any, - **kwargs: t.Any - ) -> Future[T]: + def from_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: """ Ran from vapoursynth threads to move data to the event loop. """ assert self._token is not None - fut = Future() - def _executor(): + fut = Future[T]() + + def _executor() -> None: if not fut.set_running_or_notify_cancel(): return @@ -70,17 +56,18 @@ def _executor(): fut.set_exception(e) else: fut.set_result(result) - + self._token.run_sync_soon(_executor) return fut - async def to_thread(self, func: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any): + async def to_thread(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: # type: ignore """ Run this function in a worker thread. """ result = None - error: BaseException|None = None - def _executor(): + error: BaseException | None = None + + def _executor() -> None: nonlocal result, error try: result = func(*args, **kwargs) @@ -88,24 +75,29 @@ def _executor(): error = e await to_thread.run_sync(_executor, limiter=self.limiter) + if error is not None: + # unreachable? assert isinstance(error, BaseException) - raise t.cast(BaseException, error) + raise error else: return result def next_cycle(self) -> Future[None]: scope = CancelScope() - future = Future() - def continuation(): + future = Future[None]() + TrioEventLoop.to_thread + + def continuation() -> None: if scope.cancel_called: future.set_exception(Cancelled()) else: future.set_result(None) + self.from_thread(continuation) return future - async def await_future(self, future: Future[T]) -> T: + async def await_future[T](self, future: Future[T]) -> T: """ Await a concurrent future. @@ -114,9 +106,10 @@ async def await_future(self, future: Future[T]) -> T: """ event = Event() - result: T|None = None - error: BaseException|None = None - def _when_done(_): + result: T | None = None + error: BaseException | None = None + + def _when_done(_: Future[T]) -> None: nonlocal error, result if (error := future.exception()) is not None: pass @@ -132,12 +125,12 @@ def _when_done(_): if error is not None: with self.wrap_cancelled(): - raise t.cast(BaseException, error) + raise error else: - return t.cast(T, result) + return result # type: ignore @contextlib.contextmanager - def wrap_cancelled(self): + def wrap_cancelled(self) -> Iterator[None]: """ Wraps vsengine.loops.Cancelled into the native cancellation error. """ diff --git a/vsengine/convert.py b/vsengine/convert.py index 0b1780f..586b9a0 100644 --- a/vsengine/convert.py +++ b/vsengine/convert.py @@ -2,17 +2,20 @@ # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 + import functools -import typing as t +from collections.abc import Callable, Mapping +from typing import Any + import vapoursynth as vs -from vsengine._helpers import use_inline, wrap_variable_size, EnvironmentTypes +from vsengine._helpers import EnvironmentTypes, use_inline, wrap_variable_size # The heuristics code for nodes. # Usually the nodes are tagged so this heuristics code is not required. @functools.lru_cache -def yuv_heuristic(width: int, height: int) -> t.Mapping[str, str]: +def yuv_heuristic(width: int, height: int) -> Mapping[str, str]: result = {} if width >= 3840: @@ -55,22 +58,19 @@ def yuv_heuristic(width: int, height: int) -> t.Mapping[str, str]: # Move this function out of the closure to avoid capturing clip. def _convert_yuv( - c: vs.VideoNode, - *, - core: vs.Core, - real_rgb24: vs.VideoFormat, - default_args: t.Dict[str, t.Any], - scaler: t.Union[str, t.Callable[..., vs.VideoNode]] -): + c: vs.VideoNode, + *, + core: vs.Core, + real_rgb24: vs.VideoFormat, + default_args: dict[str, Any], + scaler: str | Callable[..., vs.VideoNode], +) -> vs.VideoNode: # We make yuv_heuristic not configurable so the heuristic # will be shared across projects. # # In my opinion, this is a quirk that should be shared. - args = { - **yuv_heuristic(c.width, c.height), - **default_args - } + args = {**yuv_heuristic(c.width, c.height), **default_args} if c.format.subsampling_w != 0 or c.format.subsampling_h != 0: # To be clear, scaler should always be a string. @@ -82,22 +82,13 @@ def _convert_yuv( # Keep bitdepth so we can dither futher down in the RGB part. return resizer( - c, - format=real_rgb24.replace( - sample_type=c.format.sample_type, - bits_per_sample=c.format.bits_per_sample - ), - **args + c, format=real_rgb24.replace(sample_type=c.format.sample_type, bits_per_sample=c.format.bits_per_sample), **args ) # Move this function out of the closure to avoid capturing clip. def _actually_resize( - c: vs.VideoNode, - *, - core: vs.Core, - convert_yuv: t.Callable[[vs.VideoNode], vs.VideoNode], - target_rgb: vs.VideoFormat + c: vs.VideoNode, *, core: vs.Core, convert_yuv: Callable[[vs.VideoNode], vs.VideoNode], target_rgb: vs.VideoFormat ) -> vs.VideoNode: # Converting to YUV is a little bit more complicated, # so I extracted it to its own function. @@ -110,29 +101,29 @@ def _actually_resize( c = c.std.RemoveFrameProps("_Matrix") # Actually perform the format conversion on a non-subsampled clip. - if c.format.color_family != vs.RGB or c.format.sample_type != vs.INTEGER or c.format.bits_per_sample != target_rgb.bits_per_sample: - c = core.resize.Point( - c, - format=target_rgb - ) + if ( + c.format.color_family != vs.RGB + or c.format.sample_type != vs.INTEGER + or c.format.bits_per_sample != target_rgb.bits_per_sample + ): + c = core.resize.Point(c, format=target_rgb) return c def to_rgb( - clip: vs.VideoNode, - env: t.Optional[EnvironmentTypes] = None, - *, - # Output: RGB bitdepth - bits_per_sample: int = 8, - - # Input: YUV - scaler: t.Union[str, t.Callable[..., vs.VideoNode]] = "Bicubic", - default_matrix: t.Optional[str] = None, - default_transfer: t.Optional[str] = None, - default_primaries: t.Optional[str] = None, - default_range: t.Optional[str] = None, - default_chromaloc: t.Optional[str] = None, + clip: vs.VideoNode, + env: EnvironmentTypes | None = None, + *, + # Output: RGB bitdepth + bits_per_sample: int = 8, + # Input: YUV + scaler: str | Callable[..., vs.VideoNode] = "Bicubic", + default_matrix: str | None = None, + default_transfer: str | None = None, + default_primaries: str | None = None, + default_range: str | None = None, + default_chromaloc: str | None = None, ) -> vs.VideoNode: """ This function converts a clip to RGB. @@ -142,7 +133,8 @@ def to_rgb( :param bits_per_sample: The bits per sample the resulting RGB clip should have. :param scaler: The name scaler function in core.resize that should be used to convert YUV to RGB. :param default_*: Manually override the defaults predicted by the heuristics. - :param yuv_heuristic: The heuristic function that takes the frame size and returns a set of yuv-metadata. (For test purposes) + :param yuv_heuristic: The heuristic function that takes the frame size and returns a set of yuv-metadata. + (For test purposes) """ # This function does a lot. @@ -165,25 +157,11 @@ def to_rgb( real_rgb24 = core.get_video_format(vs.RGB24) target_rgb = real_rgb24.replace(bits_per_sample=bits_per_sample) - # This avoids capturing `clip` in a closure creating a self-reference. + # This avoids capturing `clip` in a closure creating a self-reference. convert_yuv = functools.partial( - _convert_yuv, - core=core, - real_rgb24=real_rgb24, - default_args=default_args, - scaler=scaler + _convert_yuv, core=core, real_rgb24=real_rgb24, default_args=default_args, scaler=scaler ) - actually_resize = functools.partial( - _actually_resize, - core=core, - target_rgb=target_rgb, - convert_yuv=convert_yuv - ) - - return wrap_variable_size( - clip, - force_assumed_format=target_rgb, - func=actually_resize - ) + actually_resize = functools.partial(_actually_resize, core=core, target_rgb=target_rgb, convert_yuv=convert_yuv) + return wrap_variable_size(clip, force_assumed_format=target_rgb, func=actually_resize) diff --git a/vsengine/loops.py b/vsengine/loops.py index 293a7dd..19926d0 100644 --- a/vsengine/loops.py +++ b/vsengine/loops.py @@ -2,34 +2,27 @@ # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -from concurrent.futures import Future, CancelledError -import contextlib -import functools -import typing as t +from collections.abc import Awaitable, Callable, Iterator +from concurrent.futures import CancelledError, Future +from contextlib import contextmanager +from functools import wraps +from typing import Any import vapoursynth +__all__ = ["Cancelled", "EventLoop", "from_thread", "get_loop", "keep_environment", "set_loop", "to_thread"] -T = t.TypeVar("T") -T_co = t.TypeVar("T_co", covariant=True) +class Cancelled(Exception): # noqa: N818 + pass -__all__ = [ - "EventLoop", "Cancelled", - "get_loop", "set_loop", - "to_thread", "from_thread", "keep_environment" -] - -class Cancelled(Exception): pass - - -@contextlib.contextmanager -def _noop(): +@contextmanager +def _noop() -> Iterator[None]: yield -DONE = Future() +DONE = Future[None]() DONE.set_result(None) @@ -53,23 +46,19 @@ def detach(self) -> None: """ ... - def from_thread( - self, - func: t.Callable[..., T], - *args: t.Any, - **kwargs: t.Any - ) -> Future[T]: + def from_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: """ Ran from vapoursynth threads to move data to the event loop. """ - ... + raise NotImplementedError - def to_thread(self, func: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any) -> t.Any: + def to_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: """ Run this function in a worker thread. """ - fut = Future() - def wrapper(): + fut = Future[T]() + + def wrapper() -> None: if not fut.set_running_or_notify_cancel(): return @@ -81,6 +70,7 @@ def wrapper(): fut.set_result(result) import threading + threading.Thread(target=wrapper).start() return fut @@ -95,11 +85,11 @@ def next_cycle(self) -> Future[None]: Only works in the main thread. """ - future = Future() + future = Future[None]() self.from_thread(future.set_result, None) return future - def await_future(self, future: Future[T]) -> t.Awaitable[T]: + def await_future[T](self, future: Future[T]) -> Awaitable[T]: """ Await a concurrent future. @@ -108,8 +98,8 @@ def await_future(self, future: Future[T]) -> t.Awaitable[T]: """ raise NotImplementedError - @contextlib.contextmanager - def wrap_cancelled(self): + @contextmanager + def wrap_cancelled(self) -> Iterator[None]: """ Wraps vsengine.loops.Cancelled into the native cancellation error. """ @@ -121,7 +111,7 @@ def wrap_cancelled(self): class _NoEventLoop(EventLoop): """ - This is the default event-loop used by + This is the default event-loop used by """ def attach(self) -> None: @@ -133,13 +123,8 @@ def detach(self) -> None: def next_cycle(self) -> Future[None]: return DONE - def from_thread( - self, - func: t.Callable[..., T], - *args: t.Any, - **kwargs: t.Any - ) -> Future[T]: - fut = Future() + def from_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: + fut = Future[T]() try: result = func(*args, **kwargs) except BaseException as e: @@ -150,7 +135,7 @@ def from_thread( NO_LOOP = _NoEventLoop() -current_loop = NO_LOOP +current_loop: EventLoop = NO_LOOP def get_loop() -> EventLoop: @@ -159,6 +144,7 @@ def get_loop() -> EventLoop: """ return current_loop + def set_loop(loop: EventLoop) -> None: """ Sets the currently running loop. @@ -178,7 +164,7 @@ def set_loop(loop: EventLoop) -> None: raise -def keep_environment(func: t.Callable[..., T]) -> t.Callable[..., T]: +def keep_environment[T](func: Callable[..., T]) -> Callable[..., T]: """ This decorator will return a function that keeps the environment that was active when the decorator was applied. @@ -191,15 +177,15 @@ def keep_environment(func: t.Callable[..., T]) -> t.Callable[..., T]: except RuntimeError: environment = _noop - @functools.wraps(func) - def _wrapper(*args, **kwargs): + @wraps(func) + def _wrapper(*args: Any, **kwargs: Any) -> T: with environment(): return func(*args, **kwargs) return _wrapper -def from_thread(func: t.Callable[..., T], *args: t.Any, **kwargs: t.Any) -> Future[T]: +def from_thread[T](func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: """ Runs a function inside the current event-loop, preserving the currently running vapoursynth environment (if any). @@ -213,13 +199,13 @@ def from_thread(func: t.Callable[..., T], *args: t.Any, **kwargs: t.Any) -> Futu """ @keep_environment - def _wrapper(): + def _wrapper() -> Any: return func(*args, **kwargs) return get_loop().from_thread(_wrapper) -def to_thread(func: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any) -> t.Any: +def to_thread[T](func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: """ Runs a function in a dedicated thread or worker, preserving the currently running vapoursynth environment (if any). @@ -229,19 +215,19 @@ def to_thread(func: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any) -> t. :param kwargs: The keyword arguments to pass to the function. :return: An loop-specific object. """ + @keep_environment - def _wrapper(): + def _wrapper() -> T: return func(*args, **kwargs) - + return get_loop().to_thread(_wrapper) -async def make_awaitable(future: Future[T]) -> T: +async def make_awaitable[T](future: Future[T]) -> T: """ Makes a future awaitable. :param future: The future to make awaitable. :return: An object that can be awaited. """ - return t.cast(T, await get_loop().await_future(future)) - + return await get_loop().await_future(future) diff --git a/vsengine/policy.py b/vsengine/policy.py index 68ff67e..f73f99d 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -14,9 +14,9 @@ >>> policy = Policy(GlobalStore()) >>> policy.register() >>> with policy.new_environment() as env: - ... with env.use(): - ... vs.core.std.BlankClip().set_output() - ... print(env.outputs) + ... with env.use(): + ... vs.core.std.BlankClip().set_output() + ... print(env.outputs) {"0": } >>> policy.unregister() @@ -37,7 +37,7 @@ that can run multiple environments at once. This one behaves like vsscript. - ContextVarStore is useful when you are using event-loops like asyncio, - curio, and trio. When using this store, make sure to reuse the store + curio, and trio. When using this store, make sure to reuse the store between successive Policy-instances as otherwise the old store might leak objects. More details are written in the documentation of the contextvars module of the standard library. @@ -58,42 +58,41 @@ When reloading the application, you can call policy.unregister() """ -import typing as t -import logging -import weakref -import threading +from __future__ import annotations + import contextlib import contextvars +import logging +import threading +import weakref +from collections.abc import Iterator, Mapping +from types import TracebackType +from typing import Any, Protocol, Self -from vsengine._hospice import admit_environment - -from vapoursynth import EnvironmentPolicy, EnvironmentPolicyAPI -from vapoursynth import Environment, EnvironmentData -from vapoursynth import register_policy import vapoursynth as vs +from vapoursynth import Environment, EnvironmentData, EnvironmentPolicy, EnvironmentPolicyAPI, register_policy +from vsengine._hospice import admit_environment -__all__ = [ - "GlobalStore", "ThreadLocalStore", "ContextVarStore", - "Policy", "ManagedEnvironment" -] +__all__ = ["ContextVarStore", "GlobalStore", "ManagedEnvironment", "Policy", "ThreadLocalStore"] logger = logging.getLogger(__name__) -class EnvironmentStore(t.Protocol): +class EnvironmentStore(Protocol): """ Environment Stores manage which environment is currently active. """ - def set_current_environment(self, environment: t.Any): + + def set_current_environment(self, environment: Any) -> None: """ Set the current environment in the store. """ ... - def get_current_environment(self) -> t.Any: + def get_current_environment(self) -> Any: """ Retrieve the current environment from the store (if any) """ @@ -104,16 +103,17 @@ class GlobalStore(EnvironmentStore): """ This is the simplest store: It just stores the environment in a variable. """ - _current: t.Optional[EnvironmentData] + + _current: EnvironmentData | None __slots__ = ("_current",) def __init__(self) -> None: self._current = None - - def set_current_environment(self, environment: t.Optional[EnvironmentData]): + + def set_current_environment(self, environment: EnvironmentData | None) -> None: self._current = environment - def get_current_environment(self) -> t.Optional[EnvironmentData]: + def get_current_environment(self) -> EnvironmentData | None: return self._current @@ -129,10 +129,10 @@ class ThreadLocalStore(EnvironmentStore): def __init__(self) -> None: self._current = threading.local() - def set_current_environment(self, environment: t.Optional[EnvironmentData]): + def set_current_environment(self, environment: EnvironmentData | None) -> None: self._current.environment = environment - def get_current_environment(self) -> t.Optional[EnvironmentData]: + def get_current_environment(self) -> EnvironmentData | None: return getattr(self._current, "environment", None) @@ -140,15 +140,16 @@ class ContextVarStore(EnvironmentStore): """ If you are using AsyncIO or similar frameworks, use this store. """ - _current: contextvars.ContextVar[t.Optional[EnvironmentData]] - def __init__(self, name: str="vapoursynth") -> None: + _current: contextvars.ContextVar[EnvironmentData | None] + + def __init__(self, name: str = "vapoursynth") -> None: self._current = contextvars.ContextVar(name) - def set_current_environment(self, environment: t.Optional[EnvironmentData]): + def set_current_environment(self, environment: EnvironmentData | None) -> None: self._current.set(environment) - def get_current_environment(self) -> t.Optional[EnvironmentData]: + def get_current_environment(self) -> EnvironmentData | None: return self._current.get(None) @@ -157,12 +158,12 @@ class _ManagedPolicy(EnvironmentPolicy): This class directly interfaces with VapourSynth. """ - _api: t.Optional[EnvironmentPolicyAPI] + _api: EnvironmentPolicyAPI | None _store: EnvironmentStore _mutex: threading.Lock _local: threading.local - __slots__ = ("_api", "_store", "_mutex", "_local") + __slots__ = ("_api", "_local", "_mutex", "_store") def __init__(self, store: EnvironmentStore) -> None: self._store = store @@ -174,15 +175,15 @@ def __init__(self, store: EnvironmentStore) -> None: # should not make their switch observable from the outside. # Start the section. - def inline_section_start(self, environment: EnvironmentData): + def inline_section_start(self, environment: EnvironmentData) -> None: self._local.environment = environment # End the section. - def inline_section_end(self): + def inline_section_end(self) -> None: self._local.environment = None @property - def api(self): + def api(self) -> EnvironmentPolicyAPI: if self._api is None: raise RuntimeError("Invalid state: No access to the current API") return self._api @@ -195,20 +196,19 @@ def on_policy_cleared(self) -> None: self._api = None logger.debug("Policy cleared.") - def get_current_environment(self) -> t.Optional[EnvironmentData]: + def get_current_environment(self) -> EnvironmentData | None: # For small segments, allow switching the environment inline. # This is useful for vsengine-functions that require access to the # vapoursynth api, but don't want to invoke the store for it. - if (env := getattr(self._local, "environment", None)) is not None: - if self.is_alive(env): - return env + if (env := getattr(self._local, "environment", None)) is not None and self.is_alive(env): + return env # We wrap everything in a mutex to make sure # no context-switch can reliably happen in this section. with self._mutex: current_environment = self._store.get_current_environment() if current_environment is None: - return + return None if current_environment() is None: logger.warning(f"Got dead environment: {current_environment()!r}") @@ -223,9 +223,9 @@ def get_current_environment(self) -> t.Optional[EnvironmentData]: self._store.set_current_environment(None) return None - return t.cast(EnvironmentData, received_environment) + return received_environment - def set_environment(self, environment: EnvironmentData) -> t.Optional[EnvironmentData]: + def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None: with self._mutex: previous_environment = self._store.get_current_environment() @@ -242,20 +242,22 @@ def set_environment(self, environment: EnvironmentData) -> t.Optional[Environmen if previous_environment is not None: return previous_environment() + return None + -class ManagedEnvironment: +class ManagedEnvironment(contextlib.AbstractContextManager["ManagedEnvironment"]): _environment: Environment - _data: EnvironmentData - _policy: 'Policy' - __slots__ = ("_environment", "_data", "_policy") + _data: EnvironmentData | None + _policy: Policy + __slots__ = ("_data", "_environment", "_policy") - def __init__(self, environment: Environment, data: EnvironmentData, policy: 'Policy') -> None: + def __init__(self, environment: Environment, data: EnvironmentData, policy: Policy) -> None: self._environment = environment self._data = data self._policy = policy @property - def vs_environment(self): + def vs_environment(self) -> Environment: """ Returns the vapoursynth.Environment-object representing this environment. """ @@ -270,7 +272,7 @@ def core(self) -> vs.Core: return vs.core.core @property - def outputs(self) -> t.Mapping[int, vs.VideoOutputTuple]: + def outputs(self) -> Mapping[int, vs.VideoOutputTuple | vs.AudioNode]: """ Returns the output within this environment. """ @@ -278,7 +280,7 @@ def outputs(self) -> t.Mapping[int, vs.VideoOutputTuple]: return vs.get_outputs() @contextlib.contextmanager - def inline_section(self) -> t.Generator[None, None, None]: + def inline_section(self) -> Iterator[None]: """ Private API! @@ -287,21 +289,21 @@ def inline_section(self) -> t.Generator[None, None, None]: If you follow the rules below, switching the environment will be invisible to the caller. - + Rules for safely calling this function: - Do not suspend greenlets within the block! - Do not yield or await within the block! - Do not use __enter__ and __exit__ directly. - This function is not reentrant. """ - self._policy.managed.inline_section_start(self._data) + self._policy.managed.inline_section_start(self._data) # type: ignore try: yield finally: self._policy.managed.inline_section_end() @contextlib.contextmanager - def use(self) -> t.Generator[None, None, None]: + def use(self) -> Iterator[None]: """ Switches to this environment within a block. """ @@ -312,20 +314,20 @@ def use(self) -> t.Generator[None, None, None]: # Workaround: On 32bit systems, environment policies do not reset. self._policy.managed.set_environment(prev_environment) - def switch(self): + def switch(self) -> None: """ Switches to the given environment without storing which environment has been defined previously. """ self._environment.use().__enter__() - def dispose(self): + def dispose(self) -> None: if self.disposed: return logger.debug(f"Disposing environment {self._data!r}.") - admit_environment(self._data, self.core) - self._policy.api.destroy_environment(self._data) + admit_environment(self._data, self.core) # type: ignore + self._policy.api.destroy_environment(self._data) # type: ignore self._data = None @property @@ -335,22 +337,23 @@ def disposed(self) -> bool: """ return self._data is None - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, _, __, ___): + def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: self.dispose() - def __del__(self): + def __del__(self) -> None: if self._data is None: return import warnings + warnings.warn(f"Disposing {self!r} inside __del__. This might cause leaks.", ResourceWarning) self.dispose() -class Policy: +class Policy(contextlib.AbstractContextManager["Policy"]): """ A managed policy is a very simple policy that just stores the environment data within the given store. @@ -358,28 +361,29 @@ class Policy: For convenience (especially for testing), this is a context manager that makes sure policies are being unregistered when leaving a block. """ + _managed: _ManagedPolicy def __init__(self, store: EnvironmentStore) -> None: self._managed = _ManagedPolicy(store) - def register(self): + def register(self) -> None: """ Registers the policy with VapourSynth. """ register_policy(self._managed) - - def unregister(self): + + def unregister(self) -> None: """ Unregisters the policy from VapourSynth. """ self._managed.api.unregister_policy() - def __enter__(self): + def __enter__(self) -> Self: self.register() return self - def __exit__(self, _, __, ___): + def __exit__(self, _: type[BaseException] | None, __: BaseException | None, ___: TracebackType | None) -> None: self.unregister() def new_environment(self) -> ManagedEnvironment: @@ -398,7 +402,7 @@ def new_environment(self) -> ManagedEnvironment: return ManagedEnvironment(env, data, self) @property - def api(self): + def api(self) -> EnvironmentPolicyAPI: """ Returns the API instance for more complex interactions. @@ -407,11 +411,10 @@ def api(self): return self._managed.api @property - def managed(self): + def managed(self) -> _ManagedPolicy: """ Returns the actual policy within VapourSynth. You will rarely need to use this directly. """ return self._managed - diff --git a/vsengine/py.typed b/vsengine/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/vsengine/video.py b/vsengine/video.py index 510eb9f..da59d5e 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -5,20 +5,20 @@ """ vsengine.render renders video frames for you. """ -import typing as t + +from collections.abc import Iterator, Sequence from concurrent.futures import Future import vapoursynth -from vsengine._futures import unified, UnifiedFuture -from vsengine._nodes import close_when_needed, buffer_futures -from vsengine._helpers import use_inline, EnvironmentTypes +from vsengine._futures import UnifiedFuture, unified +from vsengine._helpers import EnvironmentTypes, use_inline +from vsengine._nodes import buffer_futures, close_when_needed + @unified() def frame( - node: vapoursynth.VideoNode, - frameno: int, - env: t.Optional[EnvironmentTypes]=None + node: vapoursynth.VideoNode, frameno: int, env: EnvironmentTypes | None = None ) -> Future[vapoursynth.VideoFrame]: with use_inline("frame", env): return node.get_frame_async(frameno) @@ -26,39 +26,36 @@ def frame( @unified() def planes( - node: vapoursynth.VideoNode, - frameno: int, - env: t.Optional[EnvironmentTypes]=None, - *, - planes: t.Optional[t.Sequence[int]]=None -) -> Future[t.Tuple[bytes, ...]]: - def _extract(frame: vapoursynth.VideoFrame): + node: vapoursynth.VideoNode, + frameno: int, + env: EnvironmentTypes | None = None, + *, + planes: Sequence[int] | None = None, +) -> Future[tuple[bytes, ...]]: + def _extract(frame: vapoursynth.VideoFrame) -> tuple[bytes, ...]: try: # This might be a variable format clip. # extract the plane as late as possible. - if planes is None: - ps = range(len(frame)) - else: - ps = planes - return [bytes(frame[p]) for p in ps] + ps = range(len(frame)) if planes is None else planes + return tuple(bytes(frame[p]) for p in ps) finally: frame.close() + return frame(node, frameno, env).map(_extract) @unified(type="generator") def frames( - node: vapoursynth.VideoNode, - env: t.Optional[EnvironmentTypes]=None, - *, - prefetch: int=0, - backlog: t.Optional[int]=None, - - # Unlike the implementation provided by VapourSynth, - # we don't have to care about backwards compatibility and - # can just do the right thing from the beginning. - close: bool=True -) -> t.Iterable[Future[vapoursynth.VideoFrame]]: + node: vapoursynth.VideoNode, + env: EnvironmentTypes | None = None, + *, + prefetch: int = 0, + backlog: int | None = None, + # Unlike the implementation provided by VapourSynth, + # we don't have to care about backwards compatibility and + # can just do the right thing from the beginning. + close: bool = True, +) -> Iterator[Future[vapoursynth.VideoFrame]]: with use_inline("frames", env): length = len(node) @@ -72,58 +69,58 @@ def frames( it = close_when_needed(it) return it + @unified(type="generator") def render( - node: vapoursynth.VideoNode, - env: t.Optional[int]=None, - *, - prefetch: int=0, - backlog: t.Optional[int]=0, - - y4m: bool = False -) -> t.Iterable[Future[t.Tuple[int, bytes]]]: - + node: vapoursynth.VideoNode, + env: int | None = None, + *, + prefetch: int = 0, + backlog: int | None = 0, + y4m: bool = False, +) -> Iterator[Future[tuple[int, bytes]]]: frame_count = len(node) - + if y4m: y4mformat = "" if node.format.color_family == vapoursynth.GRAY: - y4mformat = 'mono' + y4mformat = "mono" if node.format.bits_per_sample > 8: y4mformat = y4mformat + str(node.format.bits_per_sample) elif node.format.color_family == vapoursynth.YUV: if node.format.subsampling_w == 1 and node.format.subsampling_h == 1: - y4mformat = '420' + y4mformat = "420" elif node.format.subsampling_w == 1 and node.format.subsampling_h == 0: - y4mformat = '422' + y4mformat = "422" elif node.format.subsampling_w == 0 and node.format.subsampling_h == 0: - y4mformat = '444' + y4mformat = "444" elif node.format.subsampling_w == 2 and node.format.subsampling_h == 2: - y4mformat = '410' + y4mformat = "410" elif node.format.subsampling_w == 2 and node.format.subsampling_h == 0: - y4mformat = '411' + y4mformat = "411" elif node.format.subsampling_w == 0 and node.format.subsampling_h == 1: - y4mformat = '440' + y4mformat = "440" if node.format.bits_per_sample > 8: - y4mformat = y4mformat + 'p' + str(node.format.bits_per_sample) + y4mformat = y4mformat + "p" + str(node.format.bits_per_sample) else: raise ValueError("Can only use GRAY and YUV for V4M-Streams") if len(y4mformat) > 0: - y4mformat = 'C' + y4mformat + ' ' + y4mformat = "C" + y4mformat + " " - data = 'YUV4MPEG2 {y4mformat}W{width} H{height} F{fps_num}:{fps_den} Ip A0:0 XLENGTH={length}\n'.format( + data = "YUV4MPEG2 {y4mformat}W{width} H{height} F{fps_num}:{fps_den} Ip A0:0 XLENGTH={length}\n".format( y4mformat=y4mformat, width=node.width, height=node.height, fps_num=node.fps_num, fps_den=node.fps_den, - length=frame_count + length=frame_count, ) yield UnifiedFuture.resolve((0, data.encode("ascii"))) current_frame = 0 - def render_single_frame(frame: vapoursynth.VideoFrame) -> t.Tuple[int, bytes]: + + def render_single_frame(frame: vapoursynth.VideoFrame) -> tuple[int, bytes]: buf = [] if y4m: buf.append(b"FRAME\n") @@ -136,4 +133,3 @@ def render_single_frame(frame: vapoursynth.VideoFrame) -> t.Tuple[int, bytes]: for frame, fut in enumerate(frames(node, env, prefetch=prefetch, backlog=backlog).futures, 1): current_frame = frame yield UnifiedFuture.from_future(fut).map(render_single_frame) - diff --git a/vsengine/vpy.py b/vsengine/vpy.py index f27052a..3537de3 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -35,38 +35,37 @@ A Script-instance is awaitable, in which it will await the completion of the script. """ -import typing as t -import traceback -import textwrap -import runpy -import types + import ast import os +import runpy +import textwrap +import traceback +import types +from collections.abc import Awaitable, Callable, Generator, Mapping from concurrent.futures import Future +from contextlib import AbstractContextManager +from types import TracebackType +from typing import Any, Self from vapoursynth import Environment, get_current_environment -from vsengine.loops import to_thread, make_awaitable -from vsengine.policy import Policy, ManagedEnvironment -from vsengine._futures import unified, UnifiedFuture +from vsengine._futures import UnifiedFuture, unified +from vsengine.loops import make_awaitable, to_thread +from vsengine.policy import ManagedEnvironment, Policy +type Runner[T] = Callable[[Callable[[], T]], Future[T]] +Executor = Callable[[AbstractContextManager[None], types.ModuleType], None] -T = t.TypeVar("T") -Runner = t.Callable[[t.Callable[[], T]], Future[T]] -Executor = t.Callable[[t.ContextManager[None], types.ModuleType], None] +__all__ = ["ExecutionFailed", "code", "script", "variables"] -__all__ = [ - "ExecutionFailed", "script", "code", "variables" -] - - -class ExecutionFailed(Exception): +class ExecutionFailed(Exception): # noqa: N818 #: It contains the actual exception that has been raised. parent_error: BaseException - def __init__(self, parent_error: BaseException): + def __init__(self, parent_error: BaseException) -> None: msg = textwrap.indent(self.extract_traceback(parent_error), "| ") super().__init__(f"An exception was raised while running the script.\n{msg}") self.parent_error = parent_error @@ -77,18 +76,17 @@ def extract_traceback(error: BaseException) -> str: msg = "".join(msg) return msg -class WrapAllErrors: - def __enter__(self): - pass +class WrapAllErrors(AbstractContextManager[None]): + def __enter__(self) -> None: ... - def __exit__(self, exc, val, tb): + def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: if val is not None: raise ExecutionFailed(val) from None -def inline_runner(func: t.Callable[[], T]) -> Future[T]: - fut = Future() +def inline_runner[T](func: Callable[[], T]) -> Future[T]: + fut = Future[T]() try: result = func() except BaseException as e: @@ -98,9 +96,9 @@ def inline_runner(func: t.Callable[[], T]) -> Future[T]: return fut -def chdir_runner(dir: os.PathLike, parent: Runner[T]) -> Runner[T]: - def runner(func, *args, **kwargs): - def _wrapped(): +def chdir_runner[T](dir: os.PathLike[str], parent: Runner[T]) -> Runner[T]: + def runner(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Future[T]: + def _wrapped() -> T: current = os.getcwd() os.chdir(dir) try: @@ -111,26 +109,29 @@ def _wrapped(): raise finally: os.chdir(current) + return parent(_wrapped) + return runner -class Script: - environment: t.Union[Environment, ManagedEnvironment] +class Script(AbstractContextManager["Script"], Awaitable["Script"]): + environment: Environment | ManagedEnvironment - def __init__(self, - what: Executor, - module: types.ModuleType, - environment: t.Union[Environment, ManagedEnvironment], - runner: Runner[T] + def __init__( + self, + what: Executor, + module: types.ModuleType, + environment: Environment | ManagedEnvironment, + runner: Runner[Self], ) -> None: self.what = what self.environment = environment self.runner = runner self.module = module - self._future = None + self._future: Future[Self] | None = None - def _run_inline(self) -> 'Script': + def _run_inline(self) -> Self: with self.environment.use(): self.what(WrapAllErrors(), self.module) return self @@ -139,10 +140,10 @@ def _run_inline(self) -> 'Script': # Public API @unified() - def get_variable(self, name: str, default: t.Optional[str]=None) -> Future[t.Optional[str]]: + def get_variable(self, name: str, default: str | None = None) -> Future[str | None]: return UnifiedFuture.resolve(getattr(self.module, name, default)) - def run(self) -> Future['Script']: + def run(self) -> Future[Self]: """ Runs the script. @@ -153,13 +154,13 @@ def run(self) -> Future['Script']: self._future = self.runner(self._run_inline) return self._future - def result(self) -> 'Script': + def result(self) -> Self: """ Runs the script and blocks until the script has finished running. """ return self.run().result() - def dispose(self): + def dispose(self) -> None: """ Disposes the managed environment. """ @@ -167,37 +168,36 @@ def dispose(self): raise ValueError("You can only scripts backed by managed environments") self.environment.dispose() - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, _, __, ___): + def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: if isinstance(self.environment, ManagedEnvironment): self.dispose() - async def run_async(self): + async def run_async(self) -> Self: """ Runs the script asynchronously, but it returns a coroutine. """ return await make_awaitable(self.run()) - def __await__(self): + def __await__(self) -> Generator[Any, None, Self]: """ Runs the script and waits until the script has completed. """ return self.run_async().__await__() - -EnvironmentType = t.Union[Environment, ManagedEnvironment, Policy, Script] +EnvironmentType = Environment | ManagedEnvironment | Policy | Script def script( - script: os.PathLike, - environment: t.Optional[EnvironmentType]=None, - *, - module_name: str = "__vapoursynth__", - inline: bool=True, - chdir: t.Optional[os.PathLike] = None + script: os.PathLike[str], + environment: EnvironmentType | None = None, + *, + module_name: str = "__vapoursynth__", + inline: bool = True, + chdir: os.PathLike[str] | None = None, ) -> Script: """ Runs the script at the given path. @@ -214,7 +214,8 @@ def script( :returns: A script object. It script starts running when you call start() on it, or await it. """ - def _execute(ctx, module): + + def _execute(ctx: AbstractContextManager[None], module: types.ModuleType) -> None: with ctx: runpy.run_path(str(script), module.__dict__, module.__name__) @@ -222,12 +223,12 @@ def _execute(ctx, module): def variables( - variables: t.Mapping[str, str], - environment: t.Optional[EnvironmentType]=None, - *, - module_name: str = "__vapoursynth__", - inline: bool=True, - chdir: t.Optional[os.PathLike] = None + variables: Mapping[str, str], + environment: EnvironmentType | None = None, + *, + module_name: str = "__vapoursynth__", + inline: bool = True, + chdir: os.PathLike[str] | None = None, ) -> Script: """ Sets variables to the module. @@ -246,7 +247,8 @@ def variables( :returns: A script object. It script starts running when you call start() on it, or await it. """ - def _execute(ctx, module): + + def _execute(ctx: AbstractContextManager[None], module: types.ModuleType) -> None: with ctx: for k, v in variables.items(): setattr(module, k, v) @@ -255,12 +257,12 @@ def _execute(ctx, module): def code( - script: t.Union[str,bytes,ast.AST,types.CodeType], - environment: t.Optional[EnvironmentType]=None, - *, - module_name: str = "__vapoursynth__", - inline: bool=True, - chdir: t.Optional[os.PathLike] = None + script: str | bytes | ast.Module | types.CodeType, + environment: EnvironmentType | None = None, + *, + module_name: str = "__vapoursynth__", + inline: bool = True, + chdir: os.PathLike[str] | None = None, ) -> Script: """ Runs the given code snippet. @@ -279,39 +281,31 @@ def code( :returns: A script object. It script starts running when you call start() on it, or await it. """ - def _execute(ctx, module): + + def _execute(ctx: AbstractContextManager[None], module: types.ModuleType) -> None: + nonlocal script + with ctx: if isinstance(script, types.CodeType): code = script else: - code = compile( - script, - filename="", - dont_inherit=True, - flags=0, - mode="exec" - ) + code = compile(script, filename="", dont_inherit=True, flags=0, mode="exec") exec(code, module.__dict__, module.__dict__) + return _load(_execute, environment, module_name=module_name, inline=inline, chdir=chdir) def _load( - script: Executor, - environment: t.Optional[EnvironmentType]=None, - *, - module_name: str = "__vapoursynth__", - inline: bool=True, - chdir: t.Optional[os.PathLike] = None + script: Executor, + environment: EnvironmentType | None = None, + *, + module_name: str = "__vapoursynth__", + inline: bool = True, + chdir: os.PathLike[str] | None = None, ) -> Script: - if inline: - runner = inline_runner - else: - runner = to_thread + runner = inline_runner if inline else to_thread - if isinstance(environment, Script): - module = environment.module - else: - module = types.ModuleType(module_name) + module = environment.module if isinstance(environment, Script) else types.ModuleType(module_name) if isinstance(environment, Script): environment = environment.environment @@ -324,4 +318,3 @@ def _load( runner = chdir_runner(chdir, runner) return Script(script, module, environment, runner) - From ebf123c43aa0b02b9fafc1ef5ea03c4d641e949b Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 17:57:40 +0100 Subject: [PATCH 04/60] add vs stubs --- pyproject.toml | 8 +++-- uv.lock | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 33c1b46..66c42ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,8 +14,12 @@ trio = ["trio"] test = ["pytest"] [dependency-groups] -dev = ["mypy>=1.19.0", "ruff>=0.14.7", "trio"] - +dev = [ + "mypy>=1.19.0", + "ruff>=0.14.7", + "trio", + "vsstubs ; python_version>='3.13'", +] [build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" diff --git a/uv.lock b/uv.lock index d2fca56..abfbc3e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] [[package]] name = "attrs" @@ -34,6 +38,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.13' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -113,6 +129,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246, upload-time = "2025-11-29T14:01:44.13Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mypy" version = "1.19.0" @@ -228,6 +265,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, ] +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", marker = "python_full_version >= '3.13'" }, + { name = "pygments", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + [[package]] name = "ruff" version = "0.14.7" @@ -254,6 +304,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -289,6 +348,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, ] +[[package]] +name = "typer" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version >= '3.13'" }, + { name = "rich", marker = "python_full_version >= '3.13'" }, + { name = "shellingham", marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -328,6 +402,7 @@ dev = [ { name = "mypy" }, { name = "ruff" }, { name = "trio" }, + { name = "vsstubs", marker = "python_full_version >= '3.13'" }, ] [package.metadata] @@ -343,4 +418,19 @@ dev = [ { name = "mypy", specifier = ">=1.19.0" }, { name = "ruff", specifier = ">=0.14.7" }, { name = "trio" }, + { name = "vsstubs", marker = "python_full_version >= '3.13'" }, +] + +[[package]] +name = "vsstubs" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich", marker = "python_full_version >= '3.13'" }, + { name = "typer", marker = "python_full_version >= '3.13'" }, + { name = "vapoursynth", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/dd1505d1b5f090dceb4181865f148a99e80ac152e3c12ff453494c27f505/vsstubs-1.0.2.tar.gz", hash = "sha256:a184e1d8523d7fc9dab599d784d470e5df07471143ef35f3a1badd6e6588c1d1", size = 22530, upload-time = "2025-12-02T16:54:59.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/0b/bae98f4601fe4f04925ee30bda99fdc06d7f50191e204f7956ab42e23b5b/vsstubs-1.0.2-py3-none-any.whl", hash = "sha256:aefdf2027dec3979e3f714856ad571147a0744939cf2c4e779706463ad1f0bc4", size = 27716, upload-time = "2025-12-02T16:54:58.659Z" }, ] From cf2efd557d6f75f9b3d30a5f7b138a7ff0bad25e Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 17:59:46 +0100 Subject: [PATCH 05/60] fix unified typing --- tests/test_futures.py | 35 ++++++--- vsengine/_futures.py | 166 ++++++++++++++++++++++-------------------- vsengine/video.py | 8 +- vsengine/vpy.py | 2 +- 4 files changed, 117 insertions(+), 94 deletions(-) diff --git a/tests/test_futures.py b/tests/test_futures.py index 9dbb95a..8864589 100644 --- a/tests/test_futures.py +++ b/tests/test_futures.py @@ -18,6 +18,7 @@ def resolve(value): fut.set_result(value) return fut + def reject(err): fut = Future() fut.set_exception(err) @@ -28,20 +29,26 @@ def contextmanager(): @contextlib.contextmanager def noop(): yield 1 + return resolve(noop()) + def asynccontextmanager(): @contextlib.asynccontextmanager async def noop(): yield 2 + return resolve(noop()) + def succeeds(): return resolve(1) + def fails(): return reject(RuntimeError()) + def fails_early(): raise RuntimeError() @@ -50,7 +57,7 @@ def future_iterator(): n = 0 while True: yield resolve(n) - n+=1 + n += 1 class WrappedUnifiedFuture(UnifiedFuture): @@ -62,7 +69,6 @@ class WrappedUnifiedIterable(UnifiedIterator): class TestUnifiedFuture(unittest.TestCase): - @wrap_test_for_asyncio async def test_is_await(self): await UnifiedFuture.from_call(succeeds) @@ -121,16 +127,19 @@ def _init_thread(fut): fut.set_result(threading.current_thread()) fut = Future() - thr = threading.Thread(target=lambda:_init_thread(fut)) + thr = threading.Thread(target=lambda: _init_thread(fut)) + def _wrapper(): return fut fut = UnifiedFuture.from_call(_wrapper) loop_thread = None + def _record_loop_thr(_): nonlocal loop_thread loop_thread = threading.current_thread() + fut.add_loop_callback(_record_loop_thr) thr.start() cb_thread = await fut @@ -139,13 +148,14 @@ def _record_loop_thr(_): class UnifiedIteratorTest(unittest.TestCase): - def test_run_as_completed_succeeds(self): set_loop(NO_LOOP) my_futures = [Future(), Future()] results = [] + def _add_to_result(f): results.append(f.result()) + state = UnifiedIterator(iter(my_futures)).run_as_completed(_add_to_result) self.assertFalse(state.done()) my_futures[1].set_result(2) @@ -160,8 +170,9 @@ def test_run_as_completed_forwards_errors(self): my_futures = [Future(), Future()] results = [] errors = [] + def _add_to_result(f): - if (exc := f.exception()): + if exc := f.exception(): errors.append(exc) else: results.append(f.result()) @@ -182,6 +193,7 @@ def test_run_as_completed_cancels(self): set_loop(NO_LOOP) my_futures = [Future(), Future()] results = [] + def _add_to_result(f): results.append(f.result()) return False @@ -198,6 +210,7 @@ def test_run_as_completed_cancels_on_crash(self): set_loop(NO_LOOP) my_futures = [Future(), Future()] err = RuntimeError("test") + def _crash(_): raise err @@ -240,12 +253,15 @@ def _it(): def test_run_as_completed_cancels_on_iterator_crash(self): err = RuntimeError("test") + def _it(): if False: yield Future() raise err + def _noop(_): pass + state = UnifiedIterator(_it()).run_as_completed(_noop) self.assertTrue(state.done()) self.assertIs(state.exception(), err) @@ -254,7 +270,7 @@ def test_can_iter_futures(self): n = 0 for fut in UnifiedIterator.from_call(future_iterator).futures: self.assertEqual(n, fut.result()) - n+=1 + n += 1 if n > 100: break @@ -262,7 +278,7 @@ def test_can_iter(self): n = 0 for n2 in UnifiedIterator.from_call(future_iterator): self.assertEqual(n, n2) - n+=1 + n += 1 if n > 100: break @@ -271,13 +287,12 @@ async def test_can_aiter(self): n = 0 async for n2 in UnifiedIterator.from_call(future_iterator): self.assertEqual(n, n2) - n+=1 + n += 1 if n > 100: break class UnifiedFunctionTest(unittest.TestCase): - def test_unified_auto_future_return_a_unified_future(self): @unified() def test_func(): @@ -299,7 +314,7 @@ def test_func(): self.assertEqual(next(f), 2) def test_unified_generator_accepts_other_iterables(self): - @unified(type="generator") + @unified(kind="generator") def test_func(): return iter((resolve(1), resolve(2))) diff --git a/vsengine/_futures.py b/vsengine/_futures.py index 375a658..97e7f6e 100644 --- a/vsengine/_futures.py +++ b/vsengine/_futures.py @@ -10,28 +10,25 @@ from functools import wraps from inspect import isgeneratorfunction from types import TracebackType -from typing import Any, Self +from typing import Any, Literal, Self, overload from vsengine.loops import Cancelled, get_loop, keep_environment -type UnifiedRunner[T, **P] = Callable[P, Future[T] | Iterator[Future[T]]] -type UnifiedCallable[T] = Callable[..., UnifiedFuture[T] | UnifiedIterator[T]] - class UnifiedFuture[T]( Future[T], AbstractContextManager[Any, Any], AbstractAsyncContextManager[Any, Any], Awaitable[T] ): @classmethod - def from_call[**P](cls, func: UnifiedRunner[T, P], *args: P.args, **kwargs: P.kwargs) -> UnifiedFuture[T]: + def from_call[**P](cls, func: Callable[P, Future[T]], *args: P.args, **kwargs: P.kwargs) -> Self: try: future = func(*args, **kwargs) except Exception as e: return cls.reject(e) - return cls.from_future(future) # type: ignore + return cls.from_future(future) @classmethod - def from_future(cls, future: Future[T]) -> UnifiedFuture[T]: + def from_future(cls, future: Future[T]) -> Self: if isinstance(future, cls): return future @@ -47,13 +44,13 @@ def _receive(fn: Future[T]) -> None: return result @classmethod - def resolve(cls, value: T) -> UnifiedFuture[T]: + def resolve(cls, value: T) -> Self: future = cls() future.set_result(value) return future @classmethod - def reject(cls, error: BaseException) -> UnifiedFuture[Any]: + def reject(cls, error: BaseException) -> Self: future = cls() future.set_exception(error) return future @@ -75,7 +72,7 @@ def then[V]( ) -> UnifiedFuture[V]: result = UnifiedFuture[V]() - def _run_cb[T0](cb: Callable[[T0], V], v: T0) -> None: + def _run_cb(cb: Callable[[Any], V], v: Any) -> None: try: r = cb(v) except BaseException as e: @@ -93,7 +90,7 @@ def _done(fn: Future[T]) -> None: if success_cb is not None: _run_cb(success_cb, self.result()) else: - result.set_result(self.result()) # type: ignore + result.set_result(self.result()) # type: ignore[arg-type] self.add_done_callback(_done) return result @@ -105,13 +102,13 @@ def catch[V](self, cb: Callable[[BaseException], V]) -> UnifiedFuture[V]: return self.then(None, cb) # Nicer Syntax - def __enter__(self) -> None: + def __enter__(self) -> Any: obj = self.result() if isinstance(obj, AbstractContextManager): return obj.__enter__() - raise NotImplementedError("(async) with is not implemented for this objec") + raise NotImplementedError("(async) with is not implemented for this object") def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: obj = self.result() @@ -119,7 +116,7 @@ def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, t if isinstance(obj, AbstractContextManager): return obj.__exit__(exc, val, tb) - raise NotImplementedError("(async) with is not implemented for this objec") + raise NotImplementedError("(async) with is not implemented for this object") async def awaitable(self) -> T: return await get_loop().await_future(self) @@ -127,7 +124,7 @@ async def awaitable(self) -> T: def __await__(self) -> Generator[Any, None, T]: return self.awaitable().__await__() - async def __aenter__(self) -> T: + async def __aenter__(self) -> Any: result = await self.awaitable() if isinstance(result, AbstractAsyncContextManager): @@ -135,7 +132,7 @@ async def __aenter__(self) -> T: if isinstance(result, AbstractContextManager): return result.__enter__() - raise NotImplementedError("(async) with is not implemented for this objec") + raise NotImplementedError("(async) with is not implemented for this object") async def __aexit__( self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None @@ -147,7 +144,7 @@ async def __aexit__( if isinstance(result, AbstractContextManager): return result.__exit__(exc, val, tb) - raise NotImplementedError("(async) with is not implemented for this objec") + raise NotImplementedError("(async) with is not implemented for this object") class UnifiedIterator[T](Iterator[T], AsyncIterator[T]): @@ -155,8 +152,8 @@ def __init__(self, future_iterable: Iterator[Future[T]]) -> None: self.future_iterable = future_iterable @classmethod - def from_call[**P](cls, func: UnifiedRunner[T, P], *args: P.args, **kwargs: P.kwargs) -> UnifiedIterator[T]: - return cls(func(*args, **kwargs)) # type: ignore + def from_call[**P](cls, func: Callable[P, Iterator[Future[T]]], *args: P.args, **kwargs: P.kwargs) -> Self: + return cls(func(*args, **kwargs)) @property def futures(self) -> Iterator[Future[T]]: @@ -273,89 +270,100 @@ async def __anext__(self) -> T: return await get_loop().await_future(fut) -# TODO: Probably needs overloads +@overload +def unified[T, **P]( + *, + kind: Literal["generator"], +) -> Callable[ + [Callable[P, Iterator[Future[T]]]], + Callable[P, UnifiedIterator[T]], +]: ... + -# type UnifiedRunner[T, **P] = Callable[P, Future[T] | Iterator[Future[T]]] -# type UnifiedCallable[T] = Callable[..., UnifiedFuture[T] | UnifiedIterator[T]] +@overload +def unified[T, **P]( + *, + kind: Literal["future"], +) -> Callable[ + [Callable[P, Future[T]]], + Callable[P, UnifiedFuture[T]], +]: ... -# @overload -# def unified[T, **P]( -# type : s t r = ..., -# future_class: type[UnifiedFuture[T]] = UnifiedFuture[Any], -# iterable_class: type[UnifiedIterator[T]] = UnifiedIterator[Any], -# ) -> ( -# Callable[ -# [Runner[P, Future[T]]], -# Callable[..., UnifiedFuture[T]], -# ] -# | Callable[ -# [Runner[P, Iterator[Future[T]]]], -# Callable[..., UnifiedIterator[T]], -# ] -# ): ... +@overload +def unified[T, **P]( + *, + kind: Literal["generator"], + iterable_class: type[UnifiedIterator[T]], +) -> Callable[ + [Callable[P, Iterator[Future[T]]]], + Callable[P, UnifiedIterator[T]], +]: ... -# @overload -# def unified[T, **P]( -# typee: L iteral["future"] = "future", future_class: type[UnifiedFuture[T]] = UnifiedFuture[Any] -# ) -> Callable[ -# [Runner[P, Future[T]]], -# Callable[..., UnifiedFuture[T]], -# ]: ... +@overload +def unified[T, **P]( + *, + kind: Literal["future"], + future_class: type[UnifiedFuture[T]], +) -> Callable[ + [Callable[P, Future[T]]], + Callable[P, UnifiedFuture[T]], +]: ... -# @overload -# def unified[T, **P]( -# typee: Literal["generator"] = "generator", -# *, -# iterable_class: type[UnifiedIterator[T]] = UnifiedIterator[Any], -# ) -> Callable[ -# [Runner[P, Iterator[Future[T]]]], -# Callable[..., UnifiedIterator[T]], -# ]: ... +@overload +def unified[T, **P]( + *, + kind: Literal["auto"] = "auto", + iterable_class: type[UnifiedIterator[Any]] = ..., + future_class: type[UnifiedFuture[Any]] = ..., +) -> Callable[ + [Callable[P, Future[T] | Iterator[Future[T]]]], + Callable[P, UnifiedFuture[T] | UnifiedIterator[T]], +]: ... +# Implementation def unified[T, **P]( - type: str = "auto", - future_class: type[UnifiedFuture[T]] = UnifiedFuture[Any], - iterable_class: type[UnifiedIterator[T]] = UnifiedIterator[Any], -) -> ( - Callable[ - [Callable[P, Future[T]]], - Callable[..., UnifiedFuture[T]], - ] - | Callable[ - [Callable[P, Iterator[Future[T]]]], - Callable[..., UnifiedIterator[T]], - ] -): - def _wrap_generator(func: UnifiedRunner[T, P]) -> UnifiedCallable[T]: + *, + kind: str = "auto", + iterable_class: type[UnifiedIterator[Any]] = UnifiedIterator[Any], + future_class: type[UnifiedFuture[Any]] = UnifiedFuture[Any], +) -> Any: + """ + Decorator to normalize functions returning Future[T] or Iterator[Future[T]] + into functions returning UnifiedFuture[T] or UnifiedIterator[T]. + """ + + def _decorator_generator(func: Callable[P, Iterator[Future[T]]]) -> Callable[P, UnifiedIterator[T]]: @wraps(func) - def _wrapped(*args: Any, **kwargs: Any) -> UnifiedIterator[T]: + def _wrapped(*args: P.args, **kwargs: P.kwargs) -> UnifiedIterator[T]: return iterable_class.from_call(func, *args, **kwargs) return _wrapped - def _wrap_future(func: UnifiedRunner[T, P]) -> UnifiedCallable[T]: + def _decorator_future(func: Callable[P, Future[T]]) -> Callable[P, UnifiedFuture[T]]: @wraps(func) - def _wrapped(*args: Any, **kwargs: Any) -> UnifiedFuture[T]: + def _wrapped(*args: P.args, **kwargs: P.kwargs) -> UnifiedFuture[T]: return future_class.from_call(func, *args, **kwargs) return _wrapped - def _wrapper(func: UnifiedRunner[T, P]) -> UnifiedCallable[T]: - if type == "auto": + def decorator( + func: Callable[P, Iterator[Future[T]]] | Callable[P, Future[T]], + ) -> Callable[P, UnifiedIterator[T]] | Callable[P, UnifiedFuture[T]]: + if kind == "auto": if isgeneratorfunction(func): - return _wrap_generator(func) - return _wrap_future(func) + return _decorator_generator(func) + return _decorator_future(func) # type:ignore[arg-type] - if type == "generator": - return _wrap_generator(func) + if kind == "generator": + return _decorator_generator(func) # type:ignore[arg-type] - if type == "future": - return _wrap_future(func) + if kind == "future": + return _decorator_future(func) # type:ignore[arg-type] raise NotImplementedError - return _wrapper + return decorator diff --git a/vsengine/video.py b/vsengine/video.py index da59d5e..1d02040 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -16,7 +16,7 @@ from vsengine._nodes import buffer_futures, close_when_needed -@unified() +@unified(kind="future") def frame( node: vapoursynth.VideoNode, frameno: int, env: EnvironmentTypes | None = None ) -> Future[vapoursynth.VideoFrame]: @@ -24,7 +24,7 @@ def frame( return node.get_frame_async(frameno) -@unified() +@unified(kind="future") def planes( node: vapoursynth.VideoNode, frameno: int, @@ -44,7 +44,7 @@ def _extract(frame: vapoursynth.VideoFrame) -> tuple[bytes, ...]: return frame(node, frameno, env).map(_extract) -@unified(type="generator") +@unified(kind="generator") def frames( node: vapoursynth.VideoNode, env: EnvironmentTypes | None = None, @@ -70,7 +70,7 @@ def frames( return it -@unified(type="generator") +@unified(kind="generator") def render( node: vapoursynth.VideoNode, env: int | None = None, diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 3537de3..a9e8bc6 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -139,7 +139,7 @@ def _run_inline(self) -> Self: ### # Public API - @unified() + @unified(kind="future") def get_variable(self, name: str, default: str | None = None) -> Future[str | None]: return UnifiedFuture.resolve(getattr(self.module, name, default)) From ccef8b63d80badb9484c87eb260e9a7d633f52ed Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 17:59:58 +0100 Subject: [PATCH 06/60] use logger.warning --- vsengine/_hospice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsengine/_hospice.py b/vsengine/_hospice.py index 6f73d74..b773f63 100644 --- a/vsengine/_hospice.py +++ b/vsengine/_hospice.py @@ -100,7 +100,7 @@ def _collectstage2(phase: Literal["start", "stop"], _: dict[str, int]) -> None: with lock: for ident in tuple(stage2): if _is_core_still_used(ident): - logger.warn(f"Core is still in use in stage 2. ID:{ident}") + logger.warning(f"Core is still in use in stage 2. ID:{ident}") continue stage2.remove(ident) From b2dd35482e18a2b9e0448d34e81a0c86d92c2145 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 18:00:57 +0100 Subject: [PATCH 07/60] fix EnvironmentStore typing --- vsengine/policy.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/vsengine/policy.py b/vsengine/policy.py index f73f99d..3cdae7e 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -68,7 +68,7 @@ import weakref from collections.abc import Iterator, Mapping from types import TracebackType -from typing import Any, Protocol, Self +from typing import TYPE_CHECKING, Self import vapoursynth as vs from vapoursynth import Environment, EnvironmentData, EnvironmentPolicy, EnvironmentPolicyAPI, register_policy @@ -81,18 +81,18 @@ logger = logging.getLogger(__name__) -class EnvironmentStore(Protocol): +class EnvironmentStore: """ Environment Stores manage which environment is currently active. """ - def set_current_environment(self, environment: Any) -> None: + def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: """ Set the current environment in the store. """ ... - def get_current_environment(self) -> Any: + def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: """ Retrieve the current environment from the store (if any) """ @@ -104,16 +104,16 @@ class GlobalStore(EnvironmentStore): This is the simplest store: It just stores the environment in a variable. """ - _current: EnvironmentData | None + _current: weakref.ReferenceType[EnvironmentData] | None __slots__ = ("_current",) def __init__(self) -> None: self._current = None - def set_current_environment(self, environment: EnvironmentData | None) -> None: + def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: self._current = environment - def get_current_environment(self) -> EnvironmentData | None: + def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: return self._current @@ -129,10 +129,10 @@ class ThreadLocalStore(EnvironmentStore): def __init__(self) -> None: self._current = threading.local() - def set_current_environment(self, environment: EnvironmentData | None) -> None: + def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: self._current.environment = environment - def get_current_environment(self) -> EnvironmentData | None: + def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: return getattr(self._current, "environment", None) @@ -141,15 +141,15 @@ class ContextVarStore(EnvironmentStore): If you are using AsyncIO or similar frameworks, use this store. """ - _current: contextvars.ContextVar[EnvironmentData | None] + _current: contextvars.ContextVar[weakref.ReferenceType[EnvironmentData] | None] def __init__(self, name: str = "vapoursynth") -> None: self._current = contextvars.ContextVar(name) - def set_current_environment(self, environment: EnvironmentData | None) -> None: + def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: self._current.set(environment) - def get_current_environment(self) -> EnvironmentData | None: + def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: return self._current.get(None) @@ -217,6 +217,9 @@ def get_current_environment(self) -> EnvironmentData | None: received_environment = current_environment() + if TYPE_CHECKING: + assert received_environment + if not self.is_alive(received_environment): logger.warning(f"Got dead environment: {received_environment!r}") # Remove the environment. @@ -307,12 +310,13 @@ def use(self) -> Iterator[None]: """ Switches to this environment within a block. """ - prev_environment = self._policy.managed._store.get_current_environment() + # prev_environment = self._policy.managed._store.get_current_environment() with self._environment.use(): yield - # Workaround: On 32bit systems, environment policies do not reset. - self._policy.managed.set_environment(prev_environment) + # FIXME + # # Workaround: On 32bit systems, environment policies do not reset. + # self._policy.managed.set_environment(prev_environment) def switch(self) -> None: """ From 93a69eb56f9a9aa8b09fbfe5d57a4944cf28c957 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 18:11:09 +0100 Subject: [PATCH 08/60] render: fix type hint --- vsengine/video.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vsengine/video.py b/vsengine/video.py index 1d02040..dde5694 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -73,7 +73,7 @@ def frames( @unified(kind="generator") def render( node: vapoursynth.VideoNode, - env: int | None = None, + env: EnvironmentTypes | None = None, *, prefetch: int = 0, backlog: int | None = 0, @@ -125,7 +125,7 @@ def render_single_frame(frame: vapoursynth.VideoFrame) -> tuple[int, bytes]: if y4m: buf.append(b"FRAME\n") - for plane in frame: + for plane in iter(frame): buf.append(bytes(plane)) return current_frame, b"".join(buf) From c181c63d6fa5979908f3683ca52f16f4360d90f5 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 18:11:32 +0100 Subject: [PATCH 09/60] Iterable -> Iterator --- vsengine/_nodes.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vsengine/_nodes.py b/vsengine/_nodes.py index b1274d5..f358b1a 100644 --- a/vsengine/_nodes.py +++ b/vsengine/_nodes.py @@ -2,7 +2,7 @@ # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -from collections.abc import Iterable +from collections.abc import Iterable, Iterator from concurrent.futures import Future from contextlib import AbstractContextManager from threading import RLock @@ -12,7 +12,7 @@ def buffer_futures[T_co]( futures: Iterable[Future[T_co]], prefetch: int = 0, backlog: int | None = None -) -> Iterable[Future[T_co]]: +) -> Iterator[Future[T_co]]: if prefetch == 0: prefetch = core.num_threads if backlog is None: @@ -70,7 +70,6 @@ def _refill() -> None: _refill() sidx = 0 - fut: Future[T_co] try: while (not finished) or (len(reorder) > 0) or running > 0: if sidx not in reorder: @@ -89,7 +88,7 @@ def _refill() -> None: finished = True -def close_when_needed[T](future_iterable: Iterable[Future[AbstractContextManager[T]]]) -> Iterable[Future[T]]: +def close_when_needed[T](future_iterable: Iterable[Future[AbstractContextManager[T]]]) -> Iterator[Future[T]]: def copy_future_and_run_cb_before(fut: Future[AbstractContextManager[T]]) -> Future[T]: f = Future[T]() From cdae5b391b64c957b3f81100776b986897756915 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 18:11:52 +0100 Subject: [PATCH 10/60] add pytest --- pyproject.toml | 1 + uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 66c42ec..1425e10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ test = ["pytest"] [dependency-groups] dev = [ "mypy>=1.19.0", + "pytest>=9.0.1", "ruff>=0.14.7", "trio", "vsstubs ; python_version>='3.13'", diff --git a/uv.lock b/uv.lock index abfbc3e..eeb9da8 100644 --- a/uv.lock +++ b/uv.lock @@ -400,6 +400,7 @@ trio = [ [package.dev-dependencies] dev = [ { name = "mypy" }, + { name = "pytest" }, { name = "ruff" }, { name = "trio" }, { name = "vsstubs", marker = "python_full_version >= '3.13'" }, @@ -416,6 +417,7 @@ provides-extras = ["trio", "test"] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = ">=1.19.0" }, + { name = "pytest", specifier = ">=9.0.1" }, { name = "ruff", specifier = ">=0.14.7" }, { name = "trio" }, { name = "vsstubs", marker = "python_full_version >= '3.13'" }, From 623f92b66a92fe603b84210f73a18bda239a8053 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 18:16:32 +0100 Subject: [PATCH 11/60] format tests --- extra_tests/pytest.py | 42 ++-- extra_tests/unittest.py | 22 +- tests/fixtures/pytest_core_in_module.py | 5 +- tests/fixtures/pytest_core_stored_in_test.py | 2 - tests/fixtures/pytest_core_succeeds.py | 1 - tests/fixtures/unittest_core_in_module.py | 3 +- .../fixtures/unittest_core_stored_in_test.py | 3 +- tests/fixtures/unittest_core_succeeds.py | 2 +- tests/test_convert.py | 47 ++-- tests/test_futures.py | 8 +- tests/test_helpers.py | 41 ++-- tests/test_hospice.py | 32 ++- tests/test_loop_adapters.py | 73 +++--- tests/test_loops.py | 37 ++- tests/test_policy.py | 41 ++-- tests/test_policy_store.py | 12 +- tests/test_tests_pytest.py | 10 +- tests/test_tests_unittest.py | 9 +- tests/test_video.py | 11 +- tests/test_vpy.py | 222 +++++++++--------- 20 files changed, 285 insertions(+), 338 deletions(-) diff --git a/extra_tests/pytest.py b/extra_tests/pytest.py index b462350..33d0915 100644 --- a/extra_tests/pytest.py +++ b/extra_tests/pytest.py @@ -4,29 +4,20 @@ # SPDX-License-Identifier: EUPL-1.2 import pathlib + import pytest -from vsengine.policy import Policy, GlobalStore -from vsengine._hospice import any_alive, freeze +from vsengine._hospice import any_alive, freeze +from vsengine.policy import GlobalStore, Policy -DEFAULT_STAGES = ( - "initial-core", - "reloaded-core" -) +DEFAULT_STAGES = ("initial-core", "reloaded-core") -KNOWN_STAGES = [ - "no-core", - "initial-core", - "reloaded-core", - "unique-core" -] +KNOWN_STAGES = ["no-core", "initial-core", "reloaded-core", "unique-core"] DEFAULT_ERROR_MESSAGE = [ "Your test suite left a dangling object to a vapoursynth core.", - "Please make sure this does not happen, " - "as this might cause some previewers to crash " - "after reloading a script." + "Please make sure this does not happen, as this might cause some previewers to crash after reloading a script.", ] @@ -36,18 +27,22 @@ def pytest_configure(config: "Config") -> None: config.addinivalue_line( "markers", 'vpy(*stages: Literal["no_core", "first_core", "second_core"]): ' - 'Mark what stages should be run. (Defaults to first_core+second_core)' + "Mark what stages should be run. (Defaults to first_core+second_core)", ) + ### # Make sure a policy is registered before tests are collected. current_policy = None current_env = None + + def pytest_sessionstart(session): global current_policy current_policy = Policy(GlobalStore()) current_policy.register() + def pytest_sessionfinish(): global current_policy, current_env if current_env is not None: @@ -130,6 +125,7 @@ def pytest_pycollect_makeitem(collector, name, obj) -> None: else: obj._vpy_stages = DEFAULT_STAGES + def pytest_generate_tests(metafunc): obj = metafunc.function if hasattr(obj, "_vpy_stages"): @@ -154,7 +150,11 @@ def pytest_collection_modifyitems(session, config, items): new_items.extend(stages[stage]) # Add two synthetic tests that make sure the environment is clean. if stage in ("initial-core", "reloaded-core"): - new_items.append(EnsureCleanEnvironment.from_parent(virtual_parent, name=f"@check-clean-environment[{stage}]", stage=stage)) + new_items.append( + EnsureCleanEnvironment.from_parent( + virtual_parent, name=f"@check-clean-environment[{stage}]", stage=stage + ) + ) items[:] = new_items @@ -162,6 +162,8 @@ def pytest_collection_modifyitems(session, config, items): ### # Do the magic current_stage = "no-core" + + @pytest.hookimpl(tryfirst=True) def pytest_pyfunc_call(pyfuncitem): global current_stage, current_env @@ -205,14 +207,18 @@ def pytest_pyfunc_call(pyfuncitem): if any_alive(): freeze() if failed is False: - pyfuncitem._repr_failure_py = lambda _, style=None: CleanupFailed(None, "\n".join(DEFAULT_ERROR_MESSAGE)) + pyfuncitem._repr_failure_py = lambda _, style=None: CleanupFailed( + None, "\n".join(DEFAULT_ERROR_MESSAGE) + ) assert False else: pre_rfp = pyfuncitem._repr_failure_py + def _new_rfp(*args, **kwargs): previous = pre_rfp(*args, **kwargs) err = "\n".join(DEFAULT_ERROR_MESSAGE) return CleanupFailed(previous, err) + pyfuncitem._repr_failure_py = _new_rfp raise failed elif failed: diff --git a/extra_tests/unittest.py b/extra_tests/unittest.py index 7e98a07..1cf5998 100644 --- a/extra_tests/unittest.py +++ b/extra_tests/unittest.py @@ -4,20 +4,17 @@ # SPDX-License-Identifier: EUPL-1.2 import sys from unittest.main import TestProgram -from vsengine.policy import Policy, GlobalStore -from vsengine._hospice import any_alive, freeze +from vsengine._hospice import any_alive, freeze +from vsengine.policy import GlobalStore, Policy DEFAULT_ERROR_MESSAGE = [ "Your test suite left a dangling object to a vapoursynth core.", - "Please make sure this does not happen, " - "as this might cause some previewers to crash " - "after reloading a script." + "Please make sure this does not happen, as this might cause some previewers to crash after reloading a script.", ] class MultiCoreTestProgram(TestProgram): - def __init__(self, *args, **kwargs): self._policy = Policy(GlobalStore()) self._policy.register() @@ -38,9 +35,8 @@ def parseArgs(self, argv: list[str]) -> None: def runTests(self): any_alive_left = False - with self._policy.new_environment() as e1: - with e1.use(): - self._run_once() + with self._policy.new_environment() as e1, e1.use(): + self._run_once() del e1 if self.exit and not self.result.wasSuccessful(): @@ -52,9 +48,8 @@ def runTests(self): freeze() super().parseArgs(self.argv) - with self._policy.new_environment() as e2: - with e2.use(): - self._run_once() + with self._policy.new_environment() as e2, e2.use(): + self._run_once() del e2 if any_alive(): @@ -71,10 +66,9 @@ def runTests(self): sys.exit(0) - def main(): MultiCoreTestProgram(module=None) + if __name__ == "__main__": main() - diff --git a/tests/fixtures/pytest_core_in_module.py b/tests/fixtures/pytest_core_in_module.py index 581005e..d45fb6d 100644 --- a/tests/fixtures/pytest_core_in_module.py +++ b/tests/fixtures/pytest_core_in_module.py @@ -1,15 +1,14 @@ -import pytest from vapoursynth import core - clip = core.std.BlankClip() def test_should_never_be_run(): import os + try: os._exit(3) except AttributeError: import sys - sys.exit(3) + sys.exit(3) diff --git a/tests/fixtures/pytest_core_stored_in_test.py b/tests/fixtures/pytest_core_stored_in_test.py index b678503..8fb9334 100644 --- a/tests/fixtures/pytest_core_stored_in_test.py +++ b/tests/fixtures/pytest_core_stored_in_test.py @@ -1,7 +1,5 @@ -import pytest from vapoursynth import core - test = [0] diff --git a/tests/fixtures/pytest_core_succeeds.py b/tests/fixtures/pytest_core_succeeds.py index 9efe584..3198664 100644 --- a/tests/fixtures/pytest_core_succeeds.py +++ b/tests/fixtures/pytest_core_succeeds.py @@ -1,4 +1,3 @@ -import pytest from vapoursynth import core diff --git a/tests/fixtures/unittest_core_in_module.py b/tests/fixtures/unittest_core_in_module.py index 50f6938..1059c51 100644 --- a/tests/fixtures/unittest_core_in_module.py +++ b/tests/fixtures/unittest_core_in_module.py @@ -1,11 +1,10 @@ import unittest -from vapoursynth import core +from vapoursynth import core core.std.BlankClip class TestCoreInModule(unittest.TestCase): - def test_something(self): raise RuntimeError("We should not even get here.") diff --git a/tests/fixtures/unittest_core_stored_in_test.py b/tests/fixtures/unittest_core_stored_in_test.py index e5da8d1..fb3cb2f 100644 --- a/tests/fixtures/unittest_core_stored_in_test.py +++ b/tests/fixtures/unittest_core_stored_in_test.py @@ -1,11 +1,10 @@ import unittest -from vapoursynth import core +from vapoursynth import core atom = [None] class TestCoreStoredLongTerm(unittest.TestCase): - def test_something(self): atom[0] = core.std.BlankClip diff --git a/tests/fixtures/unittest_core_succeeds.py b/tests/fixtures/unittest_core_succeeds.py index 38dd2d7..8b55544 100644 --- a/tests/fixtures/unittest_core_succeeds.py +++ b/tests/fixtures/unittest_core_succeeds.py @@ -1,8 +1,8 @@ import unittest + from vapoursynth import core class TestCoreSucceeds(unittest.TestCase): - def test_something(self): core.std.BlankClip().get_frame(0) diff --git a/tests/test_convert.py b/tests/test_convert.py index 324b0d1..b9cb12b 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,16 +1,15 @@ -import os import json +import os import unittest -from vapoursynth import core import vapoursynth as vs +from vapoursynth import core from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy from vsengine.convert import to_rgb, yuv_heuristic - DIR = os.path.dirname(__file__) -# Generated with +# Generated with # mediainfo -Output=JOSN -Full [Filenames] # | jq '.media.track[] | select(."@type" == "Video") | {matrix: .matrix_coefficients, width: .Width, height: .Height, primaries: .colour_primaries, transfer: .transfer_characteristics, chromaloc: .ChromaSubsampling_Position} | select(.matrix)' | jq -s # @@ -22,28 +21,10 @@ with open(PATH) as h: HEURISTIC_EXAMPLES = json.load(h) -MATRIX_MAPPING = { - "BT.2020 non-constant": "2020ncl", - "BT.709": "709", - "BT.470 System B/G": "470bg", - "BT.601": "170m" -} -TRANSFER_MAPPING = { - "PQ": "st2084", - "BT.709": "709", - "BT.470 System B/G": "470bg", - "BT.601": "601" -} -PRIMARIES_MAPPING = { - "BT.2020": "2020", - "BT.709": "709", - "BT.601 PAL": "470bg", - "BT.601 NTSC": "170m" -} -CHROMALOC_MAPPING = { - None: "left", - "Type 2": "top_left" -} +MATRIX_MAPPING = {"BT.2020 non-constant": "2020ncl", "BT.709": "709", "BT.470 System B/G": "470bg", "BT.601": "170m"} +TRANSFER_MAPPING = {"PQ": "st2084", "BT.709": "709", "BT.470 System B/G": "470bg", "BT.601": "601"} +PRIMARIES_MAPPING = {"BT.2020": "2020", "BT.709": "709", "BT.601 PAL": "470bg", "BT.601 NTSC": "170m"} +CHROMALOC_MAPPING = {None: "left", "Type 2": "top_left"} class TestToRGB(unittest.TestCase): @@ -56,6 +37,7 @@ def tearDown(self) -> None: def test_heuristics_provides_all_arguments(self): yuv = core.std.BlankClip(format=vs.YUV420P8) + def _pseudo_scaler(c, **args): self.assertTrue("chromaloc_in_s" in args) self.assertTrue("range_in_s" in args) @@ -80,13 +62,12 @@ def test_heuristics_with_examples(self): raw_matrix = result["matrix_in_s"] raw_chromaloc = result["chromaloc_in_s"] - if raw_primary != PRIMARIES_MAPPING[example["primaries"]]: - count_misses += 1 - elif raw_transfer != TRANSFER_MAPPING[example["transfer"]]: - count_misses += 1 - elif raw_matrix != MATRIX_MAPPING[example["matrix"]]: - count_misses += 1 - elif raw_chromaloc != CHROMALOC_MAPPING[example["chromaloc"]]: + if ( + raw_primary != PRIMARIES_MAPPING[example["primaries"]] + or raw_transfer != TRANSFER_MAPPING[example["transfer"]] + or raw_matrix != MATRIX_MAPPING[example["matrix"]] + or raw_chromaloc != CHROMALOC_MAPPING[example["chromaloc"]] + ): count_misses += 1 else: count_hits += 1 diff --git a/tests/test_futures.py b/tests/test_futures.py index 8864589..fc8d5ac 100644 --- a/tests/test_futures.py +++ b/tests/test_futures.py @@ -3,14 +3,14 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import unittest -import threading import contextlib +import threading +import unittest from concurrent.futures import Future -from vsengine._testutils import wrap_test_for_asyncio from vsengine._futures import UnifiedFuture, UnifiedIterator, unified -from vsengine.loops import set_loop, NO_LOOP +from vsengine._testutils import wrap_test_for_asyncio +from vsengine.loops import NO_LOOP, set_loop def resolve(value): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 975f5f1..f029ed0 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -3,10 +3,9 @@ import vapoursynth as vs from vapoursynth import core -from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy -from vsengine.policy import Policy, GlobalStore - from vsengine._helpers import use_inline, wrap_variable_size +from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy +from vsengine.policy import GlobalStore, Policy class TestUseInline(unittest.TestCase): @@ -22,30 +21,24 @@ def test_with_standalone(self): pass def test_with_set_environment(self): - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - with use_inline("test_with_set_environment", None): - pass + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + with use_inline("test_with_set_environment", None): + pass def test_fails_without_an_environment(self): - with Policy(GlobalStore()): - with self.assertRaises(EnvironmentError): - with use_inline("test_fails_without_an_environment", None): - pass + with Policy(GlobalStore()), self.assertRaises(EnvironmentError): + with use_inline("test_fails_without_an_environment", None): + pass def test_accepts_a_managed_environment(self): - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with use_inline("test_accepts_a_managed_environment", env): - self.assertEqual(env.vs_environment, vs.get_current_environment()) - + with Policy(GlobalStore()) as p, p.new_environment() as env: + with use_inline("test_accepts_a_managed_environment", env): + self.assertEqual(env.vs_environment, vs.get_current_environment()) def test_accepts_a_standard_environment(self): - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with use_inline("test_accepts_a_standard_environment", env.vs_environment): - self.assertEqual(env.vs_environment, vs.get_current_environment()) + with Policy(GlobalStore()) as p, p.new_environment() as env: + with use_inline("test_accepts_a_standard_environment", env.vs_environment): + self.assertEqual(env.vs_environment, vs.get_current_environment()) class TestWrapVariable(unittest.TestCase): @@ -58,9 +51,11 @@ def tearDown(self) -> None: def test_wrap_variable_bypasses_on_non_variable(self): bc = core.std.BlankClip() + def _wrapper(c): self.assertIs(c, bc) return c + wrap_variable_size(bc, bc.format, _wrapper) def test_wrap_caches_different_formats(self): @@ -69,6 +64,7 @@ def test_wrap_caches_different_formats(self): sp = core.std.Splice([bc24, bc48, bc24, bc48], mismatch=True) counter = 0 + def _wrapper(c): nonlocal counter counter += 1 @@ -87,6 +83,7 @@ def test_wrap_caches_different_sizes(self): sp = core.std.Splice([bc1, bc2, bc1, bc2], mismatch=True) counter = 0 + def _wrapper(c): nonlocal counter counter += 1 @@ -104,6 +101,7 @@ def test_wrap_stops_caching_once_size_exceeded(self): sp = core.std.Splice([*bcs, *bcs], mismatch=True) counter = 0 + def _wrapper(c): nonlocal counter counter += 1 @@ -114,4 +112,3 @@ def _wrapper(c): pass self.assertGreaterEqual(counter, 101) - diff --git a/tests/test_hospice.py b/tests/test_hospice.py index 7241649..e9a76b5 100644 --- a/tests/test_hospice.py +++ b/tests/test_hospice.py @@ -3,16 +3,17 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 +import contextlib import gc -import weakref import logging -import contextlib import unittest +import weakref from vsengine._hospice import admit_environment, any_alive, freeze, unfreeze -class Obj: pass +class Obj: + pass @contextlib.contextmanager @@ -23,8 +24,8 @@ def hide_logs(): finally: logging.disable(logging.NOTSET) -class HospiceTest(unittest.TestCase): +class HospiceTest(unittest.TestCase): def test_hospice_delays_connection(self): o1 = Obj() o2 = Obj() @@ -72,10 +73,15 @@ def test_hospice_reports_alive_objects_correctly(self): del o1 with hide_logs(): - self.assertTrue(any_alive(), "The hospice did report that all objects are not alive anymore. This is obviously not true.") + self.assertTrue( + any_alive(), + "The hospice did report that all objects are not alive anymore. This is obviously not true.", + ) del o2 - self.assertFalse(any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true.") + self.assertFalse( + any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true." + ) def test_hospice_can_forget_about_cores_safely(self): o1 = Obj() @@ -84,13 +90,21 @@ def test_hospice_can_forget_about_cores_safely(self): del o1 with hide_logs(): - self.assertTrue(any_alive(), "The hospice did report that all objects are not alive anymore. This is obviously not true.") + self.assertTrue( + any_alive(), + "The hospice did report that all objects are not alive anymore. This is obviously not true.", + ) freeze() - self.assertFalse(any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true.") + self.assertFalse( + any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true." + ) unfreeze() with hide_logs(): - self.assertTrue(any_alive(), "The hospice did report that all objects are not alive anymore. This is obviously not true.") + self.assertTrue( + any_alive(), + "The hospice did report that all objects are not alive anymore. This is obviously not true.", + ) del o2 gc.collect() diff --git a/tests/test_loop_adapters.py b/tests/test_loop_adapters.py index dca73b4..322efcc 100644 --- a/tests/test_loop_adapters.py +++ b/tests/test_loop_adapters.py @@ -3,27 +3,27 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 +import asyncio import contextlib import threading import unittest +from concurrent.futures import CancelledError, Future -import asyncio - -from concurrent.futures import Future, CancelledError - -from vsengine.loops import EventLoop, get_loop, set_loop, Cancelled -from vsengine.loops import NO_LOOP, _NoEventLoop from vsengine.adapters.asyncio import AsyncIOLoop +from vsengine.loops import NO_LOOP, Cancelled, EventLoop, _NoEventLoop, set_loop def make_async(func): def _wrapped(self, *args, **kwargs): return self.run_within_loop(func, args, kwargs) + return _wrapped + def is_async(func): def _wrapped(self, *args, **kwargs): return self.run_within_loop_async(func, args, kwargs) + return _wrapped @@ -52,23 +52,18 @@ def assertCancelled(self): @make_async def test_wrap_cancelled_without_cancellation(self): - with self.with_loop() as loop: - with loop.wrap_cancelled(): - pass + with self.with_loop() as loop, loop.wrap_cancelled(): + pass @make_async def test_wrap_cancelled_with_cancellation(self): - with self.with_loop() as loop: - with self.assertCancelled(): - with loop.wrap_cancelled(): - raise Cancelled + with self.with_loop() as loop, self.assertCancelled(), loop.wrap_cancelled(): + raise Cancelled @make_async def test_wrap_cancelled_with_other_exception(self): - with self.with_loop() as loop: - with self.assertRaises(RuntimeError): - with loop.wrap_cancelled(): - raise RuntimeError() + with self.with_loop() as loop, self.assertRaises(RuntimeError), loop.wrap_cancelled(): + raise RuntimeError() @make_async def test_next_cycle_doesnt_throw_when_not_cancelled(self): @@ -82,7 +77,7 @@ def test_next_cycle_doesnt_throw_when_not_cancelled(self): def test_from_thread_with_success(self) -> None: def test_func(): return self - + with self.with_loop() as loop: fut = loop.from_thread(test_func) yield @@ -92,7 +87,7 @@ def test_func(): def test_from_thread_with_failure(self) -> None: def test_func(): raise RuntimeError - + with self.with_loop() as loop: fut = loop.from_thread(test_func) yield @@ -102,6 +97,7 @@ def test_func(): def test_from_thread_forwards_correctly(self) -> None: a = None k = None + def test_func(*args, **kwargs): nonlocal a, k a = args @@ -111,7 +107,7 @@ def test_func(*args, **kwargs): fut = loop.from_thread(test_func, 1, 2, 3, a="b", c="d") yield fut.result(timeout=0.5) - self.assertEqual(a, (1,2,3)) + self.assertEqual(a, (1, 2, 3)) self.assertEqual(k, {"a": "b", "c": "d"}) @make_async @@ -123,20 +119,19 @@ def test_func(): t2 = yield from self.resolve_to_thread_future(loop.to_thread(test_func)) self.assertNotEqual(threading.current_thread(), t2) - @make_async def test_to_thread_runs_inline_with_failure(self) -> None: def test_func(): raise RuntimeError - - with self.with_loop() as loop: - with self.assertRaises(RuntimeError): - yield from self.resolve_to_thread_future(loop.to_thread(test_func)) + + with self.with_loop() as loop, self.assertRaises(RuntimeError): + yield from self.resolve_to_thread_future(loop.to_thread(test_func)) @make_async def test_to_thread_forwards_correctly(self) -> None: a = None k = None + def test_func(*args, **kwargs): nonlocal a, k a = args @@ -144,12 +139,11 @@ def test_func(*args, **kwargs): with self.with_loop() as loop: yield from self.resolve_to_thread_future(loop.to_thread(test_func, 1, 2, 3, a="b", c="d")) - self.assertEqual(a, (1,2,3)) + self.assertEqual(a, (1, 2, 3)) self.assertEqual(k, {"a": "b", "c": "d"}) class AsyncAdapterTest(AdapterTest): - def run_within_loop(self, func, args, kwargs): async def wrapped(_): result = func(self, *args, **kwargs) @@ -167,23 +161,23 @@ async def wait_for(self, coro, timeout): async def next_cycle(self): pass - + @is_async async def test_await_future_success(self): with self.with_loop() as loop: fut = Future() + def _setter(): fut.set_result(1) + threading.Thread(target=_setter).start() - self.assertEqual( - await self.wait_for(loop.await_future(fut), 0.5), - 1 - ) + self.assertEqual(await self.wait_for(loop.await_future(fut), 0.5), 1) @is_async async def test_await_future_failure(self): with self.with_loop() as loop: fut = Future() + def _setter(): fut.set_exception(RuntimeError()) @@ -192,16 +186,15 @@ def _setter(): await self.wait_for(loop.await_future(fut), 0.5) - class NoLoopTest(AdapterTest, unittest.TestCase): - def make_loop(self) -> EventLoop: return _NoEventLoop() def run_within_loop(self, func, args, kwargs): result = func(self, *args, **kwargs) if hasattr(result, "__iter__"): - for _ in result: pass + for _ in result: + pass @contextlib.contextmanager def assertCancelled(self): @@ -209,9 +202,10 @@ def assertCancelled(self): yield def resolve_to_thread_future(self, fut): - if False: yield + if False: + yield return fut.result(timeout=0.5) - + class AsyncIOTest(AsyncAdapterTest, unittest.TestCase): def make_loop(self) -> AsyncIOLoop: @@ -220,6 +214,7 @@ def make_loop(self) -> AsyncIOLoop: def run_within_loop_async(self, func, args, kwargs): async def wrapped(): await func(self, *args, **kwargs) + asyncio.run(wrapped()) async def next_cycle(self): @@ -246,6 +241,7 @@ def resolve_to_thread_future(self, fut): print("Skipping trio") else: from vsengine.adapters.trio import TrioEventLoop + class TrioTest(AsyncAdapterTest, unittest.TestCase): def make_loop(self) -> AsyncIOLoop: return TrioEventLoop(self.nursery) @@ -258,12 +254,14 @@ async def wrapped(): async with trio.open_nursery() as nursery: self.nursery = nursery await func(self, *args, **kwargs) + trio.run(wrapped) def resolve_to_thread_future(self, fut): done = False result = None error = None + async def _awaiter(): nonlocal done, error, result try: @@ -291,4 +289,3 @@ async def wait_for(self, coro, timeout): def assertCancelled(self): with self.assertRaises(trio.Cancelled): yield - diff --git a/tests/test_loops.py b/tests/test_loops.py index 3392c0e..7500869 100644 --- a/tests/test_loops.py +++ b/tests/test_loops.py @@ -4,19 +4,16 @@ # SPDX-License-Identifier: EUPL-1.2 import queue -import unittest import threading -from concurrent.futures import Future, CancelledError +import unittest +from concurrent.futures import CancelledError, Future import vapoursynth from vsengine._testutils import forcefully_unregister_policy +from vsengine.loops import Cancelled, EventLoop, _NoEventLoop, from_thread, get_loop, set_loop, to_thread from vsengine.policy import Policy, ThreadLocalStore -from vsengine.loops import _NoEventLoop, Cancelled, from_thread, get_loop, set_loop -from vsengine.loops import to_thread, from_thread -from vsengine.loops import EventLoop - class FailingEventLoop: def attach(self): @@ -30,6 +27,7 @@ def attach(self): def detach(self): pass + class SpinLoop(EventLoop): def __init__(self) -> None: self.queue = queue.Queue() @@ -60,17 +58,13 @@ def from_thread(self, func, *args, **kwargs): class NoLoopTest(unittest.TestCase): - - def test_wrap_cancelled_converts_the_exception(self) -> None: loop = _NoEventLoop() - with self.assertRaises(CancelledError): - with loop.wrap_cancelled(): - raise Cancelled + with self.assertRaises(CancelledError), loop.wrap_cancelled(): + raise Cancelled class LoopApiTest(unittest.TestCase): - def tearDown(self) -> None: forcefully_unregister_policy() @@ -102,11 +96,10 @@ def test(): return vapoursynth.get_current_environment() try: - with Policy(ThreadLocalStore()) as p: - with p.new_environment() as env1: - with env1.use(): - fut = from_thread(test) - self.assertEqual(fut.result(timeout=0.1), env1.vs_environment) + with Policy(ThreadLocalStore()) as p, p.new_environment() as env1: + with env1.use(): + fut = from_thread(test) + self.assertEqual(fut.result(timeout=0.1), env1.vs_environment) finally: loop.stop() thr.join() @@ -132,11 +125,10 @@ def test_loop_to_thread_retains_environment(self): def test(): return vapoursynth.get_current_environment() - with Policy(ThreadLocalStore()) as p: - with p.new_environment() as env1: - with env1.use(): - fut = to_thread(test) - self.assertEqual(fut.result(timeout=0.1), env1.vs_environment) + with Policy(ThreadLocalStore()) as p, p.new_environment() as env1: + with env1.use(): + fut = to_thread(test) + self.assertEqual(fut.result(timeout=0.1), env1.vs_environment) def test_loop_to_thread_does_not_require_environment(self): def test(): @@ -144,4 +136,3 @@ def test(): fut = to_thread(test) fut.result(timeout=0.1) - diff --git a/tests/test_policy.py b/tests/test_policy.py index e728352..00d4703 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -7,17 +7,14 @@ import vapoursynth from vsengine._testutils import forcefully_unregister_policy - -from vsengine.policy import GlobalStore -from vsengine.policy import Policy +from vsengine.policy import GlobalStore, Policy class PolicyTest(unittest.TestCase): - def setUp(self) -> None: forcefully_unregister_policy() self.policy = Policy(GlobalStore()) - + def tearDown(self) -> None: forcefully_unregister_policy() @@ -58,7 +55,6 @@ def test_context_manager_on_error(self): class ManagedEnvironmentTest(unittest.TestCase): - def setUp(self) -> None: forcefully_unregister_policy() self.store = GlobalStore() @@ -95,29 +91,26 @@ def test_environment_can_switch(self): env.dispose() def test_environment_can_capture_outputs(self): - with self.policy.new_environment() as env1: - with self.policy.new_environment() as env2: - with env1.use(): - vapoursynth.core.std.BlankClip().set_output(0) + with self.policy.new_environment() as env1, self.policy.new_environment() as env2: + with env1.use(): + vapoursynth.core.std.BlankClip().set_output(0) - self.assertEqual(len(env1.outputs), 1) - self.assertEqual(len(env2.outputs), 0) + self.assertEqual(len(env1.outputs), 1) + self.assertEqual(len(env2.outputs), 0) def test_environment_can_capture_cores(self): - with self.policy.new_environment() as env1: - with self.policy.new_environment() as env2: - self.assertNotEqual(env1.core, env2.core) + with self.policy.new_environment() as env1, self.policy.new_environment() as env2: + self.assertNotEqual(env1.core, env2.core) def test_inline_section_is_invisible(self): - with self.policy.new_environment() as env1: - with self.policy.new_environment() as env2: - env1.switch() - - env_before = self.store.get_current_environment() + with self.policy.new_environment() as env1, self.policy.new_environment() as env2: + env1.switch() - with env2.inline_section(): - self.assertNotEqual(vapoursynth.get_current_environment(), env1.vs_environment) - self.assertEqual(env_before, self.store.get_current_environment()) + env_before = self.store.get_current_environment() - self.assertEqual(vapoursynth.get_current_environment(), env1.vs_environment) + with env2.inline_section(): + self.assertNotEqual(vapoursynth.get_current_environment(), env1.vs_environment) self.assertEqual(env_before, self.store.get_current_environment()) + + self.assertEqual(vapoursynth.get_current_environment(), env1.vs_environment) + self.assertEqual(env_before, self.store.get_current_environment()) diff --git a/tests/test_policy_store.py b/tests/test_policy_store.py index 91b203a..729afd1 100644 --- a/tests/test_policy_store.py +++ b/tests/test_policy_store.py @@ -2,17 +2,14 @@ # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import unittest - import concurrent.futures as futures +import unittest from contextvars import copy_context -from vsengine.policy import GlobalStore, ThreadLocalStore, ContextVarStore -from vsengine.policy import EnvironmentStore +from vsengine.policy import ContextVarStore, EnvironmentStore, GlobalStore, ThreadLocalStore class BaseStoreTest: - def create_store(self) -> EnvironmentStore: raise NotImplementedError @@ -34,13 +31,11 @@ def test_basic_functionality(self): class TestGlobalStore(BaseStoreTest, unittest.TestCase): - def create_store(self) -> EnvironmentStore: return GlobalStore() class TestThreadLocalStore(BaseStoreTest, unittest.TestCase): - def create_store(self) -> EnvironmentStore: return ThreadLocalStore() @@ -57,7 +52,6 @@ def thread(): class TestContextVarStore(BaseStoreTest, unittest.TestCase): - def create_store(self) -> EnvironmentStore: return ContextVarStore("store_test") @@ -81,7 +75,7 @@ def context(p, n): ctx = copy_context() ctx.run(context, None, 1) self.assertEqual(self.store.get_current_environment(), None) - + self.store.set_current_environment(2) self.assertEqual(self.store.get_current_environment(), 2) ctx.run(context, 1, 3) diff --git a/tests/test_tests_pytest.py b/tests/test_tests_pytest.py index 6a7ed2b..e5ffdf5 100644 --- a/tests/test_tests_pytest.py +++ b/tests/test_tests_pytest.py @@ -1,9 +1,7 @@ import os +import subprocess import sys -import platform import unittest -import subprocess - DIR = os.path.dirname(__file__) PATH = os.path.join(DIR, "fixtures") @@ -16,13 +14,13 @@ def run_fixture(fixture: str, expect_status: int = 0): else: path += os.pathsep + os.path.abspath(os.path.join("..")) - env = {**os.environ, "PYTHONPATH" : path} + env = {**os.environ, "PYTHONPATH": path} process = subprocess.run( [sys.executable, "-m", "pytest", os.path.join(PATH, f"{fixture}.py"), "-o", "cache_dir=/build/.cache"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE, - env=env + env=env, ) if process.returncode != expect_status: print() @@ -32,7 +30,6 @@ def run_fixture(fixture: str, expect_status: int = 0): class TestUnittestWrapper(unittest.TestCase): - def test_core_in_module(self): run_fixture("pytest_core_in_module", 2) @@ -41,4 +38,3 @@ def test_stored_in_test(self): def test_succeeds(self): run_fixture("pytest_core_succeeds", 0) - diff --git a/tests/test_tests_unittest.py b/tests/test_tests_unittest.py index a7c0c35..b48f430 100644 --- a/tests/test_tests_unittest.py +++ b/tests/test_tests_unittest.py @@ -1,8 +1,7 @@ import os +import subprocess import sys import unittest -import subprocess - DIR = os.path.dirname(__file__) PATH = os.path.join(DIR, "fixtures") @@ -15,13 +14,13 @@ def run_fixture(fixture: str, expect_status: int = 0): else: path += os.pathsep + os.path.abspath(os.path.join("..")) - env = {**os.environ, "PYTHONPATH" : path} + env = {**os.environ, "PYTHONPATH": path} process = subprocess.run( [sys.executable, "-m", "vsengine.tests.unittest", fixture], stderr=subprocess.STDOUT, stdout=subprocess.PIPE, - env=env + env=env, ) if process.returncode != expect_status: print() @@ -31,7 +30,6 @@ def run_fixture(fixture: str, expect_status: int = 0): class TestUnittestWrapper(unittest.TestCase): - def test_core_in_module(self): run_fixture("unittest_core_in_module", 1) @@ -40,4 +38,3 @@ def test_stored_in_test(self): def test_succeeds(self): run_fixture("unittest_core_succeeds", 0) - diff --git a/tests/test_video.py b/tests/test_video.py index 08cef9d..fad36b9 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -2,16 +2,13 @@ # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -from typing import * import unittest +from typing import * -from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy - -from vapoursynth import core, PresetVideoFormat, VideoFormat, GRAY8, RGB24 -from vapoursynth import VideoNode, VideoFrame - -from vsengine.video import frame, planes, frames, render +from vapoursynth import GRAY8, RGB24, PresetVideoFormat, VideoFormat, VideoFrame, VideoNode, core +from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy +from vsengine.video import frame, frames, planes, render AnyFormat = Union[PresetVideoFormat, VideoFormat] diff --git a/tests/test_vpy.py b/tests/test_vpy.py index 306eb52..95cff36 100644 --- a/tests/test_vpy.py +++ b/tests/test_vpy.py @@ -3,23 +3,30 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import os import ast -import types -import unittest +import contextlib +import os import textwrap import threading -import contextlib +import types +import unittest import vapoursynth -from vsengine._testutils import forcefully_unregister_policy -from vsengine._testutils import BLACKBOARD, wrap_test_for_asyncio -from vsengine.policy import Policy, GlobalStore +from vsengine._testutils import BLACKBOARD, forcefully_unregister_policy, wrap_test_for_asyncio from vsengine.loops import NO_LOOP, set_loop -from vsengine.vpy import Script, script, code, variables, chdir_runner, _load -from vsengine.vpy import inline_runner, ExecutionFailed, WrapAllErrors - +from vsengine.policy import GlobalStore, Policy +from vsengine.vpy import ( + ExecutionFailed, + Script, + WrapAllErrors, + _load, + chdir_runner, + code, + inline_runner, + script, + variables, +) DIR = os.path.dirname(__file__) PATH = os.path.join(DIR, "fixtures", "test.vpy") @@ -30,18 +37,19 @@ def noop(): yield -class TestException(Exception): pass +class TestException(Exception): + pass def callback_script(func): def _script(ctx, module): with ctx: func(module) + return _script class ScriptTest(unittest.TestCase): - def setUp(self) -> None: forcefully_unregister_policy() @@ -51,15 +59,15 @@ def tearDown(self) -> None: def test_run_executes_successfully(self): run = False + @callback_script def test_code(_): nonlocal run run = True - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - script.run() + with Policy(GlobalStore()) as p, p.new_environment() as env: + script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + script.run() self.assertTrue(run) def test_run_wraps_exception(self): @@ -67,24 +75,23 @@ def test_run_wraps_exception(self): def test_code(_): raise TestException() - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - fut = script.run() - self.assertIsInstance(fut.exception(), ExecutionFailed) - self.assertIsInstance(fut.exception().parent_error, TestException) + with Policy(GlobalStore()) as p, p.new_environment() as env: + script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + fut = script.run() + self.assertIsInstance(fut.exception(), ExecutionFailed) + self.assertIsInstance(fut.exception().parent_error, TestException) def test_execute_resolves_immediately(self): run = False + @callback_script def test_code(_): nonlocal run run = True - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - script.result() + with Policy(GlobalStore()) as p, p.new_environment() as env: + script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + script.result() self.assertTrue(run) def test_execute_resolves_to_script(self): @@ -92,69 +99,68 @@ def test_execute_resolves_to_script(self): def test_code(_): pass - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - self.assertIs(script.result(), script) + with Policy(GlobalStore()) as p, p.new_environment() as env: + script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + self.assertIs(script.result(), script) def test_execute_resolves_immediately_when_raising(self): @callback_script def test_code(_): raise TestException - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - try: - script.result() - except ExecutionFailed as err: - self.assertIsInstance(err.parent_error, TestException) - except Exception as e: - self.fail(f"Wrong exception: {e!r}") - else: - self.fail("Test execution didn't fail properly.") + with Policy(GlobalStore()) as p, p.new_environment() as env: + script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + try: + script.result() + except ExecutionFailed as err: + self.assertIsInstance(err.parent_error, TestException) + except Exception as e: + self.fail(f"Wrong exception: {e!r}") + else: + self.fail("Test execution didn't fail properly.") @wrap_test_for_asyncio async def test_run_async(self): run = False + @callback_script def test_code(_): nonlocal run run = True - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - await script.run_async() + with Policy(GlobalStore()) as p, p.new_environment() as env: + script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + await script.run_async() self.assertTrue(run) @wrap_test_for_asyncio async def test_await_directly(self): run = False + @callback_script def test_code(_): nonlocal run run = True - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - await Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + with Policy(GlobalStore()) as p, p.new_environment() as env: + await Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) self.assertTrue(run) def test_cant_dispose_non_managed_environments(self): @callback_script def test_code(_): pass - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - with self.assertRaises(ValueError): - script.dispose() + + with Policy(GlobalStore()) as p, p.new_environment() as env: + script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + with self.assertRaises(ValueError): + script.dispose() def test_disposes_managed_environment(self): @callback_script def test_code(_): pass + with Policy(GlobalStore()) as p: env = p.new_environment() script = Script(test_code, types.ModuleType("__test__"), env, inline_runner) @@ -169,16 +175,17 @@ def test_noop_context_manager_for_non_managed_environments(self): @callback_script def test_code(_): pass - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) as s: - pass - self.assertFalse(env.disposed) + + with Policy(GlobalStore()) as p, p.new_environment() as env: + with Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) as s: + pass + self.assertFalse(env.disposed) def test_disposing_context_manager_for_managed_environments(self): @callback_script def test_code(_): pass + with Policy(GlobalStore()) as p: env = p.new_environment() with Script(test_code, types.ModuleType("__test__"), env, inline_runner): @@ -191,6 +198,7 @@ def test_code(_): def test_chdir_changes_chdir(self): curdir = None + @callback_script def test_code(_): nonlocal curdir @@ -204,6 +212,7 @@ def test_chdir_changes_chdir_back(self): @callback_script def test_code(_): pass + wrapped = chdir_runner(DIR, inline_runner) before = os.getcwd() @@ -212,19 +221,19 @@ def test_code(_): def test_load_uses_current_environment(self): vpy_env = None + @callback_script def test_code(_): nonlocal vpy_env vpy_env = vapoursynth.get_current_environment() - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - _load(test_code, None, inline=False, chdir=None).result() - self.assertEqual(vpy_env, env.vs_environment) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + _load(test_code, None, inline=False, chdir=None).result() + self.assertEqual(vpy_env, env.vs_environment) def test_load_creates_new_environment(self): vpy_env = None + @callback_script def test_code(_): nonlocal vpy_env @@ -282,44 +291,41 @@ def test_code_2(module): def test_load_runs_chdir(self): curdir = None + @callback_script def test_code(_): nonlocal curdir curdir = os.getcwd() - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - previous = os.getcwd() - _load(test_code, None, inline=True, chdir=DIR).result() - self.assertEqual(curdir, DIR) - self.assertEqual(os.getcwd(), previous) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + previous = os.getcwd() + _load(test_code, None, inline=True, chdir=DIR).result() + self.assertEqual(curdir, DIR) + self.assertEqual(os.getcwd(), previous) def test_load_runs_in_thread_when_requested(self): thread = None + @callback_script def test_code(_): nonlocal thread thread = threading.current_thread() - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - _load(test_code, None, inline=False, chdir=None).result() - self.assertIsNot(thread, threading.current_thread()) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + _load(test_code, None, inline=False, chdir=None).result() + self.assertIsNot(thread, threading.current_thread()) def test_load_runs_inline_by_default(self): thread = None + @callback_script def test_code(_): nonlocal thread thread = threading.current_thread() - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - _load(test_code, None, chdir=None).result() - self.assertIs(thread, threading.current_thread()) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + _load(test_code, None, chdir=None).result() + self.assertIs(thread, threading.current_thread()) def test_code_runs_string(self): CODE = textwrap.dedent(""" @@ -327,11 +333,9 @@ def test_code_runs_string(self): BLACKBOARD["vpy_test_runs_raw_code_str"] = True """) - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - code(CODE).result() - self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_str"), True) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + code(CODE).result() + self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_str"), True) def test_code_runs_bytes(self): CODE = textwrap.dedent(""" @@ -340,47 +344,39 @@ def test_code_runs_bytes(self): BLACKBOARD["vpy_test_runs_raw_code_bytes"] = True """).encode("latin-1") - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - code(CODE).result() - self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_bytes"), True) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + code(CODE).result() + self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_bytes"), True) def test_code_runs_ast(self): - CODE = ast.parse(textwrap.dedent(""" + CODE = ast.parse( + textwrap.dedent(""" from vsengine._testutils import BLACKBOARD BLACKBOARD["vpy_test_runs_raw_code_ast"] = True - """)) + """) + ) - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - code(CODE).result() - self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_ast"), True) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + code(CODE).result() + self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_ast"), True) def test_script_runs(self): BLACKBOARD.clear() - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - script(PATH).result() - self.assertEqual(BLACKBOARD.get("vpy_run_script"), True) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + script(PATH).result() + self.assertEqual(BLACKBOARD.get("vpy_run_script"), True) def test_script_runs_with_custom_name(self): BLACKBOARD.clear() - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - script(PATH, module_name="__test__").result() - self.assertEqual(BLACKBOARD.get("vpy_run_script_name"), "__test__") + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + script(PATH, module_name="__test__").result() + self.assertEqual(BLACKBOARD.get("vpy_run_script_name"), "__test__") def test_can_get_and_set_variables(self): - with Policy(GlobalStore()) as p: - with p.new_environment() as env: - with env.use(): - script = variables({"a": 1}) - script.result() - self.assertEqual(script.get_variable("a").result(), 1) + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + script = variables({"a": 1}) + script.result() + self.assertEqual(script.get_variable("a").result(), 1) def test_wrap_exceptions_wraps_exception(self): err = RuntimeError() From 5ddc2446c92140cc32692bd4d81e43b12a1462b3 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 6 Dec 2025 23:09:57 +0100 Subject: [PATCH 12/60] more typing schizo fixes --- vsengine/_futures.py | 2 +- vsengine/loops.py | 23 +++++++++++------------ vsengine/vpy.py | 15 +++++++++------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/vsengine/_futures.py b/vsengine/_futures.py index 97e7f6e..25f2b58 100644 --- a/vsengine/_futures.py +++ b/vsengine/_futures.py @@ -60,7 +60,7 @@ def add_done_callback(self, fn: Callable[[Future[T]], Any]) -> None: # The done_callback should inherit the environment of the current call. super().add_done_callback(keep_environment(fn)) - def add_loop_callback(self, func: Callable[[UnifiedFuture[T]], None]) -> None: + def add_loop_callback(self, func: Callable[[Future[T]], None]) -> None: def _wrapper(future: Future[T]) -> None: get_loop().from_thread(func, future) diff --git a/vsengine/loops.py b/vsengine/loops.py index 19926d0..3a016a9 100644 --- a/vsengine/loops.py +++ b/vsengine/loops.py @@ -6,7 +6,6 @@ from concurrent.futures import CancelledError, Future from contextlib import contextmanager from functools import wraps -from typing import Any import vapoursynth @@ -46,17 +45,17 @@ def detach(self) -> None: """ ... - def from_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: + def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ Ran from vapoursynth threads to move data to the event loop. """ raise NotImplementedError - def to_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: + def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ Run this function in a worker thread. """ - fut = Future[T]() + fut = Future[R]() def wrapper() -> None: if not fut.set_running_or_notify_cancel(): @@ -123,8 +122,8 @@ def detach(self) -> None: def next_cycle(self) -> Future[None]: return DONE - def from_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: - fut = Future[T]() + def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: + fut = Future[R]() try: result = func(*args, **kwargs) except BaseException as e: @@ -164,7 +163,7 @@ def set_loop(loop: EventLoop) -> None: raise -def keep_environment[T](func: Callable[..., T]) -> Callable[..., T]: +def keep_environment[**P, R](func: Callable[P, R]) -> Callable[P, R]: """ This decorator will return a function that keeps the environment that was active when the decorator was applied. @@ -178,14 +177,14 @@ def keep_environment[T](func: Callable[..., T]) -> Callable[..., T]: environment = _noop @wraps(func) - def _wrapper(*args: Any, **kwargs: Any) -> T: + def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R: with environment(): return func(*args, **kwargs) return _wrapper -def from_thread[T](func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: +def from_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ Runs a function inside the current event-loop, preserving the currently running vapoursynth environment (if any). @@ -199,13 +198,13 @@ def from_thread[T](func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[ """ @keep_environment - def _wrapper() -> Any: + def _wrapper() -> R: return func(*args, **kwargs) return get_loop().from_thread(_wrapper) -def to_thread[T](func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: +def to_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ Runs a function in a dedicated thread or worker, preserving the currently running vapoursynth environment (if any). @@ -217,7 +216,7 @@ def to_thread[T](func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T] """ @keep_environment - def _wrapper() -> T: + def _wrapper() -> R: return func(*args, **kwargs) return get_loop().to_thread(_wrapper) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index a9e8bc6..cac2f37 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -46,7 +46,7 @@ from concurrent.futures import Future from contextlib import AbstractContextManager from types import TracebackType -from typing import Any, Self +from typing import Any, Concatenate, Self from vapoursynth import Environment, get_current_environment @@ -54,8 +54,8 @@ from vsengine.loops import make_awaitable, to_thread from vsengine.policy import ManagedEnvironment, Policy -type Runner[T] = Callable[[Callable[[], T]], Future[T]] -Executor = Callable[[AbstractContextManager[None], types.ModuleType], None] +type Runner[R] = Callable[[Callable[[], R]], Future[R]] +type Executor = Callable[[AbstractContextManager[None], types.ModuleType], None] __all__ = ["ExecutionFailed", "code", "script", "variables"] @@ -96,11 +96,14 @@ def inline_runner[T](func: Callable[[], T]) -> Future[T]: return fut -def chdir_runner[T](dir: os.PathLike[str], parent: Runner[T]) -> Runner[T]: - def runner(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Future[T]: - def _wrapped() -> T: +def chdir_runner[**P, R]( + dir: os.PathLike[str], parent: Runner[R] +) -> Callable[Concatenate[Callable[P, R], P], Future[R]]: + def runner(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: + def _wrapped() -> R: current = os.getcwd() os.chdir(dir) + try: f = func(*args, **kwargs) return f From 6427161922408b6fc8a217ccfb938a7431d4a107 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 01:38:44 +0100 Subject: [PATCH 13/60] refactor(vpy): overhaul script loading api and types Split `Script` class into `Script` and `ManagedScript` to correctly handle environment lifecycles and enforce type safety. - Rename `script` to `load_file` and `code` to `load_code` - Remove `variables` function - Add type overloads for loader functions - Update imports and type aliases BREAKING CHANGE: Renamed `script` and `code` to `load_file` and `load_code`. Removed `variables`. --- vsengine/vpy.py | 215 +++++++++++++++++++++++++----------------------- 1 file changed, 113 insertions(+), 102 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index cac2f37..ab85c54 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -36,29 +36,30 @@ script. """ +from __future__ import annotations + import ast import os import runpy import textwrap import traceback import types -from collections.abc import Awaitable, Callable, Generator, Mapping +from collections.abc import Awaitable, Buffer, Callable, Generator from concurrent.futures import Future from contextlib import AbstractContextManager -from types import TracebackType -from typing import Any, Concatenate, Self +from types import NoneType, TracebackType +from typing import Any, Concatenate, overload from vapoursynth import Environment, get_current_environment -from vsengine._futures import UnifiedFuture, unified -from vsengine.loops import make_awaitable, to_thread -from vsengine.policy import ManagedEnvironment, Policy - -type Runner[R] = Callable[[Callable[[], R]], Future[R]] -type Executor = Callable[[AbstractContextManager[None], types.ModuleType], None] +from ._futures import UnifiedFuture, unified +from .loops import make_awaitable, to_thread +from .policy import ManagedEnvironment, Policy +__all__ = ["ExecutionFailed", "load_code", "load_file"] -__all__ = ["ExecutionFailed", "code", "script", "variables"] +type Runner[R] = Callable[[Callable[[], R]], Future[R]] +type Executor = Callable[[WrapAllErrors, types.ModuleType], None] class ExecutionFailed(Exception): # noqa: N818 @@ -97,7 +98,7 @@ def inline_runner[T](func: Callable[[], T]) -> Future[T]: def chdir_runner[**P, R]( - dir: os.PathLike[str], parent: Runner[R] + dir: str | os.PathLike[str], parent: Runner[R] ) -> Callable[Concatenate[Callable[P, R], P], Future[R]]: def runner(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: def _wrapped() -> R: @@ -118,35 +119,35 @@ def _wrapped() -> R: return runner -class Script(AbstractContextManager["Script"], Awaitable["Script"]): - environment: Environment | ManagedEnvironment +class AbstractScript[EnvironmentT: (Environment, ManagedEnvironment)](Awaitable[None]): + environment: EnvironmentT def __init__( self, - what: Executor, + executor: Executor, module: types.ModuleType, - environment: Environment | ManagedEnvironment, - runner: Runner[Self], + environment: EnvironmentT, + runner: Runner[None], ) -> None: - self.what = what + self.executor = executor self.environment = environment self.runner = runner self.module = module - self._future: Future[Self] | None = None - - def _run_inline(self) -> Self: - with self.environment.use(): - self.what(WrapAllErrors(), self.module) - return self + self._future: Future[None] | None = None - ### - # Public API + def __await__(self) -> Generator[Any, None, None]: + """ + Runs the script and waits until the script has completed. + """ + return self.run_async().__await__() - @unified(kind="future") - def get_variable(self, name: str, default: str | None = None) -> Future[str | None]: - return UnifiedFuture.resolve(getattr(self.module, name, default)) + async def run_async(self) -> None: + """ + Runs the script asynchronously, but it returns a coroutine. + """ + return await make_awaitable(self.run()) - def run(self) -> Future[Self]: + def run(self) -> Future[None]: """ Runs the script. @@ -157,51 +158,68 @@ def run(self) -> Future[Self]: self._future = self.runner(self._run_inline) return self._future - def result(self) -> Self: + def result(self) -> None: """ Runs the script and blocks until the script has finished running. """ - return self.run().result() + self.run().result() + + @unified(kind="future") + def get_variable(self, name: str, default: str | None = None) -> Future[str | None]: + return UnifiedFuture[str | None].resolve(getattr(self.module, name, default)) + + def _run_inline(self) -> None: + with self.environment.use(): + self.executor(WrapAllErrors(), self.module) + + +class Script(AbstractScript[Environment]): ... - def dispose(self) -> None: - """ - Disposes the managed environment. - """ - if not isinstance(self.environment, ManagedEnvironment): - raise ValueError("You can only scripts backed by managed environments") - self.environment.dispose() - def __enter__(self) -> Self: - return self +class ManagedScript(AbstractScript[ManagedEnvironment], AbstractContextManager[None]): + def __enter__(self) -> None: + return None def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: - if isinstance(self.environment, ManagedEnvironment): - self.dispose() + self.dispose() - async def run_async(self) -> Self: + def dispose(self) -> None: """ - Runs the script asynchronously, but it returns a coroutine. + Disposes the managed environment. """ - return await make_awaitable(self.run()) + self.environment.dispose() - def __await__(self) -> Generator[Any, None, Self]: - """ - Runs the script and waits until the script has completed. - """ - return self.run_async().__await__() +@overload +def load_file( + script: str | os.PathLike[str], + environment: Environment | Script | None = None, + *, + module_name: str = "__vapoursynth__", + inline: bool = True, + chdir: str | os.PathLike[str] | None = None, +) -> Script: ... -EnvironmentType = Environment | ManagedEnvironment | Policy | Script + +@overload +def load_file( + script: str | os.PathLike[str], + environment: Policy | ManagedEnvironment | ManagedScript, + *, + module_name: str = "__vapoursynth__", + inline: bool = True, + chdir: str | os.PathLike[str] | None = None, +) -> ManagedScript: ... -def script( - script: os.PathLike[str], - environment: EnvironmentType | None = None, +def load_file( + script: str | os.PathLike[str], + environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, *, module_name: str = "__vapoursynth__", inline: bool = True, - chdir: os.PathLike[str] | None = None, -) -> Script: + chdir: str | os.PathLike[str] | None = None, +) -> AbstractScript[Any]: """ Runs the script at the given path. @@ -218,55 +236,43 @@ def script( or await it. """ - def _execute(ctx: AbstractContextManager[None], module: types.ModuleType) -> None: + def _execute(ctx: WrapAllErrors, module: types.ModuleType) -> None: with ctx: runpy.run_path(str(script), module.__dict__, module.__name__) return _load(_execute, environment, module_name=module_name, inline=inline, chdir=chdir) -def variables( - variables: Mapping[str, str], - environment: EnvironmentType | None = None, +@overload +def load_code( + script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | types.CodeType, + environment: Environment | Script | None = None, *, module_name: str = "__vapoursynth__", inline: bool = True, - chdir: os.PathLike[str] | None = None, -) -> Script: - """ - Sets variables to the module. + chdir: str | os.PathLike[str] | None = None, +) -> Script: ... - :param path: If path is a path, the interpreter will run the file behind that path. - Otherwise it will execute it itself. - :param environment: Defines the environment in which the code should run. If passed - a Policy, it will create a new environment from the policy, which - can be acessed using the environment attribute. If the environment - is another Script, it will take the environment and module of the - script. - :param module_name: The name the module should get. Defaults to __vapoursynth__. - :param inline: Run the code inline, e.g. not in a separate thread. - :param chdir: Change the currently running directory while the script is running. - This is unsafe when running multiple scripts at once. - :returns: A script object. It script starts running when you call start() on it, - or await it. - """ - - def _execute(ctx: AbstractContextManager[None], module: types.ModuleType) -> None: - with ctx: - for k, v in variables.items(): - setattr(module, k, v) - return _load(_execute, environment, module_name=module_name, inline=inline, chdir=chdir) +@overload +def load_code( + script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | types.CodeType, + environment: Policy | ManagedEnvironment | ManagedScript, + *, + module_name: str = "__vapoursynth__", + inline: bool = True, + chdir: str | os.PathLike[str] | None = None, +) -> ManagedScript: ... -def code( - script: str | bytes | ast.Module | types.CodeType, - environment: EnvironmentType | None = None, +def load_code( + script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | types.CodeType, + environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, *, module_name: str = "__vapoursynth__", inline: bool = True, - chdir: os.PathLike[str] | None = None, -) -> Script: + chdir: str | os.PathLike[str] | None = None, +) -> AbstractScript[Any]: """ Runs the given code snippet. @@ -285,7 +291,7 @@ def code( or await it. """ - def _execute(ctx: AbstractContextManager[None], module: types.ModuleType) -> None: + def _execute(ctx: WrapAllErrors, module: types.ModuleType) -> None: nonlocal script with ctx: @@ -299,25 +305,30 @@ def _execute(ctx: AbstractContextManager[None], module: types.ModuleType) -> Non def _load( - script: Executor, - environment: EnvironmentType | None = None, + executor: Executor, + environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, *, module_name: str = "__vapoursynth__", inline: bool = True, - chdir: os.PathLike[str] | None = None, -) -> Script: + chdir: str | os.PathLike[str] | None = None, +) -> AbstractScript[Any]: runner = inline_runner if inline else to_thread module = environment.module if isinstance(environment, Script) else types.ModuleType(module_name) - if isinstance(environment, Script): - environment = environment.environment - elif isinstance(environment, Policy): - environment = environment.new_environment() - elif environment is None: - environment = get_current_environment() - if chdir is not None: runner = chdir_runner(chdir, runner) - return Script(script, module, environment, runner) + if isinstance(environment, AbstractScript): + environment = environment.environment + + if isinstance(environment, (Environment, NoneType)): + if environment is None: + environment = get_current_environment() + + return Script(executor, module, environment, runner) + + if isinstance(environment, Policy): + environment = environment.new_environment() + + return ManagedScript(executor, module, environment, runner) From 23be417a307a481bffd46833e505fe3b355bf345 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 02:28:33 +0100 Subject: [PATCH 14/60] refactor(vpy): generalize module parameter and improve typing Rename `module_name` to `module` in `load_file` and `load_code` to allow passing existing module objects. Add overloads to better handle return types based on the provided environment. Update docstrings to fix incorrect parameter references and typos. BREAKING CHANGE: The `module_name` parameter has been renamed to `module`. --- vsengine/vpy.py | 110 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 36 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index ab85c54..e657b83 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -43,11 +43,10 @@ import runpy import textwrap import traceback -import types from collections.abc import Awaitable, Buffer, Callable, Generator from concurrent.futures import Future from contextlib import AbstractContextManager -from types import NoneType, TracebackType +from types import CodeType, ModuleType, NoneType, TracebackType from typing import Any, Concatenate, overload from vapoursynth import Environment, get_current_environment @@ -59,7 +58,7 @@ __all__ = ["ExecutionFailed", "load_code", "load_file"] type Runner[R] = Callable[[Callable[[], R]], Future[R]] -type Executor = Callable[[WrapAllErrors, types.ModuleType], None] +type Executor = Callable[[WrapAllErrors, ModuleType], None] class ExecutionFailed(Exception): # noqa: N818 @@ -125,7 +124,7 @@ class AbstractScript[EnvironmentT: (Environment, ManagedEnvironment)](Awaitable[ def __init__( self, executor: Executor, - module: types.ModuleType, + module: ModuleType, environment: EnvironmentT, runner: Runner[None], ) -> None: @@ -193,9 +192,9 @@ def dispose(self) -> None: @overload def load_file( script: str | os.PathLike[str], - environment: Environment | Script | None = None, + environment: Environment | None = None, *, - module_name: str = "__vapoursynth__", + module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, ) -> Script: ... @@ -204,9 +203,29 @@ def load_file( @overload def load_file( script: str | os.PathLike[str], - environment: Policy | ManagedEnvironment | ManagedScript, + environment: Script, + *, + inline: bool = True, + chdir: str | os.PathLike[str] | None = None, +) -> Script: ... + + +@overload +def load_file( + script: str | os.PathLike[str], + environment: Policy | ManagedEnvironment, + *, + module: str | ModuleType = "__vapoursynth__", + inline: bool = True, + chdir: str | os.PathLike[str] | None = None, +) -> ManagedScript: ... + + +@overload +def load_file( + script: str | os.PathLike[str], + environment: ManagedScript, *, - module_name: str = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, ) -> ManagedScript: ... @@ -216,39 +235,38 @@ def load_file( script: str | os.PathLike[str], environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, *, - module_name: str = "__vapoursynth__", + module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, ) -> AbstractScript[Any]: """ Runs the script at the given path. - :param path: If path is a path, the interpreter will run the file behind that path. - Otherwise it will execute it itself. + :param script: The path to the script file to run. :param environment: Defines the environment in which the code should run. If passed a Policy, it will create a new environment from the policy, which - can be acessed using the environment attribute. - :param module_name: The name the module should get. Defaults to __vapoursynth__. + can be accessed using the environment attribute. + :param module: The name the module should get. Defaults to __vapoursynth__. :param inline: Run the code inline, e.g. not in a separate thread. :param chdir: Change the currently running directory while the script is running. This is unsafe when running multiple scripts at once. - :returns: A script object. It script starts running when you call start() on it, + :returns: A script object. The script starts running when you call run() on it, or await it. """ - def _execute(ctx: WrapAllErrors, module: types.ModuleType) -> None: + def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: with ctx: runpy.run_path(str(script), module.__dict__, module.__name__) - return _load(_execute, environment, module_name=module_name, inline=inline, chdir=chdir) + return _load(_execute, environment, module, inline, chdir) @overload def load_code( - script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | types.CodeType, - environment: Environment | Script | None = None, + script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, + environment: Environment | None = None, *, - module_name: str = "__vapoursynth__", + module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, ) -> Script: ... @@ -256,72 +274,92 @@ def load_code( @overload def load_code( - script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | types.CodeType, - environment: Policy | ManagedEnvironment | ManagedScript, + script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, + environment: Script, + *, + inline: bool = True, + chdir: str | os.PathLike[str] | None = None, +) -> Script: ... + + +@overload +def load_code( + script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, + environment: Policy | ManagedEnvironment, + *, + module: str | ModuleType = "__vapoursynth__", + inline: bool = True, + chdir: str | os.PathLike[str] | None = None, +) -> ManagedScript: ... + + +@overload +def load_code( + script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, + environment: ManagedScript, *, - module_name: str = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, ) -> ManagedScript: ... def load_code( - script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | types.CodeType, + script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, *, - module_name: str = "__vapoursynth__", + module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, ) -> AbstractScript[Any]: """ Runs the given code snippet. - :param path: If path is a path, the interpreter will run the file behind that path. - Otherwise it will execute it itself. + :param script: The code to run. Can be a string, bytes, AST, or compiled code. :param environment: Defines the environment in which the code should run. If passed a Policy, it will create a new environment from the policy, which - can be acessed using the environment attribute. If the environment + can be accessed using the environment attribute. If the environment is another Script, it will take the environment and module of the script. - :param module_name: The name the module should get. Defaults to __vapoursynth__. + :param module: The name the module should get. Defaults to __vapoursynth__. :param inline: Run the code inline, e.g. not in a separate thread. :param chdir: Change the currently running directory while the script is running. This is unsafe when running multiple scripts at once. - :returns: A script object. It script starts running when you call start() on it, + :returns: A script object. The script starts running when you call run() on it, or await it. """ - def _execute(ctx: WrapAllErrors, module: types.ModuleType) -> None: + def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: nonlocal script with ctx: - if isinstance(script, types.CodeType): + if isinstance(script, CodeType): code = script else: code = compile(script, filename="", dont_inherit=True, flags=0, mode="exec") exec(code, module.__dict__, module.__dict__) - return _load(_execute, environment, module_name=module_name, inline=inline, chdir=chdir) + return _load(_execute, environment, module, inline, chdir) def _load( executor: Executor, environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, - *, - module_name: str = "__vapoursynth__", + module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, ) -> AbstractScript[Any]: runner = inline_runner if inline else to_thread - module = environment.module if isinstance(environment, Script) else types.ModuleType(module_name) - if chdir is not None: runner = chdir_runner(chdir, runner) if isinstance(environment, AbstractScript): + module = environment.module environment = environment.environment + if isinstance(module, str): + module = ModuleType(module) + if isinstance(environment, (Environment, NoneType)): if environment is None: environment = get_current_environment() From 57f8ad5f29a43cd6ca64e7110fde23cd5e6c027c Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 03:44:27 +0100 Subject: [PATCH 15/60] add Jaded-Encoding-Thaumaturgy to copyright headers --- extra_tests/__init__.py | 1 + vsengine/__init__.py | 1 + vsengine/_futures.py | 1 + vsengine/_helpers.py | 1 + vsengine/_hospice.py | 1 + vsengine/_nodes.py | 1 + vsengine/_testutils.py | 1 + vsengine/convert.py | 1 + vsengine/loops.py | 1 + vsengine/policy.py | 1 + vsengine/video.py | 1 + vsengine/vpy.py | 1 + 12 files changed, 12 insertions(+) diff --git a/extra_tests/__init__.py b/extra_tests/__init__.py index dcb0bee..6b66964 100644 --- a/extra_tests/__init__.py +++ b/extra_tests/__init__.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 diff --git a/vsengine/__init__.py b/vsengine/__init__.py index 2c75348..4a1ca6d 100644 --- a/vsengine/__init__.py +++ b/vsengine/__init__.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 """ diff --git a/vsengine/_futures.py b/vsengine/_futures.py index 25f2b58..1076cbf 100644 --- a/vsengine/_futures.py +++ b/vsengine/_futures.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 from __future__ import annotations diff --git a/vsengine/_helpers.py b/vsengine/_helpers.py index 075d17e..bba51a5 100644 --- a/vsengine/_helpers.py +++ b/vsengine/_helpers.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 import contextlib diff --git a/vsengine/_hospice.py b/vsengine/_hospice.py index b773f63..d1d5908 100644 --- a/vsengine/_hospice.py +++ b/vsengine/_hospice.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 import gc diff --git a/vsengine/_nodes.py b/vsengine/_nodes.py index f358b1a..f24e141 100644 --- a/vsengine/_nodes.py +++ b/vsengine/_nodes.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 from collections.abc import Iterable, Iterator diff --git a/vsengine/_testutils.py b/vsengine/_testutils.py index 84a9047..959f4de 100644 --- a/vsengine/_testutils.py +++ b/vsengine/_testutils.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 """ diff --git a/vsengine/convert.py b/vsengine/convert.py index 586b9a0..975e57c 100644 --- a/vsengine/convert.py +++ b/vsengine/convert.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 diff --git a/vsengine/loops.py b/vsengine/loops.py index 3a016a9..0ade496 100644 --- a/vsengine/loops.py +++ b/vsengine/loops.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 from collections.abc import Awaitable, Callable, Iterator diff --git a/vsengine/policy.py b/vsengine/policy.py index 3cdae7e..1eca806 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 """ diff --git a/vsengine/video.py b/vsengine/video.py index dde5694..20a1d41 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 """ diff --git a/vsengine/vpy.py b/vsengine/vpy.py index e657b83..7ed7686 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 """ From 20762682055d2da82c508817d7d7e2d05b6a49ce Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 03:46:46 +0100 Subject: [PATCH 16/60] _nodes: fix typing --- vsengine/_nodes.py | 25 ++++++++++++------------- vsengine/video.py | 1 + 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/vsengine/_nodes.py b/vsengine/_nodes.py index f24e141..b81974d 100644 --- a/vsengine/_nodes.py +++ b/vsengine/_nodes.py @@ -5,15 +5,14 @@ # SPDX-License-Identifier: EUPL-1.2 from collections.abc import Iterable, Iterator from concurrent.futures import Future -from contextlib import AbstractContextManager from threading import RLock -from vapoursynth import core +from vapoursynth import RawFrame, core -def buffer_futures[T_co]( - futures: Iterable[Future[T_co]], prefetch: int = 0, backlog: int | None = None -) -> Iterator[Future[T_co]]: +def buffer_futures[FrameT: RawFrame]( + futures: Iterable[Future[FrameT]], prefetch: int = 0, backlog: int | None = None +) -> Iterator[Future[FrameT]]: if prefetch == 0: prefetch = core.num_threads if backlog is None: @@ -26,7 +25,7 @@ def buffer_futures[T_co]( finished = False running = 0 lock = RLock() - reorder = dict[int, Future[T_co]]() + reorder = dict[int, Future[FrameT]]() def _request_next() -> None: nonlocal finished, running @@ -45,7 +44,7 @@ def _request_next() -> None: reorder[idx] = fut fut.add_done_callback(_finished) - def _finished(f: Future[T_co]) -> None: + def _finished(f: Future[FrameT]) -> None: nonlocal finished, running with lock: running -= 1 @@ -89,11 +88,11 @@ def _refill() -> None: finished = True -def close_when_needed[T](future_iterable: Iterable[Future[AbstractContextManager[T]]]) -> Iterator[Future[T]]: - def copy_future_and_run_cb_before(fut: Future[AbstractContextManager[T]]) -> Future[T]: - f = Future[T]() +def close_when_needed[FrameT: RawFrame](future_iterable: Iterable[Future[FrameT]]) -> Iterator[Future[FrameT]]: + def copy_future_and_run_cb_before(fut: Future[FrameT]) -> Future[FrameT]: + f = Future[FrameT]() - def _as_completed(_: Future[AbstractContextManager[T]]) -> None: + def _as_completed(_: Future[FrameT]) -> None: try: r = fut.result() except Exception as e: @@ -105,8 +104,8 @@ def _as_completed(_: Future[AbstractContextManager[T]]) -> None: fut.add_done_callback(_as_completed) return f - def close_fut(f: Future[AbstractContextManager[T]]) -> None: - def _do_close(_: Future[AbstractContextManager[T]]) -> None: + def close_fut(f: Future[FrameT]) -> None: + def _do_close(_: Future[FrameT]) -> None: if f.exception() is None: f.result().__exit__(None, None, None) diff --git a/vsengine/video.py b/vsengine/video.py index 20a1d41..e783f6d 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -68,6 +68,7 @@ def frames( if close: it = close_when_needed(it) + return it From f6b3e89ce3db141a3c0c7dbe3e8ce0f2962a2cd6 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 18:24:30 +0100 Subject: [PATCH 17/60] ignore .vsjet folder --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3f7ce8f..678b3f6 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,6 @@ cython_debug/ result result-* !nix/lib/ + +# vsjet folder +.vsjet From d3b421299847365b65adbb0785c589c70164a01a Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 18:30:24 +0100 Subject: [PATCH 18/60] kill None --- vsengine/policy.py | 37 +++++++++++++------------------------ vsengine/vpy.py | 10 +++++++--- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/vsengine/policy.py b/vsengine/policy.py index 1eca806..96d4535 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -108,14 +108,11 @@ class GlobalStore(EnvironmentStore): _current: weakref.ReferenceType[EnvironmentData] | None __slots__ = ("_current",) - def __init__(self) -> None: - self._current = None - def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: self._current = environment def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: - return self._current + return getattr(self, "_current", None) class ThreadLocalStore(EnvironmentStore): @@ -159,17 +156,11 @@ class _ManagedPolicy(EnvironmentPolicy): This class directly interfaces with VapourSynth. """ - _api: EnvironmentPolicyAPI | None - _store: EnvironmentStore - _mutex: threading.Lock - _local: threading.local - __slots__ = ("_api", "_local", "_mutex", "_store") def __init__(self, store: EnvironmentStore) -> None: self._store = store self._mutex = threading.Lock() - self._api = None self._local = threading.local() # For engine-calls that require vapoursynth but @@ -181,20 +172,21 @@ def inline_section_start(self, environment: EnvironmentData) -> None: # End the section. def inline_section_end(self) -> None: - self._local.environment = None + del self._local.environment @property def api(self) -> EnvironmentPolicyAPI: - if self._api is None: - raise RuntimeError("Invalid state: No access to the current API") - return self._api + if hasattr(self, "_api"): + return self._api + + raise RuntimeError("Invalid state: No access to the current API") def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None: logger.debug("Successfully registered policy with VapourSynth.") self._api = special_api def on_policy_cleared(self) -> None: - self._api = None + del self._api logger.debug("Policy cleared.") def get_current_environment(self) -> EnvironmentData | None: @@ -250,9 +242,6 @@ def set_environment(self, environment: EnvironmentData | None) -> EnvironmentDat class ManagedEnvironment(contextlib.AbstractContextManager["ManagedEnvironment"]): - _environment: Environment - _data: EnvironmentData | None - _policy: Policy __slots__ = ("_data", "_environment", "_policy") def __init__(self, environment: Environment, data: EnvironmentData, policy: Policy) -> None: @@ -300,7 +289,7 @@ def inline_section(self) -> Iterator[None]: - Do not use __enter__ and __exit__ directly. - This function is not reentrant. """ - self._policy.managed.inline_section_start(self._data) # type: ignore + self._policy.managed.inline_section_start(self._data) try: yield finally: @@ -331,16 +320,16 @@ def dispose(self) -> None: return logger.debug(f"Disposing environment {self._data!r}.") - admit_environment(self._data, self.core) # type: ignore - self._policy.api.destroy_environment(self._data) # type: ignore - self._data = None + admit_environment(self._data, self.core) + self._policy.api.destroy_environment(self._data) + del self._data @property def disposed(self) -> bool: """ Checks if the environment is disposed """ - return self._data is None + return hasattr(self, "_data") def __enter__(self) -> Self: return self @@ -349,7 +338,7 @@ def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, t self.dispose() def __del__(self) -> None: - if self._data is None: + if self.disposed: return import warnings diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 7ed7686..28c5de0 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -122,6 +122,8 @@ def _wrapped() -> R: class AbstractScript[EnvironmentT: (Environment, ManagedEnvironment)](Awaitable[None]): environment: EnvironmentT + _future: Future[None] + def __init__( self, executor: Executor, @@ -133,7 +135,6 @@ def __init__( self.environment = environment self.runner = runner self.module = module - self._future: Future[None] | None = None def __await__(self) -> Generator[Any, None, None]: """ @@ -154,8 +155,11 @@ def run(self) -> Future[None]: It returns a future which completes when the script completes. When the script fails, it raises a ExecutionFailed. """ - if self._future is None: - self._future = self.runner(self._run_inline) + if hasattr(self, "_future"): + return self._future + + self._future = self.runner(self._run_inline) + return self._future def result(self) -> None: From 030cf861e5e29b37f4d708b86adc4b9ff762cad4 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 19:00:20 +0100 Subject: [PATCH 19/60] move test_utils --- {vsengine => tests}/_testutils.py | 0 tests/fixtures/test.vpy | 1 + tests/test_convert.py | 10 +- tests/test_futures.py | 2 +- tests/test_helpers.py | 104 ++--- tests/test_loops.py | 2 +- tests/test_policy.py | 2 +- tests/test_video.py | 2 +- tests/test_vpy.py | 659 +++++++++++++++--------------- 9 files changed, 401 insertions(+), 381 deletions(-) rename {vsengine => tests}/_testutils.py (100%) diff --git a/vsengine/_testutils.py b/tests/_testutils.py similarity index 100% rename from vsengine/_testutils.py rename to tests/_testutils.py diff --git a/tests/fixtures/test.vpy b/tests/fixtures/test.vpy index a0f8d10..5d87a2f 100644 --- a/tests/fixtures/test.vpy +++ b/tests/fixtures/test.vpy @@ -6,5 +6,6 @@ # SPDX-License-Identifier: EUPL-1.2 from vsengine._testutils import BLACKBOARD + BLACKBOARD["vpy_run_script"] = True BLACKBOARD["vpy_run_script_name"] = __name__ diff --git a/tests/test_convert.py b/tests/test_convert.py index b9cb12b..9d1ebcc 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -5,7 +5,7 @@ import vapoursynth as vs from vapoursynth import core -from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy +from tests._testutils import forcefully_unregister_policy, use_standalone_policy from vsengine.convert import to_rgb, yuv_heuristic DIR = os.path.dirname(__file__) @@ -35,7 +35,7 @@ def setUp(self) -> None: def tearDown(self) -> None: forcefully_unregister_policy() - def test_heuristics_provides_all_arguments(self): + def test_heuristics_provides_all_arguments(self) -> None: yuv = core.std.BlankClip(format=vs.YUV420P8) def _pseudo_scaler(c, **args): @@ -48,7 +48,7 @@ def _pseudo_scaler(c, **args): to_rgb(yuv, scaler=_pseudo_scaler) - def test_heuristics_with_examples(self): + def test_heuristics_with_examples(self) -> None: count_hits = 0 count_misses = 0 @@ -74,7 +74,7 @@ def test_heuristics_with_examples(self): self.assertGreaterEqual(count_hits, count_misses) - def test_converts_to_rgb24(self): + def test_converts_to_rgb24(self) -> None: # Should be sufficiently untagged. lel yuv8 = core.std.BlankClip(format=vs.YUV420P8) gray = core.std.BlankClip(format=vs.GRAY8) @@ -86,7 +86,7 @@ def test_converts_to_rgb24(self): self.assertEqual(int(to_rgb(clip).format), vs.RGB24) self.assertEqual(int(to_rgb(clip, bits_per_sample=16).format), vs.RGB48) - def test_supports_float(self): + def test_supports_float(self) -> None: # Test regression: Floating images cannot be shown. yuv_half = core.std.BlankClip(format=vs.YUV444PH) yuv_single = core.std.BlankClip(format=vs.YUV444PS) diff --git a/tests/test_futures.py b/tests/test_futures.py index fc8d5ac..0f469f2 100644 --- a/tests/test_futures.py +++ b/tests/test_futures.py @@ -8,8 +8,8 @@ import unittest from concurrent.futures import Future +from tests._testutils import wrap_test_for_asyncio from vsengine._futures import UnifiedFuture, UnifiedIterator, unified -from vsengine._testutils import wrap_test_for_asyncio from vsengine.loops import NO_LOOP, set_loop diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f029ed0..88a51a3 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,5 @@ -import unittest +from collections.abc import Iterator +import pytest import vapoursynth as vs from vapoursynth import core @@ -8,57 +9,68 @@ from vsengine.policy import GlobalStore, Policy -class TestUseInline(unittest.TestCase): - def setUp(self) -> None: - forcefully_unregister_policy() +@pytest.fixture(autouse=True) +def clean_policy() -> Iterator[None]: + forcefully_unregister_policy() + yield + forcefully_unregister_policy() - def tearDown(self) -> None: - forcefully_unregister_policy() - def test_with_standalone(self): +class TestUseInline: + def test_with_standalone(self) -> None: use_standalone_policy() with use_inline("test_with_standalone", None): pass - def test_with_set_environment(self): - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - with use_inline("test_with_set_environment", None): - pass - - def test_fails_without_an_environment(self): - with Policy(GlobalStore()), self.assertRaises(EnvironmentError): - with use_inline("test_fails_without_an_environment", None): - pass - - def test_accepts_a_managed_environment(self): - with Policy(GlobalStore()) as p, p.new_environment() as env: - with use_inline("test_accepts_a_managed_environment", env): - self.assertEqual(env.vs_environment, vs.get_current_environment()) - - def test_accepts_a_standard_environment(self): - with Policy(GlobalStore()) as p, p.new_environment() as env: - with use_inline("test_accepts_a_standard_environment", env.vs_environment): - self.assertEqual(env.vs_environment, vs.get_current_environment()) + def test_with_set_environment(self) -> None: + with ( + Policy(GlobalStore()) as p, + p.new_environment() as env, + env.use(), + use_inline("test_with_set_environment", None), + ): + pass + def test_fails_without_an_environment(self) -> None: + with ( + Policy(GlobalStore()), + pytest.raises(OSError), + use_inline("test_fails_without_an_environment", None), + ): + pass -class TestWrapVariable(unittest.TestCase): - def setUp(self) -> None: - forcefully_unregister_policy() + def test_accepts_a_managed_environment(self) -> None: + with ( + Policy(GlobalStore()) as p, + p.new_environment() as env, + use_inline("test_accepts_a_managed_environment", env), + ): + assert env.vs_environment == vs.get_current_environment() + + def test_accepts_a_standard_environment(self) -> None: + with ( + Policy(GlobalStore()) as p, + p.new_environment() as env, + use_inline("test_accepts_a_standard_environment", env.vs_environment), + ): + assert env.vs_environment == vs.get_current_environment() + + +class TestWrapVariable: + @pytest.fixture(autouse=True) + def setup_standalone(self) -> None: use_standalone_policy() - def tearDown(self) -> None: - forcefully_unregister_policy() - - def test_wrap_variable_bypasses_on_non_variable(self): + def test_wrap_variable_bypasses_on_non_variable(self) -> None: bc = core.std.BlankClip() def _wrapper(c): - self.assertIs(c, bc) + assert c is bc return c wrap_variable_size(bc, bc.format, _wrapper) - def test_wrap_caches_different_formats(self): + def test_wrap_caches_different_formats(self) -> None: bc24 = core.std.BlankClip(length=2) bc48 = core.std.BlankClip(format=vs.RGB48, length=2) sp = core.std.Splice([bc24, bc48, bc24, bc48], mismatch=True) @@ -72,37 +84,37 @@ def _wrapper(c): wrapped = wrap_variable_size(sp, vs.RGB24, _wrapper) for f in wrapped.frames(): - self.assertEqual(int(f.format), vs.RGB24) + assert int(f.format) == vs.RGB24 - self.assertEqual(counter, 2) - self.assertEqual(int(wrapped.format), vs.RGB24) + assert counter == 2 + assert int(wrapped.format) == vs.RGB24 - def test_wrap_caches_different_sizes(self): + def test_wrap_caches_different_sizes(self) -> None: bc1 = core.std.BlankClip(length=2, width=2, height=2) bc2 = core.std.BlankClip(length=2, width=4, height=4) sp = core.std.Splice([bc1, bc2, bc1, bc2], mismatch=True) counter = 0 - def _wrapper(c): + def _wrapper(c: vs.VideoNode) -> vs.VideoNode: nonlocal counter counter += 1 return c.resize.Point(format=vs.RGB24) wrapped = wrap_variable_size(sp, vs.RGB24, _wrapper) for f in wrapped.frames(): - self.assertEqual(int(f.format), vs.RGB24) - self.assertEqual(counter, 2) - self.assertEqual(int(wrapped.format), vs.RGB24) + assert int(f.format) == vs.RGB24 + assert counter == 2 + assert int(wrapped.format) == vs.RGB24 - def test_wrap_stops_caching_once_size_exceeded(self): + def test_wrap_stops_caching_once_size_exceeded(self) -> None: bcs = [core.std.BlankClip(length=1, width=x, height=x) for x in range(1, 102)] assert len(bcs) == 101 sp = core.std.Splice([*bcs, *bcs], mismatch=True) counter = 0 - def _wrapper(c): + def _wrapper(c: vs.VideoNode) -> vs.VideoNode: nonlocal counter counter += 1 return c.resize.Point(format=vs.RGB24) @@ -111,4 +123,4 @@ def _wrapper(c): for _ in wrapped.frames(): pass - self.assertGreaterEqual(counter, 101) + assert counter >= 101 diff --git a/tests/test_loops.py b/tests/test_loops.py index 7500869..aefcdef 100644 --- a/tests/test_loops.py +++ b/tests/test_loops.py @@ -10,7 +10,7 @@ import vapoursynth -from vsengine._testutils import forcefully_unregister_policy +from tests._testutils import forcefully_unregister_policy from vsengine.loops import Cancelled, EventLoop, _NoEventLoop, from_thread, get_loop, set_loop, to_thread from vsengine.policy import Policy, ThreadLocalStore diff --git a/tests/test_policy.py b/tests/test_policy.py index 00d4703..d30bc7c 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -6,7 +6,7 @@ import vapoursynth -from vsengine._testutils import forcefully_unregister_policy +from tests._testutils import forcefully_unregister_policy from vsengine.policy import GlobalStore, Policy diff --git a/tests/test_video.py b/tests/test_video.py index fad36b9..7f3d49e 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -7,7 +7,7 @@ from vapoursynth import GRAY8, RGB24, PresetVideoFormat, VideoFormat, VideoFrame, VideoNode, core -from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy +from tests._testutils import forcefully_unregister_policy, use_standalone_policy from vsengine.video import frame, frames, planes, render AnyFormat = Union[PresetVideoFormat, VideoFormat] diff --git a/tests/test_vpy.py b/tests/test_vpy.py index 95cff36..2ddd227 100644 --- a/tests/test_vpy.py +++ b/tests/test_vpy.py @@ -9,31 +9,33 @@ import textwrap import threading import types -import unittest +from collections.abc import Callable, Generator +from typing import Any +import pytest import vapoursynth -from vsengine._testutils import BLACKBOARD, forcefully_unregister_policy, wrap_test_for_asyncio +from tests._testutils import BLACKBOARD, forcefully_unregister_policy from vsengine.loops import NO_LOOP, set_loop from vsengine.policy import GlobalStore, Policy from vsengine.vpy import ( ExecutionFailed, + ManagedScript, Script, WrapAllErrors, _load, chdir_runner, - code, inline_runner, - script, - variables, + load_code, + load_file, ) -DIR = os.path.dirname(__file__) -PATH = os.path.join(DIR, "fixtures", "test.vpy") +DIR: str = os.path.dirname(__file__) +PATH: str = os.path.join(DIR, "fixtures", "test.vpy") @contextlib.contextmanager -def noop(): +def noop() -> Generator[None, None, None]: yield @@ -41,349 +43,354 @@ class TestException(Exception): pass -def callback_script(func): - def _script(ctx, module): +def callback_script(func: Callable[[types.ModuleType], None]) -> Callable[[Any, types.ModuleType], None]: + def _script(ctx: Any, module: types.ModuleType) -> None: with ctx: func(module) return _script -class ScriptTest(unittest.TestCase): - def setUp(self) -> None: - forcefully_unregister_policy() +@pytest.fixture(autouse=True) +def clean_policy() -> Generator[None, None, None]: + """Fixture to handle setup and teardown for policy and loops.""" + forcefully_unregister_policy() + yield + forcefully_unregister_policy() + set_loop(NO_LOOP) - def tearDown(self) -> None: - forcefully_unregister_policy() - set_loop(NO_LOOP) - def test_run_executes_successfully(self): - run = False +def test_run_executes_successfully() -> None: + run = False - @callback_script - def test_code(_): - nonlocal run - run = True + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal run + run = True - with Policy(GlobalStore()) as p, p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - script.run() - self.assertTrue(run) + with Policy(GlobalStore()) as p, p.new_environment() as env: + s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + s.run() - def test_run_wraps_exception(self): - @callback_script - def test_code(_): - raise TestException() + assert run - with Policy(GlobalStore()) as p, p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - fut = script.run() - self.assertIsInstance(fut.exception(), ExecutionFailed) - self.assertIsInstance(fut.exception().parent_error, TestException) - def test_execute_resolves_immediately(self): - run = False +def test_run_wraps_exception() -> None: + @callback_script + def test_code(_: types.ModuleType) -> None: + raise TestException() - @callback_script - def test_code(_): - nonlocal run - run = True + with Policy(GlobalStore()) as p, p.new_environment() as env: + s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + fut = s.run() - with Policy(GlobalStore()) as p, p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - script.result() - self.assertTrue(run) + exc = fut.exception() + assert isinstance(exc, ExecutionFailed) + assert isinstance(exc.parent_error, TestException) - def test_execute_resolves_to_script(self): - @callback_script - def test_code(_): - pass - with Policy(GlobalStore()) as p, p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - self.assertIs(script.result(), script) - - def test_execute_resolves_immediately_when_raising(self): - @callback_script - def test_code(_): - raise TestException - - with Policy(GlobalStore()) as p, p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - try: - script.result() - except ExecutionFailed as err: - self.assertIsInstance(err.parent_error, TestException) - except Exception as e: - self.fail(f"Wrong exception: {e!r}") - else: - self.fail("Test execution didn't fail properly.") - - @wrap_test_for_asyncio - async def test_run_async(self): - run = False - - @callback_script - def test_code(_): - nonlocal run - run = True - - with Policy(GlobalStore()) as p, p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - await script.run_async() - self.assertTrue(run) - - @wrap_test_for_asyncio - async def test_await_directly(self): - run = False - - @callback_script - def test_code(_): - nonlocal run - run = True - - with Policy(GlobalStore()) as p, p.new_environment() as env: - await Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - self.assertTrue(run) - - def test_cant_dispose_non_managed_environments(self): - @callback_script - def test_code(_): - pass +def test_execute_resolves_immediately() -> None: + run = False - with Policy(GlobalStore()) as p, p.new_environment() as env: - script = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) - with self.assertRaises(ValueError): - script.dispose() + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal run + run = True - def test_disposes_managed_environment(self): - @callback_script - def test_code(_): - pass + with Policy(GlobalStore()) as p, p.new_environment() as env: + s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + s.result() - with Policy(GlobalStore()) as p: - env = p.new_environment() - script = Script(test_code, types.ModuleType("__test__"), env, inline_runner) + assert run - try: - script.dispose() - except: - env.dispose() - raise - def test_noop_context_manager_for_non_managed_environments(self): - @callback_script - def test_code(_): - pass +def test_execute_resolves_to_script() -> None: + @callback_script + def test_code(_: types.ModuleType) -> None: + pass - with Policy(GlobalStore()) as p, p.new_environment() as env: - with Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) as s: - pass - self.assertFalse(env.disposed) + with Policy(GlobalStore()) as p, p.new_environment() as env: + s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + s.result() - def test_disposing_context_manager_for_managed_environments(self): - @callback_script - def test_code(_): - pass - with Policy(GlobalStore()) as p: - env = p.new_environment() - with Script(test_code, types.ModuleType("__test__"), env, inline_runner): - pass - try: - self.assertTrue(env.disposed) - except: - env.dispose() - raise - - def test_chdir_changes_chdir(self): - curdir = None - - @callback_script - def test_code(_): - nonlocal curdir - curdir = os.getcwd() - - wrapped = chdir_runner(DIR, inline_runner) - wrapped(test_code, noop(), 2) - self.assertEqual(curdir, DIR) - - def test_chdir_changes_chdir_back(self): - @callback_script - def test_code(_): - pass +def test_execute_resolves_immediately_when_raising() -> None: + @callback_script + def test_code(_: types.ModuleType) -> None: + raise TestException - wrapped = chdir_runner(DIR, inline_runner) - - before = os.getcwd() - wrapped(test_code, noop(), None) - self.assertEqual(os.getcwd(), before) - - def test_load_uses_current_environment(self): - vpy_env = None - - @callback_script - def test_code(_): - nonlocal vpy_env - vpy_env = vapoursynth.get_current_environment() - - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - _load(test_code, None, inline=False, chdir=None).result() - self.assertEqual(vpy_env, env.vs_environment) - - def test_load_creates_new_environment(self): - vpy_env = None - - @callback_script - def test_code(_): - nonlocal vpy_env - vpy_env = vapoursynth.get_current_environment() - - with Policy(GlobalStore()) as p: - script = _load(test_code, p, inline=True, chdir=None) - try: - script.result() - self.assertEqual(vpy_env, script.environment.vs_environment) - finally: - script.dispose() - - def test_load_chains_script(self): - @callback_script - def test_code_1(module): - self.assertFalse(hasattr(module, "test")) - module.test = True - - @callback_script - def test_code_2(module): - self.assertEqual(module.test, True) - - with Policy(GlobalStore()) as p: - script1 = _load(test_code_1, p, inline=True, chdir=None) - env = script1.environment - try: - script1.result() - script2 = _load(test_code_2, script1, inline=True, chdir=None) - script2.result() - finally: - env.dispose() - - def test_load_with_custom_name(self): - @callback_script - def test_code_1(module): - self.assertEqual(module.__name__, "__test_1__") - - @callback_script - def test_code_2(module): - self.assertEqual(module.__name__, "__test_2__") - - with Policy(GlobalStore()) as p: - try: - script1 = _load(test_code_1, p, module_name="__test_1__") - script1.result() - finally: - script1.dispose() - - try: - script2 = _load(test_code_2, p, module_name="__test_2__") - script2.result() - finally: - script2.dispose() - - def test_load_runs_chdir(self): - curdir = None - - @callback_script - def test_code(_): - nonlocal curdir - curdir = os.getcwd() - - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - previous = os.getcwd() - _load(test_code, None, inline=True, chdir=DIR).result() - self.assertEqual(curdir, DIR) - self.assertEqual(os.getcwd(), previous) - - def test_load_runs_in_thread_when_requested(self): - thread = None - - @callback_script - def test_code(_): - nonlocal thread - thread = threading.current_thread() - - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - _load(test_code, None, inline=False, chdir=None).result() - self.assertIsNot(thread, threading.current_thread()) - - def test_load_runs_inline_by_default(self): - thread = None - - @callback_script - def test_code(_): - nonlocal thread - thread = threading.current_thread() - - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - _load(test_code, None, chdir=None).result() - self.assertIs(thread, threading.current_thread()) - - def test_code_runs_string(self): - CODE = textwrap.dedent(""" - from vsengine._testutils import BLACKBOARD - BLACKBOARD["vpy_test_runs_raw_code_str"] = True - """) - - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - code(CODE).result() - self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_str"), True) - - def test_code_runs_bytes(self): - CODE = textwrap.dedent(""" - # encoding: latin-1 - from vsengine._testutils import BLACKBOARD - BLACKBOARD["vpy_test_runs_raw_code_bytes"] = True - """).encode("latin-1") - - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - code(CODE).result() - self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_bytes"), True) - - def test_code_runs_ast(self): - CODE = ast.parse( - textwrap.dedent(""" - from vsengine._testutils import BLACKBOARD - BLACKBOARD["vpy_test_runs_raw_code_ast"] = True - """) - ) - - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - code(CODE).result() - self.assertEqual(BLACKBOARD.get("vpy_test_runs_raw_code_ast"), True) - - def test_script_runs(self): - BLACKBOARD.clear() - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - script(PATH).result() - self.assertEqual(BLACKBOARD.get("vpy_run_script"), True) - - def test_script_runs_with_custom_name(self): - BLACKBOARD.clear() - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - script(PATH, module_name="__test__").result() - self.assertEqual(BLACKBOARD.get("vpy_run_script_name"), "__test__") - - def test_can_get_and_set_variables(self): - with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - script = variables({"a": 1}) - script.result() - self.assertEqual(script.get_variable("a").result(), 1) - - def test_wrap_exceptions_wraps_exception(self): - err = RuntimeError() + with Policy(GlobalStore()) as p, p.new_environment() as env: + s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) try: - with WrapAllErrors(): - raise err - except ExecutionFailed as e: - self.assertIs(e.parent_error, err) + s.result() + except ExecutionFailed as err: + assert isinstance(err.parent_error, TestException) + except Exception as e: + pytest.fail(f"Wrong exception: {e!r}") else: - self.fail("Wrap all errors swallowed the exception") + pytest.fail("Test execution didn't fail properly.") + + +@pytest.mark.asyncio +async def test_run_async() -> None: + run = False + + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal run + run = True + + with Policy(GlobalStore()) as p, p.new_environment() as env: + s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + await s.run_async() + + assert run + + +@pytest.mark.asyncio +async def test_await_directly() -> None: + run = False + + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal run + run = True + + with Policy(GlobalStore()) as p, p.new_environment() as env: + await Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) + + assert run + + +def test_disposes_managed_environment() -> None: + @callback_script + def test_code(_: types.ModuleType) -> None: + pass + + with Policy(GlobalStore()) as p: + env = p.new_environment() + s = ManagedScript(test_code, types.ModuleType("__test__"), env, inline_runner) + + try: + s.dispose() + except Exception: + env.dispose() + raise + + +def test_disposing_context_manager_for_managed_environments() -> None: + @callback_script + def test_code(_: types.ModuleType) -> None: + pass + + with Policy(GlobalStore()) as p: + env = p.new_environment() + with ManagedScript(test_code, types.ModuleType("__test__"), env, inline_runner): + pass + + try: + assert env.disposed + except Exception: + env.dispose() + raise + + +def test_chdir_changes_chdir() -> None: + curdir: str | None = None + + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal curdir + curdir = os.getcwd() + + wrapped = chdir_runner(DIR, inline_runner) + wrapped(test_code, noop(), 2) + assert curdir == DIR + + +def test_chdir_changes_chdir_back() -> None: + @callback_script + def test_code(_: types.ModuleType) -> None: + pass + + wrapped = chdir_runner(DIR, inline_runner) + + before = os.getcwd() + wrapped(test_code, noop(), None) + assert os.getcwd() == before + + +def test_load_uses_current_environment() -> None: + vpy_env: Any = None + + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal vpy_env + vpy_env = vapoursynth.get_current_environment() + + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + _load(test_code, None, inline=False, chdir=None).result() + assert vpy_env == env.vs_environment + + +def test_load_creates_new_environment() -> None: + vpy_env: Any = None + + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal vpy_env + vpy_env = vapoursynth.get_current_environment() + + with Policy(GlobalStore()) as p: + s = _load(test_code, p, inline=True, chdir=None) + try: + s.result() + assert vpy_env == s.environment.vs_environment + finally: + s.dispose() + + +def test_load_chains_script() -> None: + @callback_script + def test_code_1(module: types.ModuleType) -> None: + assert not hasattr(module, "test") + module.test = True # type: ignore[attr-defined] + + @callback_script + def test_code_2(module: types.ModuleType) -> None: + assert module.test is True + + with Policy(GlobalStore()) as p: + script1 = _load(test_code_1, p, inline=True, chdir=None) + env = script1.environment + try: + script1.result() + script2 = _load(test_code_2, script1, inline=True, chdir=None) + script2.result() + finally: + env.dispose() + + +def test_load_with_custom_name() -> None: + @callback_script + def test_code_1(module: types.ModuleType) -> None: + assert module.__name__ == "__test_1__" + + @callback_script + def test_code_2(module: types.ModuleType) -> None: + assert module.__name__ == "__test_2__" + + with Policy(GlobalStore()) as p: + try: + script1 = _load(test_code_1, p, module_name="__test_1__") + script1.result() + finally: + script1.dispose() + + try: + script2 = _load(test_code_2, p, module_name="__test_2__") + script2.result() + finally: + script2.dispose() + + +def test_load_runs_chdir() -> None: + curdir: str | None = None + + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal curdir + curdir = os.getcwd() + + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + previous = os.getcwd() + _load(test_code, None, inline=True, chdir=DIR).result() + assert curdir == DIR + assert os.getcwd() == previous + + +def test_load_runs_in_thread_when_requested() -> None: + thread: threading.Thread | None = None + + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal thread + thread = threading.current_thread() + + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + _load(test_code, None, inline=False, chdir=None).result() + assert thread is not threading.current_thread() + + +def test_load_runs_inline_by_default() -> None: + thread: threading.Thread | None = None + + @callback_script + def test_code(_: types.ModuleType) -> None: + nonlocal thread + thread = threading.current_thread() + + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + _load(test_code, None, chdir=None).result() + assert thread is threading.current_thread() + + +def test_code_runs_string() -> None: + CODE = textwrap.dedent(""" + from vsengine._testutils import BLACKBOARD + BLACKBOARD["vpy_test_runs_raw_code_str"] = True + """) + + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + load_code(CODE).result() + assert BLACKBOARD.get("vpy_test_runs_raw_code_str") is True + + +def test_code_runs_bytes() -> None: + CODE = textwrap.dedent(""" + # encoding: latin-1 + from vsengine._testutils import BLACKBOARD + BLACKBOARD["vpy_test_runs_raw_code_bytes"] = True + """).encode("latin-1") + + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + load_code(CODE).result() + assert BLACKBOARD.get("vpy_test_runs_raw_code_bytes") is True + + +def test_code_runs_ast() -> None: + CODE = ast.parse( + textwrap.dedent(""" + from vsengine._testutils import BLACKBOARD + BLACKBOARD["vpy_test_runs_raw_code_ast"] = True + """) + ) + + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + load_code(CODE).result() + assert BLACKBOARD.get("vpy_test_runs_raw_code_ast") is True + + +def test_script_runs() -> None: + BLACKBOARD.clear() + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + load_code(PATH).result() + assert BLACKBOARD.get("vpy_run_script") is True + + +def test_script_runs_with_custom_name() -> None: + BLACKBOARD.clear() + with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): + load_file(PATH, module="__test__").result() + assert BLACKBOARD.get("vpy_run_script_name") == "__test__" + + +def test_wrap_exceptions_wraps_exception() -> None: + err = RuntimeError() + try: + with WrapAllErrors(): + raise err + except ExecutionFailed as e: + assert e.parent_error is err + else: + pytest.fail("Wrap all errors swallowed the exception") From 58c8ffa889501f32922b7c1b9aa0d9c99154f462 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 19:01:10 +0100 Subject: [PATCH 20/60] delete convert.py & wrap_variable_size --- tests/test_helpers.py | 77 +------------------ vsengine/__init__.py | 4 +- vsengine/_helpers.py | 47 +----------- vsengine/convert.py | 168 ------------------------------------------ 4 files changed, 7 insertions(+), 289 deletions(-) delete mode 100644 vsengine/convert.py diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 88a51a3..3a797a6 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,11 +1,10 @@ from collections.abc import Iterator -import pytest +import pytest import vapoursynth as vs -from vapoursynth import core -from vsengine._helpers import use_inline, wrap_variable_size -from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy +from tests._testutils import forcefully_unregister_policy, use_standalone_policy +from vsengine._helpers import use_inline from vsengine.policy import GlobalStore, Policy @@ -54,73 +53,3 @@ def test_accepts_a_standard_environment(self) -> None: use_inline("test_accepts_a_standard_environment", env.vs_environment), ): assert env.vs_environment == vs.get_current_environment() - - -class TestWrapVariable: - @pytest.fixture(autouse=True) - def setup_standalone(self) -> None: - use_standalone_policy() - - def test_wrap_variable_bypasses_on_non_variable(self) -> None: - bc = core.std.BlankClip() - - def _wrapper(c): - assert c is bc - return c - - wrap_variable_size(bc, bc.format, _wrapper) - - def test_wrap_caches_different_formats(self) -> None: - bc24 = core.std.BlankClip(length=2) - bc48 = core.std.BlankClip(format=vs.RGB48, length=2) - sp = core.std.Splice([bc24, bc48, bc24, bc48], mismatch=True) - - counter = 0 - - def _wrapper(c): - nonlocal counter - counter += 1 - return c.resize.Point(format=vs.RGB24) - - wrapped = wrap_variable_size(sp, vs.RGB24, _wrapper) - for f in wrapped.frames(): - assert int(f.format) == vs.RGB24 - - assert counter == 2 - assert int(wrapped.format) == vs.RGB24 - - def test_wrap_caches_different_sizes(self) -> None: - bc1 = core.std.BlankClip(length=2, width=2, height=2) - bc2 = core.std.BlankClip(length=2, width=4, height=4) - sp = core.std.Splice([bc1, bc2, bc1, bc2], mismatch=True) - - counter = 0 - - def _wrapper(c: vs.VideoNode) -> vs.VideoNode: - nonlocal counter - counter += 1 - return c.resize.Point(format=vs.RGB24) - - wrapped = wrap_variable_size(sp, vs.RGB24, _wrapper) - for f in wrapped.frames(): - assert int(f.format) == vs.RGB24 - assert counter == 2 - assert int(wrapped.format) == vs.RGB24 - - def test_wrap_stops_caching_once_size_exceeded(self) -> None: - bcs = [core.std.BlankClip(length=1, width=x, height=x) for x in range(1, 102)] - assert len(bcs) == 101 - sp = core.std.Splice([*bcs, *bcs], mismatch=True) - - counter = 0 - - def _wrapper(c: vs.VideoNode) -> vs.VideoNode: - nonlocal counter - counter += 1 - return c.resize.Point(format=vs.RGB24) - - wrapped = wrap_variable_size(sp, vs.RGB24, _wrapper) - for _ in wrapped.frames(): - pass - - assert counter >= 101 diff --git a/vsengine/__init__.py b/vsengine/__init__.py index 4a1ca6d..ff37f2e 100644 --- a/vsengine/__init__.py +++ b/vsengine/__init__.py @@ -7,8 +7,8 @@ vsengine - A common set of function that bridge vapoursynth with your application. Parts: +- loops: Integrate vsengine with your event-loop (be it GUI-based or IO-based). +- policy: Create new isolated cores as needed. - video: Get frames or render the video. Sans-IO and memory safe. - vpy: Run .vpy-scripts in your application. -- policy: Create new isolated cores as needed. -- loops: Integrate vsengine with your event-loop (be it GUI-based or IO-based). """ diff --git a/vsengine/_helpers.py b/vsengine/_helpers.py index bba51a5..062bcb2 100644 --- a/vsengine/_helpers.py +++ b/vsengine/_helpers.py @@ -4,18 +4,16 @@ # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 import contextlib -from collections.abc import Callable, Iterator +from collections.abc import Iterator import vapoursynth as vs from vsengine.policy import ManagedEnvironment -EnvironmentTypes = vs.Environment | ManagedEnvironment - # Automatically set the environment within that block. @contextlib.contextmanager -def use_inline(function_name: str, env: EnvironmentTypes | None) -> Iterator[None]: +def use_inline(function_name: str, env: vs.Environment | ManagedEnvironment | None) -> Iterator[None]: if env is None: # Ensure there is actually an environment set in this block. try: @@ -34,44 +32,3 @@ def use_inline(function_name: str, env: EnvironmentTypes | None) -> Iterator[Non else: with env.use(): yield - - -# Variable size and format clips may require different handling depending on the actual frame size. -def wrap_variable_size( - node: vs.VideoNode, force_assumed_format: vs.VideoFormat, func: Callable[[vs.VideoNode], vs.VideoNode] -) -> vs.VideoNode: - # Check: This is not a variable format clip. - # Nothing needs to be done. - if node.format is not None and node.width != 0 and node.height != 0: - return func(node) - - def _do_resize(f: vs.VideoFrame) -> vs.VideoNode: - # Resize the node to make them assume a specific format. - # As the node should aready have this format, this should be a no-op. - return func(node.resize.Point(format=f.format, width=f.width, height=f.height)) - - _node_cache: dict[tuple[int, int, int], vs.VideoNode] | None = {} - - def _assume_format(n: int, f: vs.VideoFrame) -> vs.VideoNode: - nonlocal _node_cache - selector = (int(f.format), f.width, f.height) - - if _node_cache is None or len(_node_cache) > 100: - # Skip caching if the cahce grows too large. - _node_cache = None - wrapped = _do_resize(f) - - elif selector not in _node_cache: - # Resize and cache the node. - wrapped = _do_resize(f) - _node_cache[selector] = wrapped - - else: - # Use the cached node. - wrapped = _node_cache[selector] - - return wrapped - - # This clip must not become part of the closure, - # or otherwise we risk cyclic references. - return node.std.FrameEval(_assume_format, [node], [node]).resize.Point(format=force_assumed_format) diff --git a/vsengine/convert.py b/vsengine/convert.py deleted file mode 100644 index 975e57c..0000000 --- a/vsengine/convert.py +++ /dev/null @@ -1,168 +0,0 @@ -# vs-engine -# Copyright (C) 2022 cid-chan -# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy -# This project is licensed under the EUPL-1.2 -# SPDX-License-Identifier: EUPL-1.2 - -import functools -from collections.abc import Callable, Mapping -from typing import Any - -import vapoursynth as vs - -from vsengine._helpers import EnvironmentTypes, use_inline, wrap_variable_size - - -# The heuristics code for nodes. -# Usually the nodes are tagged so this heuristics code is not required. -@functools.lru_cache -def yuv_heuristic(width: int, height: int) -> Mapping[str, str]: - result = {} - - if width >= 3840: - result["matrix_in_s"] = "2020ncl" - elif width >= 1280: - result["matrix_in_s"] = "709" - elif height == 576: - result["matrix_in_s"] = "470bg" - else: - result["matrix_in_s"] = "170m" - - if width >= 3840: - result["transfer_in_s"] = "st2084" - elif width >= 1280: - result["transfer_in_s"] = "709" - elif height == 576: - result["transfer_in_s"] = "470bg" - else: - result["transfer_in_s"] = "601" - - if width >= 3840: - result["primaries_in_s"] = "2020" - elif width >= 1280: - result["primaries_in_s"] = "709" - elif height == 576: - result["primaries_in_s"] = "470bg" - else: - result["primaries_in_s"] = "170m" - - result["range_in_s"] = "limited" - - # ITU-T H.273 (07/2021), Note at the bottom of pg. 20 - if width >= 3840: - result["chromaloc_in_s"] = "top_left" - else: - result["chromaloc_in_s"] = "left" - - return result - - -# Move this function out of the closure to avoid capturing clip. -def _convert_yuv( - c: vs.VideoNode, - *, - core: vs.Core, - real_rgb24: vs.VideoFormat, - default_args: dict[str, Any], - scaler: str | Callable[..., vs.VideoNode], -) -> vs.VideoNode: - # We make yuv_heuristic not configurable so the heuristic - # will be shared across projects. - # - # In my opinion, this is a quirk that should be shared. - - args = {**yuv_heuristic(c.width, c.height), **default_args} - - if c.format.subsampling_w != 0 or c.format.subsampling_h != 0: - # To be clear, scaler should always be a string. - # Being able to provide a callable just makes testing args easier. - resizer = getattr(core.resize, scaler) if isinstance(scaler, str) else scaler - else: - # In this case we only do cs transforms, point resize is more then enough. - resizer = core.resize.Point - - # Keep bitdepth so we can dither futher down in the RGB part. - return resizer( - c, format=real_rgb24.replace(sample_type=c.format.sample_type, bits_per_sample=c.format.bits_per_sample), **args - ) - - -# Move this function out of the closure to avoid capturing clip. -def _actually_resize( - c: vs.VideoNode, *, core: vs.Core, convert_yuv: Callable[[vs.VideoNode], vs.VideoNode], target_rgb: vs.VideoFormat -) -> vs.VideoNode: - # Converting to YUV is a little bit more complicated, - # so I extracted it to its own function. - if c.format.color_family == vs.YUV: - c = convert_yuv(c) - - # Defaulting prefer_props to True makes resizing choke - # on GRAY clips. - if c.format == vs.GRAY: - c = c.std.RemoveFrameProps("_Matrix") - - # Actually perform the format conversion on a non-subsampled clip. - if ( - c.format.color_family != vs.RGB - or c.format.sample_type != vs.INTEGER - or c.format.bits_per_sample != target_rgb.bits_per_sample - ): - c = core.resize.Point(c, format=target_rgb) - - return c - - -def to_rgb( - clip: vs.VideoNode, - env: EnvironmentTypes | None = None, - *, - # Output: RGB bitdepth - bits_per_sample: int = 8, - # Input: YUV - scaler: str | Callable[..., vs.VideoNode] = "Bicubic", - default_matrix: str | None = None, - default_transfer: str | None = None, - default_primaries: str | None = None, - default_range: str | None = None, - default_chromaloc: str | None = None, -) -> vs.VideoNode: - """ - This function converts a clip to RGB. - - :param clip: The clip to convert to RGB - :param env: The environment the clip belongs to. (Optional if you don't use EnvironmentPolicies) - :param bits_per_sample: The bits per sample the resulting RGB clip should have. - :param scaler: The name scaler function in core.resize that should be used to convert YUV to RGB. - :param default_*: Manually override the defaults predicted by the heuristics. - :param yuv_heuristic: The heuristic function that takes the frame size and returns a set of yuv-metadata. - (For test purposes) - """ - - # This function does a lot. - # This is why there are so many comments. - - default_args = {} - if default_matrix is not None: - default_args["matrix_in_s"] = default_matrix - if default_transfer is not None: - default_args["transfer_in_s"] = default_transfer - if default_primaries is not None: - default_args["primaries_in_s"] = default_primaries - if default_range is not None: - default_args["range_in_s"] = default_range - if default_chromaloc is not None: - default_args["chromaloc_in_s"] = default_chromaloc - - with use_inline("to_rgb", env): - core = vs.core.core - real_rgb24 = core.get_video_format(vs.RGB24) - target_rgb = real_rgb24.replace(bits_per_sample=bits_per_sample) - - # This avoids capturing `clip` in a closure creating a self-reference. - convert_yuv = functools.partial( - _convert_yuv, core=core, real_rgb24=real_rgb24, default_args=default_args, scaler=scaler - ) - - actually_resize = functools.partial(_actually_resize, core=core, target_rgb=target_rgb, convert_yuv=convert_yuv) - - return wrap_variable_size(clip, force_assumed_format=target_rgb, func=actually_resize) From bf7f057a322d1b86d1f16533f001df35fb1e4ff1 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 19:01:56 +0100 Subject: [PATCH 21/60] improve readability --- vsengine/policy.py | 12 +++---- vsengine/video.py | 81 ++++++++++++++++++++++++---------------------- vsengine/vpy.py | 8 ++--- 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/vsengine/policy.py b/vsengine/policy.py index 96d4535..b431cc8 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -249,6 +249,12 @@ def __init__(self, environment: Environment, data: EnvironmentData, policy: Poli self._data = data self._policy = policy + def __enter__(self) -> Self: + return self + + def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: + self.dispose() + @property def vs_environment(self) -> Environment: """ @@ -331,12 +337,6 @@ def disposed(self) -> bool: """ return hasattr(self, "_data") - def __enter__(self) -> Self: - return self - - def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: - self.dispose() - def __del__(self) -> None: if self.disposed: return diff --git a/vsengine/video.py b/vsengine/video.py index e783f6d..fa3cdfa 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -10,30 +10,31 @@ from collections.abc import Iterator, Sequence from concurrent.futures import Future -import vapoursynth +import vapoursynth as vs from vsengine._futures import UnifiedFuture, unified -from vsengine._helpers import EnvironmentTypes, use_inline +from vsengine._helpers import use_inline from vsengine._nodes import buffer_futures, close_when_needed +from vsengine.policy import ManagedEnvironment @unified(kind="future") def frame( - node: vapoursynth.VideoNode, frameno: int, env: EnvironmentTypes | None = None -) -> Future[vapoursynth.VideoFrame]: + node: vs.VideoNode, frameno: int, env: vs.Environment | ManagedEnvironment | None = None +) -> Future[vs.VideoFrame]: with use_inline("frame", env): return node.get_frame_async(frameno) @unified(kind="future") def planes( - node: vapoursynth.VideoNode, + node: vs.VideoNode, frameno: int, - env: EnvironmentTypes | None = None, + env: vs.Environment | ManagedEnvironment | None = None, *, planes: Sequence[int] | None = None, ) -> Future[tuple[bytes, ...]]: - def _extract(frame: vapoursynth.VideoFrame) -> tuple[bytes, ...]: + def _extract(frame: vs.VideoFrame) -> tuple[bytes, ...]: try: # This might be a variable format clip. # extract the plane as late as possible. @@ -47,8 +48,8 @@ def _extract(frame: vapoursynth.VideoFrame) -> tuple[bytes, ...]: @unified(kind="generator") def frames( - node: vapoursynth.VideoNode, - env: EnvironmentTypes | None = None, + node: vs.VideoNode, + env: vs.Environment | ManagedEnvironment | None = None, *, prefetch: int = 0, backlog: int | None = None, @@ -56,7 +57,7 @@ def frames( # we don't have to care about backwards compatibility and # can just do the right thing from the beginning. close: bool = True, -) -> Iterator[Future[vapoursynth.VideoFrame]]: +) -> Iterator[Future[vs.VideoFrame]]: with use_inline("frames", env): length = len(node) @@ -74,8 +75,8 @@ def frames( @unified(kind="generator") def render( - node: vapoursynth.VideoNode, - env: EnvironmentTypes | None = None, + node: vs.VideoNode, + env: vs.Environment | ManagedEnvironment | None = None, *, prefetch: int = 0, backlog: int | None = 0, @@ -84,31 +85,32 @@ def render( frame_count = len(node) if y4m: - y4mformat = "" - if node.format.color_family == vapoursynth.GRAY: - y4mformat = "mono" - if node.format.bits_per_sample > 8: - y4mformat = y4mformat + str(node.format.bits_per_sample) - elif node.format.color_family == vapoursynth.YUV: - if node.format.subsampling_w == 1 and node.format.subsampling_h == 1: - y4mformat = "420" - elif node.format.subsampling_w == 1 and node.format.subsampling_h == 0: - y4mformat = "422" - elif node.format.subsampling_w == 0 and node.format.subsampling_h == 0: - y4mformat = "444" - elif node.format.subsampling_w == 2 and node.format.subsampling_h == 2: - y4mformat = "410" - elif node.format.subsampling_w == 2 and node.format.subsampling_h == 0: - y4mformat = "411" - elif node.format.subsampling_w == 0 and node.format.subsampling_h == 1: - y4mformat = "440" - if node.format.bits_per_sample > 8: - y4mformat = y4mformat + "p" + str(node.format.bits_per_sample) - else: - raise ValueError("Can only use GRAY and YUV for V4M-Streams") - - if len(y4mformat) > 0: - y4mformat = "C" + y4mformat + " " + match node.format.color_family: + case vs.GRAY: + y4mformat = "mono" + case vs.YUV: + match (node.format.subsampling_w, node.format.subsampling_h): + case (1, 1): + y4mformat = "420" + case (1, 0): + y4mformat = "422" + case (0, 0): + y4mformat = "444" + case (2, 2): + y4mformat = "410" + case (2, 0): + y4mformat = "411" + case (0, 1): + y4mformat = "440" + case _: + raise NotImplementedError + case _: + raise ValueError("Can only use GRAY and YUV for V4M-Streams") + + if node.format.bits_per_sample > 8: + y4mformat += f"p{node.format.bits_per_sample}" + + y4mformat = "C" + y4mformat + " " data = "YUV4MPEG2 {y4mformat}W{width} H{height} F{fps_num}:{fps_den} Ip A0:0 XLENGTH={length}\n".format( y4mformat=y4mformat, @@ -122,8 +124,9 @@ def render( current_frame = 0 - def render_single_frame(frame: vapoursynth.VideoFrame) -> tuple[int, bytes]: - buf = [] + def render_single_frame(frame: vs.VideoFrame) -> tuple[int, bytes]: + buf = list[bytes]() + if y4m: buf.append(b"FRAME\n") diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 28c5de0..d356a67 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -6,13 +6,13 @@ """ vsengine.vpy runs vpy-scripts for you. - >>> script("/path/to/my/script").result() - >>> code("print('Hello, World!')").result() + >>> load_file("/path/to/my/script").result() + >>> load_code("print('Hello, World!')").result() -script() and code() will create a Script-object which allows +load_file() and load_code() will create a Script-object which allows you to run the script and access its environment. -script() takes a path as the first argument while code() accepts +load_file() takes a path as the first argument while load_code() accepts code (either compiled, parsed or as a string/bytes) and returns the Script- object. From fd1ab109e19b62650065798ce20d66b4d08131b5 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 19:04:40 +0100 Subject: [PATCH 22/60] adapters: add jet copyright --- vsengine/adapters/__init__.py | 1 + vsengine/adapters/asyncio.py | 1 + vsengine/adapters/trio.py | 1 + 3 files changed, 3 insertions(+) diff --git a/vsengine/adapters/__init__.py b/vsengine/adapters/__init__.py index dcb0bee..6b66964 100644 --- a/vsengine/adapters/__init__.py +++ b/vsengine/adapters/__init__.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 diff --git a/vsengine/adapters/asyncio.py b/vsengine/adapters/asyncio.py index 96ca233..73f424c 100644 --- a/vsengine/adapters/asyncio.py +++ b/vsengine/adapters/asyncio.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 diff --git a/vsengine/adapters/trio.py b/vsengine/adapters/trio.py index 1abebc4..81eb357 100644 --- a/vsengine/adapters/trio.py +++ b/vsengine/adapters/trio.py @@ -1,5 +1,6 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 From 16c78081d7fd1e7ffa98ad7796955145e5f2deed Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 20:32:39 +0100 Subject: [PATCH 23/60] improve docstrings --- vsengine/loops.py | 119 +++++++++++++++++++++++++++++++-------------- vsengine/policy.py | 28 ++++++----- vsengine/video.py | 38 +++++++++++++++ vsengine/vpy.py | 2 - 4 files changed, 135 insertions(+), 52 deletions(-) diff --git a/vsengine/loops.py b/vsengine/loops.py index 0ade496..ffb1427 100644 --- a/vsengine/loops.py +++ b/vsengine/loops.py @@ -3,6 +3,9 @@ # Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 + +"""Integrate vsengine with your event-loop (be it GUI-based or IO-based).""" + from collections.abc import Awaitable, Callable, Iterator from concurrent.futures import CancelledError, Future from contextlib import contextmanager @@ -14,7 +17,7 @@ class Cancelled(Exception): # noqa: N818 - pass + """Exception raised when an operation has been cancelled.""" @contextmanager @@ -28,33 +31,54 @@ def _noop() -> Iterator[None]: class EventLoop: """ + Abstract base class for event loop integration. + These functions must be implemented to bridge VapourSynth - with the event-loop of your choice. + with the event-loop of your choice (e.g., asyncio, Qt). """ def attach(self) -> None: """ - Called when set_loop is run. + Initialize the event loop hooks. + + Called automatically when :func:`set_loop` is run. """ ... def detach(self) -> None: """ - Called when another event-loop should take over. + Clean up event loop hooks. - For example, when you restarting your application. + Called when another event-loop takes over, or when the application + is shutting down/restarting. """ ... def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ - Ran from vapoursynth threads to move data to the event loop. + Schedule a function to run on the event loop (usually the main thread). + + This is typically called from VapourSynth threads to move data or + logic back to the main application loop. + + :param func: The callable to execute. + :param args: Positional arguments for the callable. + :param kwargs: Keyword arguments for the callable. + :return: A Future representing the execution result. """ raise NotImplementedError def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ - Run this function in a worker thread. + Run a function in a separate worker thread. + + This is used to offload blocking operations from the main event loop. + The default implementation utilizes :class:`threading.Thread`. + + :param func: The callable to execute. + :param args: Positional arguments for the callable. + :param kwargs: Keyword arguments for the callable. + :return: A Future representing the execution result. """ fut = Future[R]() @@ -76,14 +100,16 @@ def wrapper() -> None: def next_cycle(self) -> Future[None]: """ - Passes control back to the event loop. + Pass control back to the event loop. - If there is no event-loop, the function will always return a resolved future. - If there is an event-loop, the function will never return a resolved future. + This allows the event loop to process pending events. - Throws vsengine.loops.Cancelled if the operation has been cancelled by that time. + * If there is **no** event-loop, the function returns an immediately resolved future. + * If there **is** an event-loop, the function returns a pending future that + resolves after the next cycle. - Only works in the main thread. + :raises vsengine.loops.Cancelled: If the operation has been cancelled. + :return: A Future that resolves when the cycle is complete. """ future = Future[None]() self.from_thread(future.set_result, None) @@ -91,17 +117,23 @@ def next_cycle(self) -> Future[None]: def await_future[T](self, future: Future[T]) -> Awaitable[T]: """ - Await a concurrent future. + Convert a concurrent Future into an Awaitable compatible with this loop. This function does not need to be implemented if the event-loop - does not support async and await. + does not support ``async`` and ``await`` syntax. + + :param future: The concurrent.futures.Future to await. + :return: An awaitable object. """ raise NotImplementedError @contextmanager def wrap_cancelled(self) -> Iterator[None]: """ - Wraps vsengine.loops.Cancelled into the native cancellation error. + Context manager to translate cancellation exceptions. + + Wraps :exc:`vsengine.loops.Cancelled` into the native cancellation + error of the specific event loop implementation (e.g., ``asyncio.CancelledError``). """ try: yield @@ -111,7 +143,10 @@ def wrap_cancelled(self) -> Iterator[None]: class _NoEventLoop(EventLoop): """ - This is the default event-loop used by + The default event-loop implementation. + + This is used when no specific loop is attached. It runs operations + synchronously/inline. """ def attach(self) -> None: @@ -140,22 +175,26 @@ def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.k def get_loop() -> EventLoop: """ - :return: The currently running loop. + Retrieve the currently active event loop. + + :return: The currently running EventLoop instance. """ return current_loop def set_loop(loop: EventLoop) -> None: """ - Sets the currently running loop. + Set the currently running event loop. - It will detach the previous loop first. If attaching fails, - it will revert to the NoLoop-implementation which runs everything inline + This function will detach the previous loop first. If attaching the new + loop fails, it reverts to the ``_NoEventLoop`` implementation which runs + everything inline. - :param loop: The event-loop instance that implements features. + :param loop: The EventLoop instance to attach. """ global current_loop current_loop.detach() + try: current_loop = loop loop.attach() @@ -166,11 +205,14 @@ def set_loop(loop: EventLoop) -> None: def keep_environment[**P, R](func: Callable[P, R]) -> Callable[P, R]: """ - This decorator will return a function that keeps the environment - that was active when the decorator was applied. + Decorate a function to preserve the VapourSynth environment. + + The returned function captures the VapourSynth environment active + at the moment the decorator is applied and restores it when the + function is executed. - :param func: A function to decorate. - :returns: A wrapped function that keeps the environment. + :param func: The function to decorate. + :return: A wrapped function that maintains the captured environment. """ try: environment = vapoursynth.get_current_environment().use @@ -187,15 +229,17 @@ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R: def from_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ - Runs a function inside the current event-loop, preserving the currently running - vapoursynth environment (if any). + Run a function inside the current event-loop. - .. note:: Be aware that the function might be called inline! + This preserves the currently running VapourSynth environment (if any). - :param func: A function to call inside the current event loop. + .. note:: + Depending on the loop implementation, the function might be called inline. + + :param func: The function to call inside the current event loop. :param args: The arguments for the function. :param kwargs: The keyword arguments to pass to the function. - :return: A future that resolves and reject depending on the outcome. + :return: A Future that resolves or rejects depending on the outcome. """ @keep_environment @@ -207,13 +251,14 @@ def _wrapper() -> R: def to_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ - Runs a function in a dedicated thread or worker, preserving the currently running - vapoursynth environment (if any). + Run a function in a dedicated thread or worker. + + This preserves the currently running VapourSynth environment (if any). - :param func: A function to call inside the current event loop. + :param func: The function to call in a worker thread. :param args: The arguments for the function. :param kwargs: The keyword arguments to pass to the function. - :return: An loop-specific object. + :return: A loop-specific Future object. """ @keep_environment @@ -225,9 +270,9 @@ def _wrapper() -> R: async def make_awaitable[T](future: Future[T]) -> T: """ - Makes a future awaitable. + Make a standard concurrent Future awaitable in the current loop. - :param future: The future to make awaitable. - :return: An object that can be awaited. + :param future: The future object to make awaitable. + :return: The result of the future, once awaited. """ return await get_loop().await_future(future) diff --git a/vsengine/policy.py b/vsengine/policy.py index b431cc8..39e1284 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -8,22 +8,20 @@ EnvironmentPolicies. -Here a quick run-down in how to use it, (but be sure to read on to select +Here is a quick run-down in how to use it, (but be sure to read on to select the best store-implementation for you): >>> import vapoursynth as vs - >>> policy = Policy(GlobalStore()) - >>> policy.register() - >>> with policy.new_environment() as env: - ... with env.use(): - ... vs.core.std.BlankClip().set_output() - ... print(env.outputs) - {"0": } - >>> policy.unregister() + >>> with Policy(GlobalStore()) as policy: + ... with policy.new_environment() as env: + ... with env.use(): + ... vs.core.std.BlankClip().set_output() + ... print(env.outputs) + {0: } To use it, you first have to pick an EnvironmentStore implementation. -A EnvironmentStore is just a simple object implementing the methods +An EnvironmentStore is just a simple object implementing the methods set_current_environment and get_current_environment. These actually implement the state an EnvironmentPolicy is responsible for managing. @@ -32,9 +30,9 @@ tailored for different uses and concurrency needs: - The GlobalStore is useful when you are ever only using one Environment - at the same time + at the same time. -- ThreadLocalStore is useful when you writing a multi-threaded applications, +- ThreadLocalStore is useful when you are writing multi-threaded applications, that can run multiple environments at once. This one behaves like vsscript. - ContextVarStore is useful when you are using event-loops like asyncio, @@ -242,6 +240,10 @@ def set_environment(self, environment: EnvironmentData | None) -> EnvironmentDat class ManagedEnvironment(contextlib.AbstractContextManager["ManagedEnvironment"]): + """ + Represents a VapourSynth environment that is managed by a policy. + """ + __slots__ = ("_data", "_environment", "_policy") def __init__(self, environment: Environment, data: EnvironmentData, policy: Policy) -> None: @@ -335,7 +337,7 @@ def disposed(self) -> bool: """ Checks if the environment is disposed """ - return hasattr(self, "_data") + return not hasattr(self, "_data") def __del__(self) -> None: if self.disposed: diff --git a/vsengine/video.py b/vsengine/video.py index fa3cdfa..466bf3e 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -22,6 +22,14 @@ def frame( node: vs.VideoNode, frameno: int, env: vs.Environment | ManagedEnvironment | None = None ) -> Future[vs.VideoFrame]: + """ + Request a specific frame from a node. + + :param node: The node to request the frame from. + :param frameno: The frame number to request. + :param env: The environment to use for the request. + :return: A future that resolves to the frame. + """ with use_inline("frame", env): return node.get_frame_async(frameno) @@ -34,6 +42,16 @@ def planes( *, planes: Sequence[int] | None = None, ) -> Future[tuple[bytes, ...]]: + """ + Request a specific frame from a node and return the planes as bytes. + + :param node: The node to request the frame from. + :param frameno: The frame number to request. + :param env: The environment to use for the request. + :param planes: The planes to return. If None, all planes are returned. + :return: A future that resolves to a tuple of bytes. + """ + def _extract(frame: vs.VideoFrame) -> tuple[bytes, ...]: try: # This might be a variable format clip. @@ -58,6 +76,16 @@ def frames( # can just do the right thing from the beginning. close: bool = True, ) -> Iterator[Future[vs.VideoFrame]]: + """ + Iterate over the frames of a node. + + :param node: The node to iterate over. + :param env: The environment to use for the request. + :param prefetch: The number of frames to prefetch. + :param backlog: The maximum number of frames to keep in the backlog. + :param close: Whether to close the frames automatically. + :return: An iterator of futures that resolve to the frames. + """ with use_inline("frames", env): length = len(node) @@ -82,6 +110,16 @@ def render( backlog: int | None = 0, y4m: bool = False, ) -> Iterator[Future[tuple[int, bytes]]]: + """ + Render a node to a stream of bytes. + + :param node: The node to render. + :param env: The environment to use for the request. + :param prefetch: The number of frames to prefetch. + :param backlog: The maximum number of frames to keep in the backlog. + :param y4m: Whether to output a Y4M header. + :return: An iterator of futures that resolve to a tuple of the frame number and the frame data. + """ frame_count = len(node) if y4m: diff --git a/vsengine/vpy.py b/vsengine/vpy.py index d356a67..a22b9c2 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -30,8 +30,6 @@ A Script object has the function run() which returns a future which will reject with ExecutionFailed or with resolve with None. -A convenience function called execute() which will block -until the script has run. A Script-instance is awaitable, in which it will await the completion of the script. From c9722c92428ebbd34627bfdedc1aa0e2f6d75a27 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 20:56:37 +0100 Subject: [PATCH 24/60] _hospice.py: lower logger level --- vsengine/_hospice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vsengine/_hospice.py b/vsengine/_hospice.py index d1d5908..2325ec6 100644 --- a/vsengine/_hospice.py +++ b/vsengine/_hospice.py @@ -38,7 +38,7 @@ def admit_environment(environment: EnvironmentData, core: Core) -> None: cores[ident] = core refnanny[ident] = ref - logger.info(f"Admitted environment {environment!r} and {core!r} as with ID:{ident}.") + logger.debug(f"Admitted environment {environment!r} and {core!r} as with ID:{ident}.") def any_alive() -> bool: @@ -72,7 +72,7 @@ def _is_core_still_used(ident: int) -> bool: def _add_tostage1(ident: int) -> None: - logger.info(f"Environment has died. Keeping core for a few gc-cycles. ID:{ident}") + logger.debug(f"Environment has died. Keeping core for a few gc-cycles. ID:{ident}") with lock: stage1.add(ident) @@ -106,7 +106,7 @@ def _collectstage2(phase: Literal["start", "stop"], _: dict[str, int]) -> None: stage2.remove(ident) garbage.append(cores.pop(ident)) - logger.info(f"Marking core {ident!r} for collection") + logger.debug(f"Marking core {ident!r} for collection") stage2.update(stage2_to_add) stage2_to_add = set() From 0007bc996b47cb7c2f568fbc417aa69abcc5895f Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 11 Dec 2025 20:57:36 +0100 Subject: [PATCH 25/60] cosmetics --- vsengine/loops.py | 4 ++-- vsengine/policy.py | 49 +++++++++++++++++++++++----------------------- vsengine/vpy.py | 20 +++++++++---------- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/vsengine/loops.py b/vsengine/loops.py index ffb1427..f772cf4 100644 --- a/vsengine/loops.py +++ b/vsengine/loops.py @@ -11,7 +11,7 @@ from contextlib import contextmanager from functools import wraps -import vapoursynth +import vapoursynth as vs __all__ = ["Cancelled", "EventLoop", "from_thread", "get_loop", "keep_environment", "set_loop", "to_thread"] @@ -215,7 +215,7 @@ def keep_environment[**P, R](func: Callable[P, R]) -> Callable[P, R]: :return: A wrapped function that maintains the captured environment. """ try: - environment = vapoursynth.get_current_environment().use + environment = vs.get_current_environment().use except RuntimeError: environment = _noop diff --git a/vsengine/policy.py b/vsengine/policy.py index 39e1284..250ad40 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -60,14 +60,15 @@ from __future__ import annotations -import contextlib -import contextvars -import logging import threading -import weakref +from abc import ABC, abstractmethod from collections.abc import Iterator, Mapping +from contextlib import AbstractContextManager, contextmanager +from contextvars import ContextVar +from logging import getLogger from types import TracebackType from typing import TYPE_CHECKING, Self +from weakref import ReferenceType, ref import vapoursynth as vs from vapoursynth import Environment, EnvironmentData, EnvironmentPolicy, EnvironmentPolicyAPI, register_policy @@ -77,25 +78,25 @@ __all__ = ["ContextVarStore", "GlobalStore", "ManagedEnvironment", "Policy", "ThreadLocalStore"] -logger = logging.getLogger(__name__) +logger = getLogger(__name__) -class EnvironmentStore: +class EnvironmentStore(ABC): """ Environment Stores manage which environment is currently active. """ - def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: + @abstractmethod + def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None: """ Set the current environment in the store. """ - ... - def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: + @abstractmethod + def get_current_environment(self) -> ReferenceType[EnvironmentData] | None: """ Retrieve the current environment from the store (if any) """ - ... class GlobalStore(EnvironmentStore): @@ -103,13 +104,13 @@ class GlobalStore(EnvironmentStore): This is the simplest store: It just stores the environment in a variable. """ - _current: weakref.ReferenceType[EnvironmentData] | None + _current: ReferenceType[EnvironmentData] | None __slots__ = ("_current",) - def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: + def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None: self._current = environment - def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: + def get_current_environment(self) -> ReferenceType[EnvironmentData] | None: return getattr(self, "_current", None) @@ -125,10 +126,10 @@ class ThreadLocalStore(EnvironmentStore): def __init__(self) -> None: self._current = threading.local() - def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: + def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None: self._current.environment = environment - def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: + def get_current_environment(self) -> ReferenceType[EnvironmentData] | None: return getattr(self._current, "environment", None) @@ -137,15 +138,15 @@ class ContextVarStore(EnvironmentStore): If you are using AsyncIO or similar frameworks, use this store. """ - _current: contextvars.ContextVar[weakref.ReferenceType[EnvironmentData] | None] + _current: ContextVar[ReferenceType[EnvironmentData] | None] def __init__(self, name: str = "vapoursynth") -> None: - self._current = contextvars.ContextVar(name) + self._current = ContextVar(name) - def set_current_environment(self, environment: weakref.ReferenceType[EnvironmentData] | None) -> None: + def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None: self._current.set(environment) - def get_current_environment(self) -> weakref.ReferenceType[EnvironmentData] | None: + def get_current_environment(self) -> ReferenceType[EnvironmentData] | None: return self._current.get(None) @@ -231,7 +232,7 @@ def set_environment(self, environment: EnvironmentData | None) -> EnvironmentDat if environment is None: self._store.set_current_environment(None) else: - self._store.set_current_environment(weakref.ref(environment)) + self._store.set_current_environment(ref(environment)) if previous_environment is not None: return previous_environment() @@ -239,7 +240,7 @@ def set_environment(self, environment: EnvironmentData | None) -> EnvironmentDat return None -class ManagedEnvironment(contextlib.AbstractContextManager["ManagedEnvironment"]): +class ManagedEnvironment(AbstractContextManager["ManagedEnvironment"]): """ Represents a VapourSynth environment that is managed by a policy. """ @@ -280,7 +281,7 @@ def outputs(self) -> Mapping[int, vs.VideoOutputTuple | vs.AudioNode]: with self.inline_section(): return vs.get_outputs() - @contextlib.contextmanager + @contextmanager def inline_section(self) -> Iterator[None]: """ Private API! @@ -303,7 +304,7 @@ def inline_section(self) -> Iterator[None]: finally: self._policy.managed.inline_section_end() - @contextlib.contextmanager + @contextmanager def use(self) -> Iterator[None]: """ Switches to this environment within a block. @@ -349,7 +350,7 @@ def __del__(self) -> None: self.dispose() -class Policy(contextlib.AbstractContextManager["Policy"]): +class Policy(AbstractContextManager["Policy"]): """ A managed policy is a very simple policy that just stores the environment data within the given store. diff --git a/vsengine/vpy.py b/vsengine/vpy.py index a22b9c2..d9b9758 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -48,7 +48,7 @@ from types import CodeType, ModuleType, NoneType, TracebackType from typing import Any, Concatenate, overload -from vapoursynth import Environment, get_current_environment +import vapoursynth as vs from ._futures import UnifiedFuture, unified from .loops import make_awaitable, to_thread @@ -117,7 +117,7 @@ def _wrapped() -> R: return runner -class AbstractScript[EnvironmentT: (Environment, ManagedEnvironment)](Awaitable[None]): +class AbstractScript[EnvironmentT: (vs.Environment, ManagedEnvironment)](Awaitable[None]): environment: EnvironmentT _future: Future[None] @@ -175,7 +175,7 @@ def _run_inline(self) -> None: self.executor(WrapAllErrors(), self.module) -class Script(AbstractScript[Environment]): ... +class Script(AbstractScript[vs.Environment]): ... class ManagedScript(AbstractScript[ManagedEnvironment], AbstractContextManager[None]): @@ -195,7 +195,7 @@ def dispose(self) -> None: @overload def load_file( script: str | os.PathLike[str], - environment: Environment | None = None, + environment: vs.Environment | None = None, *, module: str | ModuleType = "__vapoursynth__", inline: bool = True, @@ -236,7 +236,7 @@ def load_file( def load_file( script: str | os.PathLike[str], - environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, + environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, *, module: str | ModuleType = "__vapoursynth__", inline: bool = True, @@ -267,7 +267,7 @@ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: @overload def load_code( script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, - environment: Environment | None = None, + environment: vs.Environment | None = None, *, module: str | ModuleType = "__vapoursynth__", inline: bool = True, @@ -308,7 +308,7 @@ def load_code( def load_code( script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, - environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, + environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, *, module: str | ModuleType = "__vapoursynth__", inline: bool = True, @@ -346,7 +346,7 @@ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: def _load( executor: Executor, - environment: Policy | Environment | Script | ManagedEnvironment | ManagedScript | None = None, + environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, @@ -363,9 +363,9 @@ def _load( if isinstance(module, str): module = ModuleType(module) - if isinstance(environment, (Environment, NoneType)): + if isinstance(environment, (vs.Environment, NoneType)): if environment is None: - environment = get_current_environment() + environment = vs.get_current_environment() return Script(executor, module, environment, runner) From 5d0272fff1dfd46217c8579c4cbb44729fb4216c Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 13 Dec 2025 02:18:06 +0100 Subject: [PATCH 26/60] Policy: pass flags creation --- vsengine/policy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vsengine/policy.py b/vsengine/policy.py index 250ad40..0f55c31 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -361,8 +361,9 @@ class Policy(AbstractContextManager["Policy"]): _managed: _ManagedPolicy - def __init__(self, store: EnvironmentStore) -> None: + def __init__(self, store: EnvironmentStore, flags_creation: int = 0) -> None: self._managed = _ManagedPolicy(store) + self.flags_creation = flags_creation def register(self) -> None: """ @@ -393,7 +394,7 @@ def new_environment(self) -> ManagedEnvironment: For convenience, a managed environment will also serve as a context-manager that disposes the environment automatically. """ - data = self.api.create_environment() + data = self.api.create_environment(self.flags_creation) env = self.api.wrap_environment(data) logger.debug("Created new environment") return ManagedEnvironment(env, data, self) From 506a7d8d327f886b22fba1767f49cb25703963f8 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 13 Dec 2025 02:18:50 +0100 Subject: [PATCH 27/60] ManagedEnvironment.outputs: return correct type --- vsengine/policy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vsengine/policy.py b/vsengine/policy.py index 0f55c31..43b08d5 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -62,11 +62,11 @@ import threading from abc import ABC, abstractmethod -from collections.abc import Iterator, Mapping +from collections.abc import Iterator from contextlib import AbstractContextManager, contextmanager from contextvars import ContextVar from logging import getLogger -from types import TracebackType +from types import MappingProxyType, TracebackType from typing import TYPE_CHECKING, Self from weakref import ReferenceType, ref @@ -274,7 +274,7 @@ def core(self) -> vs.Core: return vs.core.core @property - def outputs(self) -> Mapping[int, vs.VideoOutputTuple | vs.AudioNode]: + def outputs(self) -> MappingProxyType[int, vs.VideoOutputTuple | vs.AudioNode]: """ Returns the output within this environment. """ From d2dd655d38c9b195ac6357ac8596ba9ff39533f8 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Sat, 13 Dec 2025 02:19:48 +0100 Subject: [PATCH 28/60] update logging messages --- vsengine/_hospice.py | 11 ++++++----- vsengine/policy.py | 25 ++++++++++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/vsengine/_hospice.py b/vsengine/_hospice.py index 2325ec6..e9c97be 100644 --- a/vsengine/_hospice.py +++ b/vsengine/_hospice.py @@ -38,7 +38,7 @@ def admit_environment(environment: EnvironmentData, core: Core) -> None: cores[ident] = core refnanny[ident] = ref - logger.debug(f"Admitted environment {environment!r} and {core!r} as with ID:{ident}.") + logger.debug("Admitted environment %r and %r as with ID:%s.", environment, core, ident) def any_alive() -> bool: @@ -72,7 +72,8 @@ def _is_core_still_used(ident: int) -> bool: def _add_tostage1(ident: int) -> None: - logger.debug(f"Environment has died. Keeping core for a few gc-cycles. ID:{ident}") + logger.debug("Environment has died. Keeping core for a few gc-cycles. ID:%s", ident) + with lock: stage1.add(ident) @@ -84,7 +85,7 @@ def _collectstage1(phase: Literal["start", "stop"], _: dict[str, int]) -> None: with lock: for ident in tuple(stage1): if _is_core_still_used(ident): - logger.warning(f"Core is still in use. ID:{ident}") + logger.warning("Core is still in use. ID:%s", ident) continue stage1.remove(ident) @@ -101,12 +102,12 @@ def _collectstage2(phase: Literal["start", "stop"], _: dict[str, int]) -> None: with lock: for ident in tuple(stage2): if _is_core_still_used(ident): - logger.warning(f"Core is still in use in stage 2. ID:{ident}") + logger.warning("Core is still in use in stage 2. ID:%s", ident) continue stage2.remove(ident) garbage.append(cores.pop(ident)) - logger.debug(f"Marking core {ident!r} for collection") + logger.debug("Marking core %r for collection", ident) stage2.update(stage2_to_add) stage2_to_add = set() diff --git a/vsengine/policy.py b/vsengine/policy.py index 43b08d5..a8d60b2 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -181,12 +181,12 @@ def api(self) -> EnvironmentPolicyAPI: raise RuntimeError("Invalid state: No access to the current API") def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None: - logger.debug("Successfully registered policy with VapourSynth.") self._api = special_api + logger.debug("Environment policy %r successfully registered with VapourSynth.", special_api) def on_policy_cleared(self) -> None: del self._api - logger.debug("Policy cleared.") + logger.debug("Environment policy successfully cleared.") def get_current_environment(self) -> EnvironmentData | None: # For small segments, allow switching the environment inline. @@ -203,7 +203,7 @@ def get_current_environment(self) -> EnvironmentData | None: return None if current_environment() is None: - logger.warning(f"Got dead environment: {current_environment()!r}") + logger.warning("Environment reference from store resolved to dead object: %r", current_environment) self._store.set_current_environment(None) return None @@ -213,7 +213,10 @@ def get_current_environment(self) -> EnvironmentData | None: assert received_environment if not self.is_alive(received_environment): - logger.warning(f"Got dead environment: {received_environment!r}") + logger.warning( + "Received environment object is not alive (Garbage collected?): %r", + received_environment, + ) # Remove the environment. self._store.set_current_environment(None) return None @@ -225,10 +228,10 @@ def set_environment(self, environment: EnvironmentData | None) -> EnvironmentDat previous_environment = self._store.get_current_environment() if environment is not None and not self.is_alive(environment): - logger.warning(f"Got dead environment: {environment!r}") + logger.warning("Attempted to set environment which is not alive: %r", environment) self._store.set_current_environment(None) else: - logger.debug(f"Setting environment: {environment!r}") + logger.debug("Environment successfully set to: %r", environment) if environment is None: self._store.set_current_environment(None) else: @@ -328,7 +331,8 @@ def dispose(self) -> None: if self.disposed: return - logger.debug(f"Disposing environment {self._data!r}.") + logger.debug("Starting disposal of environment: %r", self._data) + admit_environment(self._data, self.core) self._policy.api.destroy_environment(self._data) del self._data @@ -396,8 +400,11 @@ def new_environment(self) -> ManagedEnvironment: """ data = self.api.create_environment(self.flags_creation) env = self.api.wrap_environment(data) - logger.debug("Created new environment") - return ManagedEnvironment(env, data, self) + + try: + return ManagedEnvironment(env, data, self) + finally: + logger.debug("Successfully created new environment %r", data) @property def api(self) -> EnvironmentPolicyAPI: From b76a3df8e0d680450cb9d2a2ead57237b076943d Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Tue, 16 Dec 2025 03:01:57 +0100 Subject: [PATCH 29/60] futures: update UnifiedFuture context manager type hints --- vsengine/_futures.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/vsengine/_futures.py b/vsengine/_futures.py index 1076cbf..e258031 100644 --- a/vsengine/_futures.py +++ b/vsengine/_futures.py @@ -16,9 +16,7 @@ from vsengine.loops import Cancelled, get_loop, keep_environment -class UnifiedFuture[T]( - Future[T], AbstractContextManager[Any, Any], AbstractAsyncContextManager[Any, Any], Awaitable[T] -): +class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncContextManager[T, Any], Awaitable[T]): @classmethod def from_call[**P](cls, func: Callable[P, Future[T]], *args: P.args, **kwargs: P.kwargs) -> Self: try: @@ -103,7 +101,7 @@ def catch[V](self, cb: Callable[[BaseException], V]) -> UnifiedFuture[V]: return self.then(None, cb) # Nicer Syntax - def __enter__(self) -> Any: + def __enter__(self) -> T: obj = self.result() if isinstance(obj, AbstractContextManager): @@ -125,7 +123,7 @@ async def awaitable(self) -> T: def __await__(self) -> Generator[Any, None, T]: return self.awaitable().__await__() - async def __aenter__(self) -> Any: + async def __aenter__(self) -> T: result = await self.awaitable() if isinstance(result, AbstractAsyncContextManager): From 202691a57a7202d6248ca91d1024aceb4c5c97bb Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Fri, 19 Dec 2025 17:51:34 +0100 Subject: [PATCH 30/60] rename load_file to load_script --- vsengine/vpy.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index d9b9758..68b8078 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -6,13 +6,13 @@ """ vsengine.vpy runs vpy-scripts for you. - >>> load_file("/path/to/my/script").result() + >>> load_script("/path/to/my/script").result() >>> load_code("print('Hello, World!')").result() -load_file() and load_code() will create a Script-object which allows +load_script() and load_code() will create a Script-object which allows you to run the script and access its environment. -load_file() takes a path as the first argument while load_code() accepts +load_script() takes a path as the first argument while load_code() accepts code (either compiled, parsed or as a string/bytes) and returns the Script- object. @@ -54,7 +54,7 @@ from .loops import make_awaitable, to_thread from .policy import ManagedEnvironment, Policy -__all__ = ["ExecutionFailed", "load_code", "load_file"] +__all__ = ["ExecutionFailed", "load_code", "load_script"] type Runner[R] = Callable[[Callable[[], R]], Future[R]] type Executor = Callable[[WrapAllErrors, ModuleType], None] @@ -193,7 +193,7 @@ def dispose(self) -> None: @overload -def load_file( +def load_script( script: str | os.PathLike[str], environment: vs.Environment | None = None, *, @@ -204,7 +204,7 @@ def load_file( @overload -def load_file( +def load_script( script: str | os.PathLike[str], environment: Script, *, @@ -214,7 +214,7 @@ def load_file( @overload -def load_file( +def load_script( script: str | os.PathLike[str], environment: Policy | ManagedEnvironment, *, @@ -225,7 +225,7 @@ def load_file( @overload -def load_file( +def load_script( script: str | os.PathLike[str], environment: ManagedScript, *, @@ -234,7 +234,7 @@ def load_file( ) -> ManagedScript: ... -def load_file( +def load_script( script: str | os.PathLike[str], environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, *, From 30662fb7e1cd70bb83f32e51dda389b54d4cefe5 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Fri, 19 Dec 2025 17:53:06 +0100 Subject: [PATCH 31/60] return module globals on script completion --- vsengine/vpy.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 68b8078..03a74a8 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -57,7 +57,7 @@ __all__ = ["ExecutionFailed", "load_code", "load_script"] type Runner[R] = Callable[[Callable[[], R]], Future[R]] -type Executor = Callable[[WrapAllErrors, ModuleType], None] +type Executor[T] = Callable[[WrapAllErrors, ModuleType], T] class ExecutionFailed(Exception): # noqa: N818 @@ -117,36 +117,36 @@ def _wrapped() -> R: return runner -class AbstractScript[EnvironmentT: (vs.Environment, ManagedEnvironment)](Awaitable[None]): +class AbstractScript[EnvironmentT: (vs.Environment, ManagedEnvironment)](Awaitable[dict[str, Any]]): environment: EnvironmentT - _future: Future[None] + _future: Future[dict[str, Any]] def __init__( self, - executor: Executor, + executor: Executor[dict[str, Any]], module: ModuleType, environment: EnvironmentT, - runner: Runner[None], + runner: Runner[dict[str, Any]], ) -> None: self.executor = executor self.environment = environment self.runner = runner self.module = module - def __await__(self) -> Generator[Any, None, None]: + def __await__(self) -> Generator[Any, None, dict[str, Any]]: """ Runs the script and waits until the script has completed. """ return self.run_async().__await__() - async def run_async(self) -> None: + async def run_async(self) -> dict[str, Any]: """ Runs the script asynchronously, but it returns a coroutine. """ return await make_awaitable(self.run()) - def run(self) -> Future[None]: + def run(self) -> Future[dict[str, Any]]: """ Runs the script. @@ -170,9 +170,9 @@ def result(self) -> None: def get_variable(self, name: str, default: str | None = None) -> Future[str | None]: return UnifiedFuture[str | None].resolve(getattr(self.module, name, default)) - def _run_inline(self) -> None: + def _run_inline(self) -> dict[str, Any]: with self.environment.use(): - self.executor(WrapAllErrors(), self.module) + return self.executor(WrapAllErrors(), self.module) class Script(AbstractScript[vs.Environment]): ... @@ -257,9 +257,9 @@ def load_script( or await it. """ - def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: + def _execute(ctx: WrapAllErrors, module: ModuleType) -> dict[str, Any]: with ctx: - runpy.run_path(str(script), module.__dict__, module.__name__) + return runpy.run_path(str(script), module.__dict__, module.__name__) return _load(_execute, environment, module, inline, chdir) @@ -331,7 +331,7 @@ def load_code( or await it. """ - def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: + def _execute(ctx: WrapAllErrors, module: ModuleType) -> dict[str, Any]: nonlocal script with ctx: @@ -339,13 +339,16 @@ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: code = script else: code = compile(script, filename="", dont_inherit=True, flags=0, mode="exec") + exec(code, module.__dict__, module.__dict__) + return module.__dict__ + return _load(_execute, environment, module, inline, chdir) def _load( - executor: Executor, + executor: Executor[dict[str, Any]], environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, module: str | ModuleType = "__vapoursynth__", inline: bool = True, From 5937f13f7e44cd8636c7e3fe860fa4afb095dc57 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Fri, 19 Dec 2025 17:54:03 +0100 Subject: [PATCH 32/60] improve get_variable type hints with overloads --- vsengine/vpy.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 03a74a8..5647472 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -166,9 +166,15 @@ def result(self) -> None: """ self.run().result() + @overload @unified(kind="future") - def get_variable(self, name: str, default: str | None = None) -> Future[str | None]: - return UnifiedFuture[str | None].resolve(getattr(self.module, name, default)) + def get_variable(self, name: str, default: None = None) -> Future[Any | None]: ... + @overload + @unified(kind="future") + def get_variable[T](self, name: str, default: T) -> Future[Any | T]: ... + @unified(kind="future") + def get_variable(self, name: str, default: Any = None) -> Future[Any]: + return UnifiedFuture[Any].resolve(getattr(self.module, name, default)) def _run_inline(self) -> dict[str, Any]: with self.environment.use(): From 69c5a556ddb6d701abf1d8b0bf5476e8e9a4520e Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Fri, 19 Dec 2025 18:01:15 +0100 Subject: [PATCH 33/60] return self from ManagedScript.__enter__ --- vsengine/vpy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 5647472..3e264ae 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -46,7 +46,7 @@ from concurrent.futures import Future from contextlib import AbstractContextManager from types import CodeType, ModuleType, NoneType, TracebackType -from typing import Any, Concatenate, overload +from typing import Any, Concatenate, Self, overload import vapoursynth as vs @@ -184,9 +184,9 @@ def _run_inline(self) -> dict[str, Any]: class Script(AbstractScript[vs.Environment]): ... -class ManagedScript(AbstractScript[ManagedEnvironment], AbstractContextManager[None]): - def __enter__(self) -> None: - return None +class ManagedScript(AbstractScript[ManagedEnvironment], AbstractContextManager["ManagedScript"]): + def __enter__(self) -> Self: + return self def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: self.dispose() From f111a9601634fa4a8d5677ca553c2a92c9c21595 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Fri, 19 Dec 2025 18:07:04 +0100 Subject: [PATCH 34/60] add docstrings to script execution classes --- vsengine/vpy.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 3e264ae..da4502b 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -61,22 +61,41 @@ class ExecutionFailed(Exception): # noqa: N818 + """ + Exception raised when script execution fails. + """ + #: It contains the actual exception that has been raised. parent_error: BaseException def __init__(self, parent_error: BaseException) -> None: + """ + Initialize the ExecutionFailed exception. + + :param parent_error: The original exception that occurred. + """ msg = textwrap.indent(self.extract_traceback(parent_error), "| ") super().__init__(f"An exception was raised while running the script.\n{msg}") self.parent_error = parent_error @staticmethod def extract_traceback(error: BaseException) -> str: + """ + Extract and format the traceback from an exception. + + :param error: The exception to extract the traceback from. + :return: A formatted string containing the traceback. + """ msg = traceback.format_exception(type(error), error, error.__traceback__) msg = "".join(msg) return msg class WrapAllErrors(AbstractContextManager[None]): + """ + Context manager that wraps exceptions in ExecutionFailed. + """ + def __enter__(self) -> None: ... def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: @@ -85,6 +104,12 @@ def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, t def inline_runner[T](func: Callable[[], T]) -> Future[T]: + """ + Runs a function inline and returns the result as a Future. + + :param func: The function to run. + :return: A future containing the result or exception of the function. + """ fut = Future[T]() try: result = func() @@ -98,6 +123,14 @@ def inline_runner[T](func: Callable[[], T]) -> Future[T]: def chdir_runner[**P, R]( dir: str | os.PathLike[str], parent: Runner[R] ) -> Callable[Concatenate[Callable[P, R], P], Future[R]]: + """ + Wraps a runner to change the current working directory during execution. + + :param dir: The directory to change to. + :param parent: The runner to wrap. + :return: A wrapped runner function. + """ + def runner(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: def _wrapped() -> R: current = os.getcwd() @@ -118,6 +151,12 @@ def _wrapped() -> R: class AbstractScript[EnvironmentT: (vs.Environment, ManagedEnvironment)](Awaitable[dict[str, Any]]): + """ + Base class for VapourSynth script wrappers. + + Handles execution and variable retrieval for scripts. + """ + environment: EnvironmentT _future: Future[dict[str, Any]] @@ -174,6 +213,13 @@ def get_variable(self, name: str, default: None = None) -> Future[Any | None]: . def get_variable[T](self, name: str, default: T) -> Future[Any | T]: ... @unified(kind="future") def get_variable(self, name: str, default: Any = None) -> Future[Any]: + """ + Retrieve a variable from the script's module. + + :param name: The name of the variable to retrieve. + :param default: The default value if the variable is not found. + :return: A future that resolves to the variable's value. + """ return UnifiedFuture[Any].resolve(getattr(self.module, name, default)) def _run_inline(self) -> dict[str, Any]: @@ -181,10 +227,17 @@ def _run_inline(self) -> dict[str, Any]: return self.executor(WrapAllErrors(), self.module) -class Script(AbstractScript[vs.Environment]): ... +class Script(AbstractScript[vs.Environment]): + """ + A VapourSynth script wrapper for unmanaged environments. + """ class ManagedScript(AbstractScript[ManagedEnvironment], AbstractContextManager["ManagedScript"]): + """ + A VapourSynth script wrapper for managed environments. + """ + def __enter__(self) -> Self: return self From f4137a0c735bb2c7638542bcb728eab293ff9267 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Fri, 19 Dec 2025 18:17:13 +0100 Subject: [PATCH 35/60] fix typo --- vsengine/video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsengine/video.py b/vsengine/video.py index 466bf3e..e84efb6 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -143,7 +143,7 @@ def render( case _: raise NotImplementedError case _: - raise ValueError("Can only use GRAY and YUV for V4M-Streams") + raise ValueError("Can only use GRAY and YUV for Y4M-Streams") if node.format.bits_per_sample > 8: y4mformat += f"p{node.format.bits_per_sample}" From 1e9e71211c6645dd2e39b1b07ae43dfedf428997 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Tue, 23 Dec 2025 16:31:39 +0100 Subject: [PATCH 36/60] return execution result from blocking call --- vsengine/vpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index da4502b..60a8208 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -199,11 +199,11 @@ def run(self) -> Future[dict[str, Any]]: return self._future - def result(self) -> None: + def result(self) -> dict[str, Any]: """ Runs the script and blocks until the script has finished running. """ - self.run().result() + return self.run().result() @overload @unified(kind="future") From cd5261dca98d17001a3d683b5f9167a177023ad9 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Tue, 23 Dec 2025 16:53:11 +0100 Subject: [PATCH 37/60] remove return value from _run_inline also fix a memory leak oops --- vsengine/vpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 60a8208..0f0205d 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -222,9 +222,9 @@ def get_variable(self, name: str, default: Any = None) -> Future[Any]: """ return UnifiedFuture[Any].resolve(getattr(self.module, name, default)) - def _run_inline(self) -> dict[str, Any]: + def _run_inline(self) -> None: with self.environment.use(): - return self.executor(WrapAllErrors(), self.module) + self.executor(WrapAllErrors(), self.module) class Script(AbstractScript[vs.Environment]): From 8a3b2e5fb3170e2892eaf9f67d82bb4726c75d35 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Tue, 23 Dec 2025 16:53:42 +0100 Subject: [PATCH 38/60] make script execution return None --- vsengine/vpy.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 0f0205d..5576d4f 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -150,7 +150,7 @@ def _wrapped() -> R: return runner -class AbstractScript[EnvironmentT: (vs.Environment, ManagedEnvironment)](Awaitable[dict[str, Any]]): +class AbstractScript[EnvironmentT: (vs.Environment, ManagedEnvironment)](Awaitable[None]): """ Base class for VapourSynth script wrappers. @@ -159,33 +159,33 @@ class AbstractScript[EnvironmentT: (vs.Environment, ManagedEnvironment)](Awaitab environment: EnvironmentT - _future: Future[dict[str, Any]] + _future: Future[None] def __init__( self, - executor: Executor[dict[str, Any]], + executor: Executor[None], module: ModuleType, environment: EnvironmentT, - runner: Runner[dict[str, Any]], + runner: Runner[None], ) -> None: self.executor = executor self.environment = environment self.runner = runner self.module = module - def __await__(self) -> Generator[Any, None, dict[str, Any]]: + def __await__(self) -> Generator[Any, None, None]: """ Runs the script and waits until the script has completed. """ return self.run_async().__await__() - async def run_async(self) -> dict[str, Any]: + async def run_async(self) -> None: """ Runs the script asynchronously, but it returns a coroutine. """ return await make_awaitable(self.run()) - def run(self) -> Future[dict[str, Any]]: + def run(self) -> Future[None]: """ Runs the script. @@ -199,7 +199,7 @@ def run(self) -> Future[dict[str, Any]]: return self._future - def result(self) -> dict[str, Any]: + def result(self) -> None: """ Runs the script and blocks until the script has finished running. """ @@ -316,9 +316,9 @@ def load_script( or await it. """ - def _execute(ctx: WrapAllErrors, module: ModuleType) -> dict[str, Any]: + def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: with ctx: - return runpy.run_path(str(script), module.__dict__, module.__name__) + runpy.run_path(str(script), module.__dict__, module.__name__) return _load(_execute, environment, module, inline, chdir) @@ -390,7 +390,7 @@ def load_code( or await it. """ - def _execute(ctx: WrapAllErrors, module: ModuleType) -> dict[str, Any]: + def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: nonlocal script with ctx: @@ -401,13 +401,11 @@ def _execute(ctx: WrapAllErrors, module: ModuleType) -> dict[str, Any]: exec(code, module.__dict__, module.__dict__) - return module.__dict__ - return _load(_execute, environment, module, inline, chdir) def _load( - executor: Executor[dict[str, Any]], + executor: Executor[None], environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, module: str | ModuleType = "__vapoursynth__", inline: bool = True, From 0f83fe4aaf2721484ce0eb237f0ebba510e52750 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Wed, 24 Dec 2025 22:30:29 +0100 Subject: [PATCH 39/60] unify script classes and rename exception --- vsengine/vpy.py | 207 ++++++++++++++++++++++-------------------------- 1 file changed, 95 insertions(+), 112 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 5576d4f..68929e0 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -28,8 +28,7 @@ execution. A Script object has the function run() which returns a future which will -reject with ExecutionFailed or with resolve with None. - +reject with ExecutionError or with resolve with None. A Script-instance is awaitable, in which it will await the completion of the script. @@ -39,13 +38,12 @@ import ast import os -import runpy import textwrap import traceback from collections.abc import Awaitable, Buffer, Callable, Generator from concurrent.futures import Future from contextlib import AbstractContextManager -from types import CodeType, ModuleType, NoneType, TracebackType +from types import CodeType, ModuleType, TracebackType from typing import Any, Concatenate, Self, overload import vapoursynth as vs @@ -54,13 +52,13 @@ from .loops import make_awaitable, to_thread from .policy import ManagedEnvironment, Policy -__all__ = ["ExecutionFailed", "load_code", "load_script"] +__all__ = ["ExecutionError", "load_code", "load_script"] type Runner[R] = Callable[[Callable[[], R]], Future[R]] type Executor[T] = Callable[[WrapAllErrors, ModuleType], T] -class ExecutionFailed(Exception): # noqa: N818 +class ExecutionError(Exception): """ Exception raised when script execution fails. """ @@ -70,7 +68,7 @@ class ExecutionFailed(Exception): # noqa: N818 def __init__(self, parent_error: BaseException) -> None: """ - Initialize the ExecutionFailed exception. + Initialize the ExecutionError exception. :param parent_error: The original exception that occurred. """ @@ -93,14 +91,14 @@ def extract_traceback(error: BaseException) -> str: class WrapAllErrors(AbstractContextManager[None]): """ - Context manager that wraps exceptions in ExecutionFailed. + Context manager that wraps exceptions in ExecutionError. """ def __enter__(self) -> None: ... def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: if val is not None: - raise ExecutionFailed(val) from None + raise ExecutionError(val) from None def inline_runner[T](func: Callable[[], T]) -> Future[T]: @@ -139,8 +137,7 @@ def _wrapped() -> R: try: f = func(*args, **kwargs) return f - except Exception as e: - print(e) + except Exception: raise finally: os.chdir(current) @@ -150,48 +147,40 @@ def _wrapped() -> R: return runner -class AbstractScript[EnvironmentT: (vs.Environment, ManagedEnvironment)](Awaitable[None]): - """ - Base class for VapourSynth script wrappers. - - Handles execution and variable retrieval for scripts. - """ +_missing = object() - environment: EnvironmentT - _future: Future[None] +class Script[EnvT: (vs.Environment, ManagedEnvironment)](AbstractContextManager["Script[EnvT]"], Awaitable[None]): + """VapourSynth script wrapper.""" - def __init__( - self, - executor: Executor[None], - module: ModuleType, - environment: EnvironmentT, - runner: Runner[None], - ) -> None: + def __init__(self, executor: Executor[None], module: ModuleType, environment: EnvT, runner: Runner[None]) -> None: self.executor = executor - self.environment = environment + self.environment: EnvT = environment self.runner = runner self.module = module + def __enter__(self) -> Self: + self.result() + return self + + def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: + self.dispose() + def __await__(self) -> Generator[Any, None, None]: """ Runs the script and waits until the script has completed. """ return self.run_async().__await__() - async def run_async(self) -> None: - """ - Runs the script asynchronously, but it returns a coroutine. - """ - return await make_awaitable(self.run()) - def run(self) -> Future[None]: """ Runs the script. It returns a future which completes when the script completes. - When the script fails, it raises a ExecutionFailed. + When the script fails, it raises a ExecutionError. """ + self._future: Future[None] + if hasattr(self, "_future"): return self._future @@ -199,20 +188,33 @@ def run(self) -> Future[None]: return self._future + async def run_async(self) -> None: + """ + Runs the script asynchronously, but it returns a coroutine. + """ + return await make_awaitable(self.run()) + def result(self) -> None: """ Runs the script and blocks until the script has finished running. """ return self.run().result() + def dispose(self) -> None: + """Disposes the managed environment.""" + self.module.__dict__.clear() + + if isinstance(self.environment, ManagedEnvironment): + self.environment.dispose() + @overload @unified(kind="future") - def get_variable(self, name: str, default: None = None) -> Future[Any | None]: ... + def get_variable(self, name: str) -> Future[Any]: ... @overload @unified(kind="future") def get_variable[T](self, name: str, default: T) -> Future[Any | T]: ... @unified(kind="future") - def get_variable(self, name: str, default: Any = None) -> Future[Any]: + def get_variable(self, name: str, default: Any = _missing) -> Future[Any]: """ Retrieve a variable from the script's module. @@ -220,37 +222,15 @@ def get_variable(self, name: str, default: Any = None) -> Future[Any]: :param default: The default value if the variable is not found. :return: A future that resolves to the variable's value. """ - return UnifiedFuture[Any].resolve(getattr(self.module, name, default)) + return UnifiedFuture[Any].resolve( + getattr(self.module, name) if default is _missing else getattr(self.module, name, default) + ) def _run_inline(self) -> None: with self.environment.use(): self.executor(WrapAllErrors(), self.module) -class Script(AbstractScript[vs.Environment]): - """ - A VapourSynth script wrapper for unmanaged environments. - """ - - -class ManagedScript(AbstractScript[ManagedEnvironment], AbstractContextManager["ManagedScript"]): - """ - A VapourSynth script wrapper for managed environments. - """ - - def __enter__(self) -> Self: - return self - - def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None: - self.dispose() - - def dispose(self) -> None: - """ - Disposes the managed environment. - """ - self.environment.dispose() - - @overload def load_script( script: str | os.PathLike[str], @@ -259,17 +239,17 @@ def load_script( module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> Script: ... +) -> Script[vs.Environment]: ... @overload def load_script( script: str | os.PathLike[str], - environment: Script, + environment: Script[vs.Environment], *, inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> Script: ... +) -> Script[vs.Environment]: ... @overload @@ -280,114 +260,115 @@ def load_script( module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> ManagedScript: ... +) -> Script[ManagedEnvironment]: ... @overload def load_script( script: str | os.PathLike[str], - environment: ManagedScript, + environment: Script[ManagedEnvironment], *, inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> ManagedScript: ... +) -> Script[ManagedEnvironment]: ... def load_script( script: str | os.PathLike[str], - environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, + environment: Policy | vs.Environment | ManagedEnvironment | Script[Any] | None = None, *, module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> AbstractScript[Any]: +) -> Script[Any]: """ Runs the script at the given path. :param script: The path to the script file to run. - :param environment: Defines the environment in which the code should run. If passed - a Policy, it will create a new environment from the policy, which - can be accessed using the environment attribute. + :param environment: Defines the environment in which the code should run. + If passed a Policy, it will create a new environment from the policy, + which can be accessed using the environment attribute. :param module: The name the module should get. Defaults to __vapoursynth__. :param inline: Run the code inline, e.g. not in a separate thread. :param chdir: Change the currently running directory while the script is running. This is unsafe when running multiple scripts at once. - :returns: A script object. The script starts running when you call run() on it, - or await it. + :returns: A script object. The script starts running when you call run() on it, or await it. """ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: - with ctx: - runpy.run_path(str(script), module.__dict__, module.__name__) + with ctx, open(script) as f: + exec( + compile(f.read(), filename=script, dont_inherit=True, flags=0, mode="exec"), + module.__dict__, + module.__dict__, + ) return _load(_execute, environment, module, inline, chdir) @overload def load_code( - script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, + script: str | Buffer | ast.Module | CodeType, environment: vs.Environment | None = None, *, module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> Script: ... +) -> Script[vs.Environment]: ... @overload def load_code( - script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, - environment: Script, + script: str | Buffer | ast.Module | CodeType, + environment: Script[vs.Environment], *, inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> Script: ... +) -> Script[vs.Environment]: ... @overload def load_code( - script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, + script: str | Buffer | ast.Module | CodeType, environment: Policy | ManagedEnvironment, *, module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> ManagedScript: ... +) -> Script[ManagedEnvironment]: ... @overload def load_code( - script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, - environment: ManagedScript, + script: str | Buffer | ast.Module | CodeType, + environment: Script[ManagedEnvironment], *, inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> ManagedScript: ... +) -> Script[ManagedEnvironment]: ... def load_code( - script: str | Buffer | ast.Module | ast.Expression | ast.Interactive | CodeType, - environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, + script: str | Buffer | ast.Module | CodeType, + environment: Policy | vs.Environment | ManagedEnvironment | Script[Any] | None = None, *, module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, -) -> AbstractScript[Any]: +) -> Script[Any]: """ Runs the given code snippet. :param script: The code to run. Can be a string, bytes, AST, or compiled code. - :param environment: Defines the environment in which the code should run. If passed - a Policy, it will create a new environment from the policy, which - can be accessed using the environment attribute. If the environment - is another Script, it will take the environment and module of the - script. + :param environment: Defines the environment in which the code should run. If passed a Policy, + it will create a new environment from the policy, + which can be accessed using the environment attribute. + If the environment is another Script, it will take the environment and module of the script. :param module: The name the module should get. Defaults to __vapoursynth__. :param inline: Run the code inline, e.g. not in a separate thread. :param chdir: Change the currently running directory while the script is running. This is unsafe when running multiple scripts at once. - :returns: A script object. The script starts running when you call run() on it, - or await it. + :returns: A script object. The script starts running when you call run() on it, or await it. """ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: @@ -406,30 +387,32 @@ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: def _load( executor: Executor[None], - environment: Policy | vs.Environment | Script | ManagedEnvironment | ManagedScript | None = None, - module: str | ModuleType = "__vapoursynth__", - inline: bool = True, - chdir: str | os.PathLike[str] | None = None, -) -> AbstractScript[Any]: + environment: Policy + | vs.Environment + | ManagedEnvironment + | Script[vs.Environment] + | Script[ManagedEnvironment] + | None, + module: str | ModuleType, + inline: bool, + chdir: str | os.PathLike[str] | None, +) -> Script[Any]: runner = inline_runner if inline else to_thread if chdir is not None: runner = chdir_runner(chdir, runner) - if isinstance(environment, AbstractScript): - module = environment.module - environment = environment.environment - if isinstance(module, str): module = ModuleType(module) - if isinstance(environment, (vs.Environment, NoneType)): - if environment is None: - environment = vs.get_current_environment() - + if isinstance(environment, Script): + module = environment.module + environment = environment.environment + elif environment is None: + environment = vs.get_current_environment() + elif isinstance(environment, vs.Environment): return Script(executor, module, environment, runner) - - if isinstance(environment, Policy): + elif isinstance(environment, Policy): environment = environment.new_environment() - return ManagedScript(executor, module, environment, runner) + return Script[Any](executor, module, environment, runner) From 44b3bc2f9652ffdd8506df5dbee4decc481b4c31 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Wed, 24 Dec 2025 22:56:11 +0100 Subject: [PATCH 40/60] allow passing arguments to compile in load_code --- vsengine/vpy.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 68929e0..3964080 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -314,6 +314,7 @@ def load_code( module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, + **kwargs: Any, ) -> Script[vs.Environment]: ... @@ -324,6 +325,7 @@ def load_code( *, inline: bool = True, chdir: str | os.PathLike[str] | None = None, + **kwargs: Any, ) -> Script[vs.Environment]: ... @@ -335,6 +337,7 @@ def load_code( module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, + **kwargs: Any, ) -> Script[ManagedEnvironment]: ... @@ -345,6 +348,7 @@ def load_code( *, inline: bool = True, chdir: str | os.PathLike[str] | None = None, + **kwargs: Any, ) -> Script[ManagedEnvironment]: ... @@ -355,6 +359,7 @@ def load_code( module: str | ModuleType = "__vapoursynth__", inline: bool = True, chdir: str | os.PathLike[str] | None = None, + **kwargs: Any, ) -> Script[Any]: """ Runs the given code snippet. @@ -372,13 +377,19 @@ def load_code( """ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None: - nonlocal script + nonlocal script, kwargs with ctx: if isinstance(script, CodeType): code = script else: - code = compile(script, filename="", dont_inherit=True, flags=0, mode="exec") + compile_args: dict[str, Any] = { + "filename": "", + "dont_inherit": True, + "flags": 0, + "mode": "exec", + } | kwargs + code = compile(script, **compile_args) exec(code, module.__dict__, module.__dict__) From af5cbe00ac2241f18e626ab4e065ffde7f1306ed Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Wed, 24 Dec 2025 22:56:55 +0100 Subject: [PATCH 41/60] prevent unnecessary module creation when loading from Script object --- vsengine/vpy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vsengine/vpy.py b/vsengine/vpy.py index 3964080..e0046ad 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -413,13 +413,13 @@ def _load( if chdir is not None: runner = chdir_runner(chdir, runner) - if isinstance(module, str): - module = ModuleType(module) - if isinstance(environment, Script): module = environment.module environment = environment.environment - elif environment is None: + elif isinstance(module, str): + module = ModuleType(module) + + if environment is None: environment = vs.get_current_environment() elif isinstance(environment, vs.Environment): return Script(executor, module, environment, runner) From d84fa8aa333d589045b6023ccc0a7415c0543427 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Wed, 24 Dec 2025 23:19:03 +0100 Subject: [PATCH 42/60] remove curio reference from docstring --- vsengine/policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsengine/policy.py b/vsengine/policy.py index a8d60b2..2fbdc46 100644 --- a/vsengine/policy.py +++ b/vsengine/policy.py @@ -36,7 +36,7 @@ that can run multiple environments at once. This one behaves like vsscript. - ContextVarStore is useful when you are using event-loops like asyncio, - curio, and trio. When using this store, make sure to reuse the store + or trio. When using this store, make sure to reuse the store between successive Policy-instances as otherwise the old store might leak objects. More details are written in the documentation of the contextvars module of the standard library. From 21340b4c68ec22600519daf93d1ed249950a77c9 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Wed, 24 Dec 2025 23:19:46 +0100 Subject: [PATCH 43/60] make EventLoop base class abstract --- vsengine/loops.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/vsengine/loops.py b/vsengine/loops.py index f772cf4..d1cadcb 100644 --- a/vsengine/loops.py +++ b/vsengine/loops.py @@ -6,6 +6,8 @@ """Integrate vsengine with your event-loop (be it GUI-based or IO-based).""" +import threading +from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable, Iterator from concurrent.futures import CancelledError, Future from contextlib import contextmanager @@ -29,12 +31,11 @@ def _noop() -> Iterator[None]: DONE.set_result(None) -class EventLoop: +class EventLoop(ABC): """ Abstract base class for event loop integration. - These functions must be implemented to bridge VapourSynth - with the event-loop of your choice (e.g., asyncio, Qt). + These functions must be implemented to bridge VapourSynth with the event-loop of your choice (e.g., asyncio, Qt). """ def attach(self) -> None: @@ -43,7 +44,6 @@ def attach(self) -> None: Called automatically when :func:`set_loop` is run. """ - ... def detach(self) -> None: """ @@ -52,8 +52,8 @@ def detach(self) -> None: Called when another event-loop takes over, or when the application is shutting down/restarting. """ - ... + @abstractmethod def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ Schedule a function to run on the event loop (usually the main thread). @@ -66,7 +66,6 @@ def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.k :param kwargs: Keyword arguments for the callable. :return: A Future representing the execution result. """ - raise NotImplementedError def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: """ @@ -93,9 +92,8 @@ def wrapper() -> None: else: fut.set_result(result) - import threading - threading.Thread(target=wrapper).start() + return fut def next_cycle(self) -> Future[None]: @@ -145,19 +143,9 @@ class _NoEventLoop(EventLoop): """ The default event-loop implementation. - This is used when no specific loop is attached. It runs operations - synchronously/inline. + This is used when no specific loop is attached. It runs operations synchronously/inline. """ - def attach(self) -> None: - pass - - def detach(self) -> None: - pass - - def next_cycle(self) -> Future[None]: - return DONE - def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: fut = Future[R]() try: @@ -168,6 +156,9 @@ def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.k fut.set_result(result) return fut + def next_cycle(self) -> Future[None]: + return DONE + NO_LOOP = _NoEventLoop() current_loop: EventLoop = NO_LOOP From 3f5ea53f9aaf9933bc325c71041f20ea0bd657ca Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Wed, 24 Dec 2025 23:37:35 +0100 Subject: [PATCH 44/60] make current_loop global internal --- vsengine/loops.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vsengine/loops.py b/vsengine/loops.py index d1cadcb..55f3ef5 100644 --- a/vsengine/loops.py +++ b/vsengine/loops.py @@ -161,7 +161,7 @@ def next_cycle(self) -> Future[None]: NO_LOOP = _NoEventLoop() -current_loop: EventLoop = NO_LOOP +_current_loop: EventLoop = NO_LOOP def get_loop() -> EventLoop: @@ -170,7 +170,7 @@ def get_loop() -> EventLoop: :return: The currently running EventLoop instance. """ - return current_loop + return _current_loop def set_loop(loop: EventLoop) -> None: @@ -183,14 +183,14 @@ def set_loop(loop: EventLoop) -> None: :param loop: The EventLoop instance to attach. """ - global current_loop - current_loop.detach() + global _current_loop + _current_loop.detach() try: - current_loop = loop + _current_loop = loop loop.attach() except: - current_loop = NO_LOOP + _current_loop = NO_LOOP raise @@ -249,7 +249,7 @@ def to_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) - :param func: The function to call in a worker thread. :param args: The arguments for the function. :param kwargs: The keyword arguments to pass to the function. - :return: A loop-specific Future object. + :return: A Future representing the execution result. """ @keep_environment From 2d0e7c27786218b39fbe8ac84ac56f359ad9ecaf Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 00:11:09 +0100 Subject: [PATCH 45/60] adapters: improve typing and standardize to_thread --- vsengine/adapters/asyncio.py | 37 +++++++------- vsengine/adapters/trio.py | 96 ++++++++++++------------------------ 2 files changed, 50 insertions(+), 83 deletions(-) diff --git a/vsengine/adapters/asyncio.py b/vsengine/adapters/asyncio.py index 73f424c..1fffa8f 100644 --- a/vsengine/adapters/asyncio.py +++ b/vsengine/adapters/asyncio.py @@ -7,9 +7,8 @@ import asyncio import contextlib import contextvars -from collections.abc import Callable, Coroutine, Iterator +from collections.abc import Callable, Iterator from concurrent.futures import Future -from typing import Any from vsengine.loops import Cancelled, EventLoop @@ -19,21 +18,13 @@ class AsyncIOLoop(EventLoop): Bridges vs-engine to AsyncIO. """ - loop: asyncio.AbstractEventLoop - def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None: if loop is None: loop = asyncio.get_event_loop() self.loop = loop - def attach(self) -> None: - pass - - def detach(self) -> None: - pass - - def from_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: - future = Future[T]() + def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: + future = Future[R]() ctx = contextvars.copy_context() @@ -51,17 +42,23 @@ def _wrap() -> None: self.loop.call_soon_threadsafe(_wrap) return future - def to_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Coroutine[Any, Any, T]: # type: ignore + def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: ctx = contextvars.copy_context() + future = Future[R]() - def _wrap() -> T: + def _wrap() -> R: return ctx.run(func, *args, **kwargs) - return asyncio.to_thread(_wrap) + async def _run() -> None: + try: + result = await asyncio.to_thread(_wrap) + except BaseException as e: + future.set_exception(e) + else: + future.set_result(result) - async def await_future[T](self, future: Future[T]) -> T: - with self.wrap_cancelled(): - return await asyncio.wrap_future(future, loop=self.loop) + self.loop.create_task(_run()) + return future def next_cycle(self) -> Future[None]: future = Future[None]() @@ -76,6 +73,10 @@ def continuation() -> None: self.loop.call_soon(continuation) return future + async def await_future[T](self, future: Future[T]) -> T: + with self.wrap_cancelled(): + return await asyncio.wrap_future(future, loop=self.loop) + @contextlib.contextmanager def wrap_cancelled(self) -> Iterator[None]: try: diff --git a/vsengine/adapters/trio.py b/vsengine/adapters/trio.py index 81eb357..a44033f 100644 --- a/vsengine/adapters/trio.py +++ b/vsengine/adapters/trio.py @@ -7,45 +7,35 @@ import contextlib from collections.abc import Callable, Iterator from concurrent.futures import Future -from typing import Any -from trio import Cancelled as TrioCancelled -from trio import CancelScope, CapacityLimiter, Event, Nursery, to_thread -from trio.lowlevel import TrioToken, current_trio_token +import trio from vsengine.loops import Cancelled, EventLoop class TrioEventLoop(EventLoop): - _scope: Nursery + """ + Bridges vs-engine to Trio. + """ - def __init__(self, nursery: Nursery, limiter: CapacityLimiter | None = None) -> None: + def __init__(self, nursery: trio.Nursery, limiter: trio.CapacityLimiter | None = None) -> None: if limiter is None: - limiter = to_thread.current_default_thread_limiter() + limiter = trio.to_thread.current_default_thread_limiter() self.nursery = nursery self.limiter = limiter - self._token: TrioToken | None = None + self._token: trio.lowlevel.TrioToken | None = None def attach(self) -> None: - """ - Called when set_loop is run. - """ - self._token = current_trio_token() + self._token = trio.lowlevel.current_trio_token() def detach(self) -> None: - """ - Called when another event-loop should take over. - """ self.nursery.cancel_scope.cancel() - def from_thread[T](self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future[T]: - """ - Ran from vapoursynth threads to move data to the event loop. - """ + def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: assert self._token is not None - fut = Future[T]() + fut = Future[R]() def _executor() -> None: if not fut.set_running_or_notify_cancel(): @@ -61,33 +51,25 @@ def _executor() -> None: self._token.run_sync_soon(_executor) return fut - async def to_thread(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: # type: ignore - """ - Run this function in a worker thread. - """ - result = None - error: BaseException | None = None + def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]: + future = Future[R]() - def _executor() -> None: - nonlocal result, error - try: - result = func(*args, **kwargs) - except BaseException as e: - error = e + async def _run() -> None: + def _executor() -> None: + try: + result = func(*args, **kwargs) + future.set_result(result) + except BaseException as e: + future.set_exception(e) - await to_thread.run_sync(_executor, limiter=self.limiter) + await trio.to_thread.run_sync(_executor, limiter=self.limiter) - if error is not None: - # unreachable? - assert isinstance(error, BaseException) - raise error - else: - return result + self.nursery.start_soon(_run) + return future def next_cycle(self) -> Future[None]: - scope = CancelScope() + scope = trio.CancelScope() future = Future[None]() - TrioEventLoop.to_thread def continuation() -> None: if scope.cancel_called: @@ -99,43 +81,27 @@ def continuation() -> None: return future async def await_future[T](self, future: Future[T]) -> T: - """ - Await a concurrent future. - - This function does not need to be implemented if the event-loop - does not support async and await. - """ - event = Event() - - result: T | None = None - error: BaseException | None = None + event = trio.Event() def _when_done(_: Future[T]) -> None: - nonlocal error, result - if (error := future.exception()) is not None: - pass - else: - result = future.result() self.from_thread(event.set) future.add_done_callback(_when_done) + try: await event.wait() - except TrioCancelled: + except trio.Cancelled: raise - if error is not None: + try: + return future.result() + except BaseException as exc: with self.wrap_cancelled(): - raise error - else: - return result # type: ignore + raise exc @contextlib.contextmanager def wrap_cancelled(self) -> Iterator[None]: - """ - Wraps vsengine.loops.Cancelled into the native cancellation error. - """ try: yield except Cancelled: - raise TrioCancelled.__new__(TrioCancelled) from None + raise trio.Cancelled.__new__(trio.Cancelled) from None From 123de26d3458130206b7561ab7ad92de101158b0 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 00:50:14 +0100 Subject: [PATCH 46/60] remove nix stuff --- flake.lock | 150 --------------------------------- flake.nix | 199 -------------------------------------------- nix/lib/debug.nix | 9 -- nix/lib/default.nix | 11 --- nix/lib/matrix.nix | 49 ----------- 5 files changed, 418 deletions(-) delete mode 100644 flake.lock delete mode 100644 flake.nix delete mode 100644 nix/lib/debug.nix delete mode 100644 nix/lib/default.nix delete mode 100644 nix/lib/matrix.nix diff --git a/flake.lock b/flake.lock deleted file mode 100644 index b0f48e3..0000000 --- a/flake.lock +++ /dev/null @@ -1,150 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1662019588, - "narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "2da64a81275b68fdad38af669afeda43d401e94b", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "vs_58_vs": "vs_58_vs", - "vs_58_zimg": "vs_58_zimg", - "vs_59_vs": "vs_59_vs", - "vs_59_zimg": "vs_59_zimg", - "vs_latest_vs": "vs_latest_vs", - "vs_latest_zimg": "vs_latest_zimg" - } - }, - "vs_58_vs": { - "flake": false, - "locked": { - "lastModified": 1649869847, - "narHash": "sha256-LIjNfyfpyvE+Ec6f4aGzRA4ZGoWPFhjtUw4yrenDsUQ=", - "owner": "vapoursynth", - "repo": "vapoursynth", - "rev": "fd31a2d36811b09224aed507b9009011acb6497f", - "type": "github" - }, - "original": { - "owner": "vapoursynth", - "ref": "R58", - "repo": "vapoursynth", - "type": "github" - } - }, - "vs_58_zimg": { - "flake": false, - "locked": { - "lastModified": 1651934905, - "narHash": "sha256-n4YJ0uWQ8vAlW6m2INGKYD509hAjvjbIqBY3+/rrkHs=", - "owner": "sekrit-twc", - "repo": "zimg", - "rev": "1c76327f50dd3e9b8b04200656440bd387c3888c", - "type": "github" - }, - "original": { - "owner": "sekrit-twc", - "ref": "v3.0", - "repo": "zimg", - "type": "github" - } - }, - "vs_59_vs": { - "flake": false, - "locked": { - "lastModified": 1653982033, - "narHash": "sha256-6w7GSC5ZNIhLpulni4sKq0OvuxHlTJRilBFGH5PQW8U=", - "owner": "vapoursynth", - "repo": "vapoursynth", - "rev": "da7d758ff70dc9789ed89969c2d3a307483153bf", - "type": "github" - }, - "original": { - "owner": "vapoursynth", - "ref": "R59", - "repo": "vapoursynth", - "type": "github" - } - }, - "vs_59_zimg": { - "flake": false, - "locked": { - "lastModified": 1651934905, - "narHash": "sha256-n4YJ0uWQ8vAlW6m2INGKYD509hAjvjbIqBY3+/rrkHs=", - "owner": "sekrit-twc", - "repo": "zimg", - "rev": "1c76327f50dd3e9b8b04200656440bd387c3888c", - "type": "github" - }, - "original": { - "owner": "sekrit-twc", - "ref": "v3.0", - "repo": "zimg", - "type": "github" - } - }, - "vs_latest_vs": { - "flake": false, - "locked": { - "lastModified": 1661360597, - "narHash": "sha256-Kx868jCFOjyKTWa11GwnJIXZoSZlC1RJY97xLkKlZnY=", - "owner": "vapoursynth", - "repo": "vapoursynth", - "rev": "dca55e1ad999f4e3dc6b81686aad8534a5e710af", - "type": "github" - }, - "original": { - "owner": "vapoursynth", - "repo": "vapoursynth", - "type": "github" - } - }, - "vs_latest_zimg": { - "flake": false, - "locked": { - "lastModified": 1651934905, - "narHash": "sha256-n4YJ0uWQ8vAlW6m2INGKYD509hAjvjbIqBY3+/rrkHs=", - "owner": "sekrit-twc", - "repo": "zimg", - "rev": "1c76327f50dd3e9b8b04200656440bd387c3888c", - "type": "github" - }, - "original": { - "owner": "sekrit-twc", - "ref": "v3.0", - "repo": "zimg", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 755eb62..0000000 --- a/flake.nix +++ /dev/null @@ -1,199 +0,0 @@ -{ - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - inputs.flake-utils.url = "github:numtide/flake-utils"; - - #### - # This is for our test matrix. - - # VS latest - inputs.vs_latest_vs = { - url = "github:vapoursynth/vapoursynth"; - flake = false; - }; - inputs.vs_latest_zimg = { - url = "github:sekrit-twc/zimg/v3.0"; - flake = false; - }; - - # VS R58 - inputs.vs_58_vs = { - url = "github:vapoursynth/vapoursynth/R58"; - flake = false; - }; - inputs.vs_58_zimg = { - url = "github:sekrit-twc/zimg/v3.0"; - flake = false; - }; - - # VS R59 - inputs.vs_59_vs = { - url = "github:vapoursynth/vapoursynth/R59"; - flake = false; - }; - inputs.vs_59_zimg = { - url = "github:sekrit-twc/zimg/v3.0"; - flake = false; - }; - - outputs = { self, nixpkgs, flake-utils, ... }@releases: - let - # Default versions for development. - defaults = { - python = "310"; - vapoursynth = "latest"; - }; - - # Supported versions - versions = { - python = [ "39" "310" ]; - vapoursynth = [ 58 59 "latest" ]; - }; - - # Version-Numbers for versions like "latest" - aliases = { - vapoursynth = { - latest = 60; - }; - }; - in - flake-utils.lib.eachSystem [ "i686-linux" "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ] (system: - let - pkgs = import nixpkgs { - inherit system; - config = { - allowUnsupportedSystem = true; - allowBroken = true; - }; - }; - - lib = pkgs.lib; - - findForRelease = release: - let - prefix = "vs_${toString release}_"; - filtered = lib.filterAttrs (k: v: lib.hasPrefix prefix k) releases; - in - lib.mapAttrs' (k: v: { name = lib.removePrefix prefix k; value = v; }) filtered; - - makeVapourSynthPackage = release: ps: - let - sources = findForRelease release; - - zimg = pkgs.zimg.overrideAttrs (old: { - src = sources.zimg; - }); - - vapoursynth = (pkgs.vapoursynth.overrideAttrs (old: { - # Do not override default python. - # We are rebuilding the python module regardless, so there - # is no need to recompile the vapoursynth module. - src = sources.vs; - version = "r" + toString (if (builtins.hasAttr (toString release) aliases.vapoursynth) then aliases.vapoursynth."${release}" else release) + ""; - configureFlags = [ "--disable-python-module" ] ++ (if (old ? configureFlags) then old.configureFlags else []); - preConfigure = '' - ${# Darwin requires special ld-flags to compile with the patch that implements vapoursynth.withPlugins. - lib.optionalString (pkgs.stdenv.isDarwin) '' - export LDFLAGS="-Wl,-U,_VSLoadPluginsNix''${LDFLAGS:+ ''${LDFLAGS}}" - ''} - ${lib.optionalString (old ? preConfigure) old.preConfigure} - ''; - })).override { zimg = zimg; }; - in - ps.buildPythonPackage { - pname = "vapoursynth"; - inherit (vapoursynth) src version; - pversion = lib.removePrefix "r" vapoursynth.version; - buildInputs = [ ps.cython vapoursynth ]; - checkPhase = "true"; - }; - - flib = import ./nix/lib pkgs; - - matrix = (flib.version-builders versions defaults).map-versions (versions: rec { - python = pkgs."python${versions.python}"; - vapoursynth = makeVapourSynthPackage versions.vapoursynth python.pkgs; - build-name = prefix: flib.versions-to-name prefix versions; - }); - in - rec { - packages = - let - package-matrix = matrix.build-with-default "vsengine" - (versions: versions.python.pkgs.buildPythonPackage rec { - pname = "vsengine"; - pversion = (builtins.fromTOML (builtins.readFile ./pyproject.toml)).project.version; - version = "r${lib.replaceStrings ["+"] ["_"] pversion}"; - format = "flit"; - src = ./.; - propagatedBuildInputs = let ps = versions.python.pkgs; in [ - ps.trio - ps.pytest - ps.setuptools - versions.vapoursynth - ]; - }); - in - package-matrix // { - dist = pkgs.runCommandNoCC "dist" { - FLIT_NO_NETWORK="1"; - SOURCE_DATE_EPOCH = "0"; - src = ./.; - } ( - let - versions = map (version-map: '' - ${version-map.python.pkgs.flit}/bin/flit build - '') matrix.passed-versions; - script = builtins.concatStringsSep "\n" versions; - in - '' - mkdir $out - cp -r $src/* . - ${script} - cp dist/* $out - '' - ); - }; - - # Build shells with each vapoursynth-version / python-tuple - devShells = matrix.build-with-default "devShell" - (versions: pkgs.mkShell { - buildInputs = [ - (versions.python.withPackages (ps: [ - ps.flit - ps.trio - ps.pytest - versions.vapoursynth - ])) - - (versions.python.withPackages (ps: [ - # ps.mkdocs-material - ps.mkdocs - ])) - ]; - }); - - checks = - let - mtx = - matrix.build "check" - (versions: pkgs.runCommandNoCC (versions.build-name "check") {} - (let py = versions.python.withPackages (ps: [packages.${versions.build-name "vsengine"}]); in '' - ${py}/bin/python -m unittest discover -s ${./tests} -v - touch $out - '')); - in - mtx // { - default = - pkgs.runCommandNoCC "all" {} '' - ${builtins.concatStringsSep "\n" (map (v: '' - echo ${v} > $out - '') (builtins.attrValues mtx))} - ''; - }; - - - # Compat with nix<2.7 - devShell = devShells.default; - defaultPackage = packages.default; - }); -} diff --git a/nix/lib/debug.nix b/nix/lib/debug.nix deleted file mode 100644 index 7ba3c4e..0000000 --- a/nix/lib/debug.nix +++ /dev/null @@ -1,9 +0,0 @@ -# vs-engine -# Copyright (C) 2022 cid-chan -# This project is licensed under the EUPL-1.2 -# SPDX-License-Identifier: EUPL-1.2 - -{ lib, ... }: -{ - strace = s: builtins.trace s s; -} diff --git a/nix/lib/default.nix b/nix/lib/default.nix deleted file mode 100644 index 0e5ae37..0000000 --- a/nix/lib/default.nix +++ /dev/null @@ -1,11 +0,0 @@ -# vs-engine -# Copyright (C) 2022 cid-chan -# This project is licensed under the EUPL-1.2 -# SPDX-License-Identifier: EUPL-1.2 - -{ lib, ... }@pkgs: -lib.foldl' (p: n: p // n) {} - (builtins.map (path: import path pkgs) [ - ./matrix.nix - ./debug.nix - ]) diff --git a/nix/lib/matrix.nix b/nix/lib/matrix.nix deleted file mode 100644 index ba1765e..0000000 --- a/nix/lib/matrix.nix +++ /dev/null @@ -1,49 +0,0 @@ -# vs-engine -# Copyright (C) 2022 cid-chan -# This project is licensed under the EUPL-1.2 -# SPDX-License-Identifier: EUPL-1.2 - -{ lib, ... }: -let strace = (import ./debug.nix { inherit lib; }).strace; -in -rec { - versions-to-name = prefix: version-map: - let - dynamicParts = lib.mapAttrsToList (k: v: "${k}${toString v}") version-map; - allParts = [prefix] ++ dynamicParts; - in - builtins.concatStringsSep "-" allParts; - - each-version = what: lib.cartesianProductOfSets what; - - version-matrix = what: prefix: func: - builtins.listToAttrs (map (versions: { - name = versions-to-name prefix versions; - value = func versions; - }) (each-version what)); - - version-matrix-with-default = what: defaults: prefix: func: - let - matrix = version-matrix what prefix func; - in - matrix // { - default = matrix."${versions-to-name prefix defaults}"; - }; - - __version-builders = what: defaults: mapper: - let - run-func-with-mapper = func: versions: (mapper func) versions; - in - { - build = prefix: func: version-matrix what prefix (run-func-with-mapper func); - build-with-default = prefix: func: version-matrix-with-default what defaults prefix (run-func-with-mapper func); - - versions = each-version what; - passed-versions = lib.mapAttrsToList (k: v: v) (version-matrix what "unused" (run-func-with-mapper (versions: versions))); - - map = next-mapper: __version-builders what defaults (f: next-mapper (mapper f)); - map-versions = version-mapper: __version-builders what defaults (f: versions: (mapper f) (version-mapper versions)); - }; - - version-builders = what: defaults: __version-builders what defaults (f: f); -} From 801a8eae23965cc68b2096f6a1468adce052c42d Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 00:50:30 +0100 Subject: [PATCH 47/60] remove test plugins --- extra_tests/__init__.py | 6 -- extra_tests/pytest.py | 232 ---------------------------------------- extra_tests/unittest.py | 74 ------------- 3 files changed, 312 deletions(-) delete mode 100644 extra_tests/__init__.py delete mode 100644 extra_tests/pytest.py delete mode 100644 extra_tests/unittest.py diff --git a/extra_tests/__init__.py b/extra_tests/__init__.py deleted file mode 100644 index 6b66964..0000000 --- a/extra_tests/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# vs-engine -# Copyright (C) 2022 cid-chan -# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy -# This project is licensed under the EUPL-1.2 -# SPDX-License-Identifier: EUPL-1.2 - diff --git a/extra_tests/pytest.py b/extra_tests/pytest.py deleted file mode 100644 index 33d0915..0000000 --- a/extra_tests/pytest.py +++ /dev/null @@ -1,232 +0,0 @@ -# vs-engine -# Copyright (C) 2022 cid-chan -# This project is licensed under the EUPL-1.2 -# SPDX-License-Identifier: EUPL-1.2 - -import pathlib - -import pytest - -from vsengine._hospice import any_alive, freeze -from vsengine.policy import GlobalStore, Policy - -DEFAULT_STAGES = ("initial-core", "reloaded-core") - -KNOWN_STAGES = ["no-core", "initial-core", "reloaded-core", "unique-core"] - - -DEFAULT_ERROR_MESSAGE = [ - "Your test suite left a dangling object to a vapoursynth core.", - "Please make sure this does not happen, as this might cause some previewers to crash after reloading a script.", -] - - -### -# Add the marker to the docs -def pytest_configure(config: "Config") -> None: - config.addinivalue_line( - "markers", - 'vpy(*stages: Literal["no_core", "first_core", "second_core"]): ' - "Mark what stages should be run. (Defaults to first_core+second_core)", - ) - - -### -# Make sure a policy is registered before tests are collected. -current_policy = None -current_env = None - - -def pytest_sessionstart(session): - global current_policy - current_policy = Policy(GlobalStore()) - current_policy.register() - - -def pytest_sessionfinish(): - global current_policy, current_env - if current_env is not None: - current_env.dispose() - current_policy.unregister() - - -### -# Ensure tests are ordered correctly -@pytest.fixture(params=DEFAULT_STAGES) -def vpy_stages(request) -> str: - return request.param - - -class CleanupFailed: - def __init__(self, previous, next_text) -> None: - self.previous = previous - self.next_text = next_text - - def __str__(self): - if self.previous is None: - return self.next_text - - return f"{self.previous}\n\n{self.next_text}" - - def __repr__(self) -> str: - return "<{} instance at {:0x}>".format(self.__class__, id(self)) - - def toterminal(self, tw): - if self.previous is not None: - self.previous.toterminal(tw) - tw.line("") - color = {"yellow": True} - tw.line("vs-engine has detected an additional problem with this test:", yellow=True, bold=True) - indent = " " - else: - color = {"red": True} - indent = "" - - for line in self.next_text.split("\n"): - tw.line(indent + line, **color) - - -class VapoursynthEnvironment(pytest.Item): - pass - - -class EnsureCleanEnvironment(pytest.Item): - def __init__(self, *, stage, **kwargs) -> None: - super().__init__(**kwargs) - self.stage = stage - self.path = "" - - def runtest(self): - global current_env - if current_env is not None: - current_env.dispose() - current_env = None - any_alive_left = any_alive() - freeze() - assert not any_alive_left, "Expected all environments to be cleaned up." - current_env = None - - def repr_failure(self, excinfo): - return CleanupFailed(None, "\n".join(DEFAULT_ERROR_MESSAGE)) - - def reportinfo(self): - return pathlib.Path(""), None, f"cleaning up: {self.stage}" - - -@pytest.hookimpl(tryfirst=True) -def pytest_pycollect_makeitem(collector, name, obj) -> None: - if collector.istestfunction(obj, name): - inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj - marker = collector.get_closest_marker("vpy") - own_markers = getattr(obj, "pytestmark", ()) - if marker or any(marker.name == "vpy" for marker in own_markers): - real_marker = marker or tuple(marker for marker in own_markers if marker.name == "vpy")[0] - obj._vpy_stages = real_marker.args - else: - obj._vpy_stages = DEFAULT_STAGES - - -def pytest_generate_tests(metafunc): - obj = metafunc.function - if hasattr(obj, "_vpy_stages"): - stages = obj._vpy_stages - metafunc.fixturenames += ["__vpy_stage"] - metafunc.parametrize(("__vpy_stage",), tuple((stage,) for stage in stages), ids=stages) - - -def pytest_collection_modifyitems(session, config, items): - stages = {} - for stage in KNOWN_STAGES: - stages[stage] = [] - - for item in items: - spec = item.callspec - stages[spec.params.get("__vpy_stage", "no-core")].append(item) - - new_items = [] - - virtual_parent = VapoursynthEnvironment.from_parent(session, name="@vs-engine") - for stage in KNOWN_STAGES: - new_items.extend(stages[stage]) - # Add two synthetic tests that make sure the environment is clean. - if stage in ("initial-core", "reloaded-core"): - new_items.append( - EnsureCleanEnvironment.from_parent( - virtual_parent, name=f"@check-clean-environment[{stage}]", stage=stage - ) - ) - - items[:] = new_items - - -### -# Do the magic -current_stage = "no-core" - - -@pytest.hookimpl(tryfirst=True) -def pytest_pyfunc_call(pyfuncitem): - global current_stage, current_env - spec = pyfuncitem.callspec - stage = spec.params.get("__vpy_stage", "no-core") - - if stage != current_stage: - if stage == "initial-core": - current_env = current_policy.new_environment() - - if stage == "reloaded-core": - if current_env is None: - current_env = current_policy.new_environment() - current_env.dispose() - current_env = current_policy.new_environment() - - if stage == "unique-core": - if current_env is not None: - current_env.dispose() - current_env = None - - current_stage = stage - - funcargs = pyfuncitem.funcargs - testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} - - if stage == "unique-core": - env = current_policy.new_environment() - try: - with env.use(): - pyfuncitem.obj(**testargs) - except BaseException as e: - failed = e - else: - failed = False - finally: - if env is not None: - env.dispose() - env = None - - if any_alive(): - freeze() - if failed is False: - pyfuncitem._repr_failure_py = lambda _, style=None: CleanupFailed( - None, "\n".join(DEFAULT_ERROR_MESSAGE) - ) - assert False - else: - pre_rfp = pyfuncitem._repr_failure_py - - def _new_rfp(*args, **kwargs): - previous = pre_rfp(*args, **kwargs) - err = "\n".join(DEFAULT_ERROR_MESSAGE) - return CleanupFailed(previous, err) - - pyfuncitem._repr_failure_py = _new_rfp - raise failed - elif failed: - raise failed - - return True - - elif current_env is not None: - with current_env.use(): - pyfuncitem.obj(**testargs) - return True diff --git a/extra_tests/unittest.py b/extra_tests/unittest.py deleted file mode 100644 index 1cf5998..0000000 --- a/extra_tests/unittest.py +++ /dev/null @@ -1,74 +0,0 @@ -# vs-engine -# Copyright (C) 2022 cid-chan -# This project is licensed under the EUPL-1.2 -# SPDX-License-Identifier: EUPL-1.2 -import sys -from unittest.main import TestProgram - -from vsengine._hospice import any_alive, freeze -from vsengine.policy import GlobalStore, Policy - -DEFAULT_ERROR_MESSAGE = [ - "Your test suite left a dangling object to a vapoursynth core.", - "Please make sure this does not happen, as this might cause some previewers to crash after reloading a script.", -] - - -class MultiCoreTestProgram(TestProgram): - def __init__(self, *args, **kwargs): - self._policy = Policy(GlobalStore()) - self._policy.register() - super().__init__(*args, **kwargs) - - def _run_once(self): - try: - super().runTests() - except SystemExit as e: - return e.code - else: - return 0 - - def parseArgs(self, argv: list[str]) -> None: - self.argv = argv - return super().parseArgs(argv) - - def runTests(self): - any_alive_left = False - - with self._policy.new_environment() as e1, e1.use(): - self._run_once() - del e1 - - if self.exit and not self.result.wasSuccessful(): - sys.exit(1) - - if any_alive(): - print(*DEFAULT_ERROR_MESSAGE, sep="\n", file=sys.stderr) - any_alive_left = True - freeze() - - super().parseArgs(self.argv) - with self._policy.new_environment() as e2, e2.use(): - self._run_once() - del e2 - - if any_alive(): - print(*DEFAULT_ERROR_MESSAGE, sep="\n", file=sys.stderr) - any_alive_left = True - freeze() - - if self.exit: - if not self.result.wasSuccessful(): - sys.exit(1) - elif any_alive_left: - sys.exit(2) - - sys.exit(0) - - -def main(): - MultiCoreTestProgram(module=None) - - -if __name__ == "__main__": - main() From 8549e3aa5a4b21f8257caab12579235d0a715983 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 00:51:09 +0100 Subject: [PATCH 48/60] update deps & stubs --- stubs/vapoursynth/__init__.pyi | 43 ++++---- uv.lock | 188 ++++++++++++++++----------------- 2 files changed, 118 insertions(+), 113 deletions(-) diff --git a/stubs/vapoursynth/__init__.pyi b/stubs/vapoursynth/__init__.pyi index e309dc8..8bb64e7 100644 --- a/stubs/vapoursynth/__init__.pyi +++ b/stubs/vapoursynth/__init__.pyi @@ -1,5 +1,10 @@ -# ruff: noqa: RUF100, E501, PYI002, PYI029, PYI046, PYI047, N801, N802, N803, N805, I001 -from collections.abc import Buffer +# This file is auto-generated. DO NOT EDIT. +# ruff: noqa +# flake8: noqa +# fmt: off +# isort: skip_file + +from collections.abc import Buffer, Callable, Iterable, Iterator, Mapping, MutableMapping from concurrent.futures import Future from ctypes import c_void_p from enum import Enum, IntEnum, IntFlag @@ -7,7 +12,7 @@ from fractions import Fraction from inspect import Signature from logging import Handler, LogRecord, StreamHandler from types import MappingProxyType, TracebackType -from typing import Any, Callable, Concatenate, Final, IO, Iterable, Iterator, Literal, Mapping, MutableMapping, NamedTuple, Protocol, Self, SupportsFloat, SupportsIndex, SupportsInt, TextIO, TypedDict, final, overload +from typing import Any, Concatenate, Final, IO, Literal, NamedTuple, Protocol, Self, SupportsFloat, SupportsIndex, SupportsInt, TextIO, TypedDict, final, overload from warnings import deprecated from weakref import ReferenceType @@ -113,19 +118,19 @@ type _VSValueIterable = ( ) type _VSValue = _VSValueSingle | _VSValueIterable -class _SupportsIter[_T](Protocol): - def __iter__(self) -> Iterator[_T]: ... +class _SupportsIter[T](Protocol): + def __iter__(self) -> Iterator[T]: ... -class _SequenceLike[_T](Protocol): - def __iter__(self) -> Iterator[_T]: ... +class _SequenceLike[T](Protocol): + def __iter__(self) -> Iterator[T]: ... def __len__(self) -> int: ... -class _GetItemIterable[_T](Protocol): - def __getitem__(self, i: SupportsIndex, /) -> _T: ... +class _GetItemIterable[T](Protocol): + def __getitem__(self, i: SupportsIndex, /) -> T: ... -class _SupportsKeysAndGetItem[_KT, _VT](Protocol): - def __getitem__(self, key: _KT, /) -> _VT: ... - def keys(self) -> Iterable[_KT]: ... +class _SupportsKeysAndGetItem[KT, VT](Protocol): + def __getitem__(self, key: KT, /) -> VT: ... + def keys(self) -> Iterable[KT]: ... class _VSCallback(Protocol): def __call__(self, *args: Any, **kwargs: Any) -> _VSValue: ... @@ -809,13 +814,13 @@ _VSPlugin = Plugin _VSFunction = Function class _Wrapper: - class Function[**_P, _R](_VSFunction): - def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, _P], _R]) -> None: ... - def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... + class Function[**P, R](_VSFunction): + def __init__[PluginT: Plugin](self, function: Callable[Concatenate[PluginT, P], R]) -> None: ... + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... class _Wrapper_Core_bound_FrameEval: class Function(_VSFunction): - def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, ...], VideoNode]) -> None: ... + def __init__[PluginT: Plugin](self, function: Callable[Concatenate[PluginT, ...], VideoNode]) -> None: ... @overload def __call__( self, @@ -859,7 +864,7 @@ class _Wrapper_Core_bound_FrameEval: class _Wrapper_VideoNode_bound_FrameEval: class Function(_VSFunction): - def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, ...], VideoNode]) -> None: ... + def __init__[PluginT: Plugin](self, function: Callable[Concatenate[PluginT, ...], VideoNode]) -> None: ... @overload def __call__( self, @@ -898,7 +903,7 @@ class _Wrapper_VideoNode_bound_FrameEval: class _Wrapper_Core_bound_ModifyFrame: class Function(_VSFunction): - def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, ...], VideoNode]) -> None: ... + def __init__[PluginT: Plugin](self, function: Callable[Concatenate[PluginT, ...], VideoNode]) -> None: ... @overload def __call__( self, clip: VideoNode, clips: VideoNode, selector: _VSCallback_std_ModifyFrame_selector_0 @@ -917,7 +922,7 @@ class _Wrapper_Core_bound_ModifyFrame: class _Wrapper_VideoNode_bound_ModifyFrame: class Function(_VSFunction): - def __init__[_PluginT: Plugin](self, function: Callable[Concatenate[_PluginT, ...], VideoNode]) -> None: ... + def __init__[PluginT: Plugin](self, function: Callable[Concatenate[PluginT, ...], VideoNode]) -> None: ... @overload def __call__(self, clips: VideoNode, selector: _VSCallback_std_ModifyFrame_selector_0) -> VideoNode: ... @overload diff --git a/uv.lock b/uv.lock index eeb9da8..8c72cb7 100644 --- a/uv.lock +++ b/uv.lock @@ -79,54 +79,54 @@ wheels = [ [[package]] name = "librt" -version = "0.6.3" +version = "0.7.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/c3/cdff3c10e2e608490dc0a310ccf11ba777b3943ad4fcead2a2ade98c21e1/librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae", size = 54209, upload-time = "2025-11-29T14:01:56.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/2c/b59249c566f98fe90e178baf59e83f628d6c38fb8bc78319301fccda0b5e/librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176", size = 27841, upload-time = "2025-11-29T14:00:58.925Z" }, - { url = "https://files.pythonhosted.org/packages/40/e8/9db01cafcd1a2872b76114c858f81cc29ce7ad606bc102020d6dabf470fb/librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057", size = 27844, upload-time = "2025-11-29T14:01:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/59/4d/da449d3a7d83cc853af539dee42adc37b755d7eea4ad3880bacfd84b651d/librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610", size = 84091, upload-time = "2025-11-29T14:01:01.118Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6c/f90306906fb6cc6eaf4725870f0347115de05431e1f96d35114392d31fda/librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c", size = 88239, upload-time = "2025-11-29T14:01:02.11Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ae/473ce7b423cfac2cb503851a89d9d2195bf615f534d5912bf86feeebbee7/librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9", size = 88815, upload-time = "2025-11-29T14:01:03.114Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6d/934df738c87fb9617cabefe4891eece585a06abe6def25b4bca3b174429d/librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c", size = 90598, upload-time = "2025-11-29T14:01:04.071Z" }, - { url = "https://files.pythonhosted.org/packages/72/89/eeaa124f5e0f431c2b39119550378ae817a4b1a3c93fd7122f0639336fff/librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c", size = 88603, upload-time = "2025-11-29T14:01:05.02Z" }, - { url = "https://files.pythonhosted.org/packages/4d/ed/c60b3c1cfc27d709bc0288af428ce58543fcb5053cf3eadbc773c24257f5/librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf", size = 92112, upload-time = "2025-11-29T14:01:06.304Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ab/f56169be5f716ef4ab0277be70bcb1874b4effc262e655d85b505af4884d/librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d", size = 20127, upload-time = "2025-11-29T14:01:07.283Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8d/222750ce82bf95125529eaab585ac7e2829df252f3cfc05d68792fb1dd2c/librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01", size = 21545, upload-time = "2025-11-29T14:01:08.184Z" }, - { url = "https://files.pythonhosted.org/packages/72/c9/f731ddcfb72f446a92a8674c6b8e1e2242773cce43a04f41549bd8b958ff/librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55", size = 20946, upload-time = "2025-11-29T14:01:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/dd/aa/3055dd440f8b8b3b7e8624539a0749dd8e1913e978993bcca9ce7e306231/librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3", size = 27874, upload-time = "2025-11-29T14:01:10.615Z" }, - { url = "https://files.pythonhosted.org/packages/ef/93/226d7dd455eaa4c26712b5ccb2dfcca12831baa7f898c8ffd3a831e29fda/librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5", size = 27852, upload-time = "2025-11-29T14:01:11.535Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8b/db9d51191aef4e4cc06285250affe0bb0ad8b2ed815f7ca77951655e6f02/librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42", size = 84264, upload-time = "2025-11-29T14:01:12.461Z" }, - { url = "https://files.pythonhosted.org/packages/8d/53/297c96bda3b5a73bdaf748f1e3ae757edd29a0a41a956b9c10379f193417/librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70", size = 88432, upload-time = "2025-11-29T14:01:13.405Z" }, - { url = "https://files.pythonhosted.org/packages/54/3a/c005516071123278e340f22de72fa53d51e259d49215295c212da16c4dc2/librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522", size = 89014, upload-time = "2025-11-29T14:01:14.373Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9b/ea715f818d926d17b94c80a12d81a79e95c44f52848e61e8ca1ff29bb9a9/librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30", size = 90807, upload-time = "2025-11-29T14:01:15.377Z" }, - { url = "https://files.pythonhosted.org/packages/f0/fc/4e2e4c87e002fa60917a8e474fd13c4bac9a759df82be3778573bb1ab954/librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917", size = 88890, upload-time = "2025-11-29T14:01:16.633Z" }, - { url = "https://files.pythonhosted.org/packages/70/7f/c7428734fbdfd4db3d5b9237fc3a857880b2ace66492836f6529fef25d92/librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530", size = 92300, upload-time = "2025-11-29T14:01:17.658Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0c/738c4824fdfe74dc0f95d5e90ef9e759d4ecf7fd5ba964d54a7703322251/librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3", size = 20159, upload-time = "2025-11-29T14:01:18.61Z" }, - { url = "https://files.pythonhosted.org/packages/f2/95/93d0e61bc617306ecf4c54636b5cbde4947d872563565c4abdd9d07a39d3/librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88", size = 21484, upload-time = "2025-11-29T14:01:19.506Z" }, - { url = "https://files.pythonhosted.org/packages/10/23/abd7ace79ab54d1dbee265f13529266f686a7ce2d21ab59a992f989009b6/librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9", size = 20935, upload-time = "2025-11-29T14:01:20.415Z" }, - { url = "https://files.pythonhosted.org/packages/83/14/c06cb31152182798ed98be73f54932ab984894f5a8fccf9b73130897a938/librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a", size = 27566, upload-time = "2025-11-29T14:01:21.609Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/ce83ca7b057b06150519152f53a0b302d7c33c8692ce2f01f669b5a819d9/librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd", size = 27753, upload-time = "2025-11-29T14:01:22.558Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ec/739a885ef0a2839b6c25f1b01c99149d2cb6a34e933ffc8c051fcd22012e/librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b", size = 83178, upload-time = "2025-11-29T14:01:23.555Z" }, - { url = "https://files.pythonhosted.org/packages/db/bd/dc18bb1489d48c0911b9f4d72eae2d304ea264e215ba80f1e6ba4a9fc41d/librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc", size = 87266, upload-time = "2025-11-29T14:01:24.532Z" }, - { url = "https://files.pythonhosted.org/packages/94/f3/d0c5431b39eef15e48088b2d739ad84b17c2f1a22c0345c6d4c4a42b135e/librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3", size = 87623, upload-time = "2025-11-29T14:01:25.798Z" }, - { url = "https://files.pythonhosted.org/packages/3b/15/9a52e90834e4bd6ee16cdbaf551cb32227cbaad27398391a189c489318bc/librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751", size = 89436, upload-time = "2025-11-29T14:01:26.769Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8a/a7e78e46e8486e023c50f21758930ef4793999115229afd65de69e94c9cc/librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8", size = 87540, upload-time = "2025-11-29T14:01:27.756Z" }, - { url = "https://files.pythonhosted.org/packages/49/01/93799044a1cccac31f1074b07c583e181829d240539657e7f305ae63ae2a/librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e", size = 90597, upload-time = "2025-11-29T14:01:29.35Z" }, - { url = "https://files.pythonhosted.org/packages/a7/29/00c7f58b8f8eb1bad6529ffb6c9cdcc0890a27dac59ecda04f817ead5277/librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf", size = 18955, upload-time = "2025-11-29T14:01:30.325Z" }, - { url = "https://files.pythonhosted.org/packages/d7/13/2739e6e197a9f751375a37908a6a5b0bff637b81338497a1bcb5817394da/librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2", size = 20263, upload-time = "2025-11-29T14:01:31.287Z" }, - { url = "https://files.pythonhosted.org/packages/e1/73/393868fc2158705ea003114a24e73bb10b03bda31e9ad7b5c5ec6575338b/librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78", size = 19575, upload-time = "2025-11-29T14:01:32.229Z" }, - { url = "https://files.pythonhosted.org/packages/48/6d/3c8ff3dec21bf804a205286dd63fd28dcdbe00b8dd7eb7ccf2e21a40a0b0/librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179", size = 28732, upload-time = "2025-11-29T14:01:33.165Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/e214b8b4aa34ed3d3f1040719c06c4d22472c40c5ef81a922d5af7876eb4/librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7", size = 29065, upload-time = "2025-11-29T14:01:34.088Z" }, - { url = "https://files.pythonhosted.org/packages/ab/90/ef61ed51f0a7770cc703422d907a757bbd8811ce820c333d3db2fd13542a/librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916", size = 93703, upload-time = "2025-11-29T14:01:35.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ae/c30bb119c35962cbe9a908a71da99c168056fc3f6e9bbcbc157d0b724d89/librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8", size = 98890, upload-time = "2025-11-29T14:01:36.031Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/47a4a78d252d36f072b79d592df10600d379a895c3880c8cbd2ac699f0ad/librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc", size = 98255, upload-time = "2025-11-29T14:01:37.058Z" }, - { url = "https://files.pythonhosted.org/packages/e5/28/779b5cc3cd9987683884eb5f5672e3251676bebaaae6b7da1cf366eb1da1/librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07", size = 100769, upload-time = "2025-11-29T14:01:38.413Z" }, - { url = "https://files.pythonhosted.org/packages/28/d7/771755e57c375cb9d25a4e106f570607fd856e2cb91b02418db1db954796/librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f", size = 98580, upload-time = "2025-11-29T14:01:39.459Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ec/8b157eb8fbc066339a2f34b0aceb2028097d0ed6150a52e23284a311eafe/librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0", size = 101706, upload-time = "2025-11-29T14:01:40.474Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/4aaead9a06c795a318282aebf7d3e3e578fa889ff396e1b640c3be4c7806/librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a", size = 19465, upload-time = "2025-11-29T14:01:41.77Z" }, - { url = "https://files.pythonhosted.org/packages/3a/61/b7e6a02746c1731670c19ba07d86da90b1ae45d29e405c0b5615abf97cde/librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf", size = 21042, upload-time = "2025-11-29T14:01:42.865Z" }, - { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246, upload-time = "2025-11-29T14:01:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/8f/bc/199533d3fc04a4cda8d7776ee0d79955ab0c64c79ca079366fbc2617e680/librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab", size = 174216, upload-time = "2025-12-15T16:51:35.384Z" }, + { url = "https://files.pythonhosted.org/packages/62/ec/09239b912a45a8ed117cb4a6616d9ff508f5d3131bd84329bf2f8d6564f1/librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba", size = 189005, upload-time = "2025-12-15T16:51:36.687Z" }, + { url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/fe/35/323611e59f8fe032649b4fb7e77f746f96eb7588fcbb31af26bae9630571/librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20", size = 174818, upload-time = "2025-12-15T16:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/40fb2bb21616c6e06b6a64022802228066e9a31618f493e03f6b9661548a/librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74", size = 189607, upload-time = "2025-12-15T16:51:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" }, + { url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" }, + { url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" }, + { url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" }, + { url = "https://files.pythonhosted.org/packages/2c/08/61c41cd8f0a6a41fc99ea78a2205b88187e45ba9800792410ed62f033584/librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848", size = 172469, upload-time = "2025-12-15T16:52:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/4ee18b4d57f01444230bc18cf59103aeab8f8c0f45e84e0e540094df1df1/librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843", size = 186804, upload-time = "2025-12-15T16:52:07.192Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" }, + { url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" }, + { url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" }, + { url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ce/157db3614cf3034b3f702ae5ba4fefda4686f11eea4b7b96542324a7a0e7/librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95", size = 194606, upload-time = "2025-12-15T16:52:20.795Z" }, + { url = "https://files.pythonhosted.org/packages/30/ef/6ec4c7e3d6490f69a4fd2803516fa5334a848a4173eac26d8ee6507bff6e/librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906", size = 206776, upload-time = "2025-12-15T16:52:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" }, + { url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" }, + { url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" }, + { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, ] [[package]] @@ -152,35 +152,35 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.0" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "librt" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, - { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, - { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, - { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, - { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, - { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -251,7 +251,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.1" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -260,9 +260,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -280,28 +280,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.7" +version = "0.14.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" }, - { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" }, - { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" }, - { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" }, - { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" }, - { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" }, - { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" }, - { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" }, - { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" }, - { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" }, - { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" }, - { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" }, - { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] [[package]] @@ -350,7 +350,7 @@ wheels = [ [[package]] name = "typer" -version = "0.20.0" +version = "0.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", marker = "python_full_version >= '3.13'" }, @@ -358,9 +358,9 @@ dependencies = [ { name = "shellingham", marker = "python_full_version >= '3.13'" }, { name = "typing-extensions", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/c1/933d30fd7a123ed981e2a1eedafceab63cb379db0402e438a13bc51bbb15/typer-0.20.1.tar.gz", hash = "sha256:68585eb1b01203689c4199bc440d6be616f0851e9f0eb41e4a778845c5a0fd5b", size = 105968, upload-time = "2025-12-19T16:48:56.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/1f2df7e7d1be3d65ddc2936d820d4a3d9777a54f4204f5ca46b8513eff77/typer-0.20.1-py3-none-any.whl", hash = "sha256:4b3bde918a67c8e03d861aa02deca90a95bbac572e71b1b9be56ff49affdb5a8", size = 47381, upload-time = "2025-12-19T16:48:53.679Z" }, ] [[package]] From 049133849b6ad089e5b40ed85be5a9b862de9a83 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 01:21:16 +0100 Subject: [PATCH 49/60] build: migrate to hatchling and overhaul config - Switch build backend to Hatchling with versioningit - Rename package to `vsengine-jet` and update metadata - Add CI workflow for linting and type checking using uv - Update Ruff configuration and enable additional rules - Update README.md with new installation instructions --- .editorconfig | 8 ---- .github/workflows/lint.yml | 57 +++++++++++++++++++++++++++ .gitignore | 3 ++ README.md | 52 ++++--------------------- pyproject.toml | 80 ++++++++++++++++++++++++++++++++------ 5 files changed, 135 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/lint.yml diff --git a/.editorconfig b/.editorconfig index 54e8379..b2d671a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,16 +1,8 @@ root = true [*] -end_of_line = lf insert_final_newline = true -[*.{py,pyi,nix}] -charset = utf-8 - [*.{py,pyi}] indent_style = space indent_size = 4 - -[*.nix] -indent_size = space -indent_size = 2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..d205c62 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,57 @@ +name: Lint + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +env: + PYTHON_VERSION: 3.14 + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ env.PYTHON_VERSION }} + enable-cache: true + + - name: Install dependencies + run: | + uv sync --locked --all-extras --no-install-package vapoursynth + + - name: Run Ruff check + id: ruff-check + run: uv run --no-sync ruff check . + + - name: Run Ruff format + if: success() || (failure() && steps.ruff-check.conclusion == 'failure') + run: uv run --no-sync ruff format --check --diff + + mypy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ env.PYTHON_VERSION }} + enable-cache: true + + - name: Install dependencies + run: uv sync --locked --all-extras --no-install-package vapoursynth + + - name: Running mypy + run: uv run --no-sync mypy . diff --git a/.gitignore b/.gitignore index 678b3f6..d0d4bb3 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,6 @@ result-* # vsjet folder .vsjet + +# versioningit +vsengine/_version.py \ No newline at end of file diff --git a/README.md b/README.md index a939a25..b835aff 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,17 @@ -vs-engine -========= +# vs-engine An engine for vapoursynth previewers, renderers and script analyis tools. -Installing ----------- +## Installation -``` -pip install vsengine +``` +pip install vsengine-jet ``` -The latest development version can be downloaded from the github-actions tab. -Install the included .whl-file. - -Using vsengine --------------- +## Using vsengine Look at this example: + ```py import vapoursynth as vs from vsengine.vpy import script @@ -25,41 +20,8 @@ script("/script/to/my.vpy").result() vs.get_output(0).output() ``` -Development ------------ - -Install the dependencies listed in `pyproject.toml` as well as `flit`. - -For convenience, -the included nix-flake contains dev-shells with different python and vapoursynth versions preconfigured. - -Running Tests -------------- - -You can run tests with this command: - -``` -python -m unittest discover -s ./tests -``` - -For users with Nix installed, -the included flake contains tests for specific vs and python versions. -These can be run by running `nix flake check`. - -Contributing ------------- - -Users might want to bring their own versions of vapoursynth related plugins and libraries. -Depending on any of them would thus be of disservice to the user. -This is the reason why depending on any plugin or library is banned in this project. -The only exception is when this dependency is optional, -meaning that the feature in question does not lose any functionality when the dependency is missing. -In any case, -the addition of new dependencies (optional or otherwise) must be coordinated with the maintainer prior to filing a PR. +## Contributing This project is licensed under the EUPL-1.2. When contributing to this project you accept that your code will be using this license. By contributing you also accept any relicencing to newer versions of the EUPL at a later point in time. - -Your commits have to be signed with a key registered with GitHub.com at the time of the merge. - diff --git a/pyproject.toml b/pyproject.toml index 1425e10..fc1fca6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,40 @@ +[build-system] +requires = ["hatchling>=1.27.0", "versioningit"] +build-backend = "hatchling.build" + [project] -name = "vsengine" -version = "0.2.0+jet" -license = { file = "COPYING" } +name = "vsengine-jet" +description = "An engine for vapoursynth previewers, renderers and script analyis tools." readme = "README.md" -authors = [{ name = "cid-chan", email = "cid+git@cid-chan.moe" }] -dynamic = ["description"] requires-python = ">=3.12" - +license = { file = "COPYING" } +authors = [{ name = "cid-chan", email = "cid+git@cid-chan.moe" }] +maintainers = [ + { name = "Vardë", email = "ichunjo.le.terrible@gmail.com" }, + { name = "Jaded Encoding Thaumaturgy", email = "jaded.encoding.thaumaturgy@gmail.com" }, +] +classifiers = [ + "Topic :: Multimedia :: Graphics", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)", + "Operating System :: OS Independent", + "Typing :: Typed", +] dependencies = ["vapoursynth>=69"] +dynamic = ["version"] + +[project.urls] +"Source Code" = "https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine" +"Contact" = "https://discord.gg/XTpc6Fa9eB" [project.optional-dependencies] trio = ["trio"] -test = ["pytest"] [dependency-groups] dev = [ @@ -21,9 +44,30 @@ dev = [ "trio", "vsstubs ; python_version>='3.13'", ] -[build-system] -requires = ["flit_core >=3.2,<4"] -build-backend = "flit_core.buildapi" + +[tool.hatch.version] +source = "versioningit" +default-version = "0.0.0+unknown" +next-version = "minor" +write = { file = "vsengine/_version.py", template = "__version__ = \"{normalized_version}\"\n__version_tuple__ = {version_tuple}" } + +[tool.hatch.version.format] +distance = "{next_version}.dev{distance}+{vcs}{rev}" +dirty = "{next_version}+dirty" +distance-dirty = "{next_version}.dev{distance}+{vcs}{rev}.dirty" + +[tool.hatch.build] +artifacts = ["vsengine/_version.py"] + +[tool.hatch.build.targets.wheel] +packages = ["vsengine"] +exclude = ["tests"] + +[tool.hatch.build.targets.sdist] +include = ["vsengine", "tests"] + +[tool.pytest.ini_options] +pythonpath = "." [tool.mypy] mypy_path = "$MYPY_CONFIG_FILE_DIR/stubs" @@ -50,13 +94,25 @@ error_summary = true [tool.ruff] line-length = 120 -# extend-exclude = ["stubs/vapoursynth/__init__.pyi"] [tool.ruff.format] docstring-code-format = true [tool.ruff.lint] -extend-select = ["E", "C4", "I", "PYI", "Q", "SIM", "N", "W", "RUF"] +extend-select = [ + "E", + "C4", + "I", + "PYI", + "Q", + "SIM", + "N", + "W", + "UP", + "FURB", + "RUF", +] + [tool.ruff.lint.per-file-ignores] "__init__.*" = ["F401", "F403"] From f5884de623bac278fc8b614691238bf491d85696 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 01:24:54 +0100 Subject: [PATCH 50/60] hospice: explain refcount threshold logic --- vsengine/_hospice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vsengine/_hospice.py b/vsengine/_hospice.py index e9c97be..b58f3f3 100644 --- a/vsengine/_hospice.py +++ b/vsengine/_hospice.py @@ -68,6 +68,8 @@ def unfreeze() -> None: def _is_core_still_used(ident: int) -> bool: + # There has to be the Core, CoreTimings and the temporary reference as an argument to getrefcount + # https://docs.python.org/3/library/sys.html#sys.getrefcount return sys.getrefcount(cores[ident]) > 3 From 6b1fbad1c8877c2dcc1aaaa7910bb631c76fcba0 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 01:43:49 +0100 Subject: [PATCH 51/60] expose submodules --- uv.lock | 9 ++------- vsengine/__init__.py | 14 ++++++++++++++ vsengine/video.py | 4 +++- vsengine/vpy.py | 2 +- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/uv.lock b/uv.lock index 8c72cb7..2b69b38 100644 --- a/uv.lock +++ b/uv.lock @@ -382,17 +382,13 @@ wheels = [ ] [[package]] -name = "vsengine" -version = "0.2.0+jet" +name = "vsengine-jet" source = { editable = "." } dependencies = [ { name = "vapoursynth" }, ] [package.optional-dependencies] -test = [ - { name = "pytest" }, -] trio = [ { name = "trio" }, ] @@ -408,11 +404,10 @@ dev = [ [package.metadata] requires-dist = [ - { name = "pytest", marker = "extra == 'test'" }, { name = "trio", marker = "extra == 'trio'" }, { name = "vapoursynth", specifier = ">=69" }, ] -provides-extras = ["trio", "test"] +provides-extras = ["trio"] [package.metadata.requires-dev] dev = [ diff --git a/vsengine/__init__.py b/vsengine/__init__.py index ff37f2e..0038ce8 100644 --- a/vsengine/__init__.py +++ b/vsengine/__init__.py @@ -12,3 +12,17 @@ - video: Get frames or render the video. Sans-IO and memory safe. - vpy: Run .vpy-scripts in your application. """ + +from vsengine.loops import * +from vsengine.policy import * +from vsengine.video import * +from vsengine.vpy import * + +__version__: str +__version_tuple__: tuple[int | str, ...] + +try: + from ._version import __version__, __version_tuple__ +except ImportError: + __version__ = "0.0.0+unknown" + __version_tuple__ = (0, 0, 0, "+unknown") diff --git a/vsengine/video.py b/vsengine/video.py index e84efb6..047680e 100644 --- a/vsengine/video.py +++ b/vsengine/video.py @@ -17,6 +17,8 @@ from vsengine._nodes import buffer_futures, close_when_needed from vsengine.policy import ManagedEnvironment +__all__ = ["frame", "frames", "planes", "render"] + @unified(kind="future") def frame( @@ -150,7 +152,7 @@ def render( y4mformat = "C" + y4mformat + " " - data = "YUV4MPEG2 {y4mformat}W{width} H{height} F{fps_num}:{fps_den} Ip A0:0 XLENGTH={length}\n".format( + data = "YUV4MPEG2 {y4mformat}W{width} H{height} F{fps_num}:{fps_den} Ip A0:0 XLENGTH={length}\n".format( # noqa: UP032 y4mformat=y4mformat, width=node.width, height=node.height, diff --git a/vsengine/vpy.py b/vsengine/vpy.py index e0046ad..2efb17f 100644 --- a/vsengine/vpy.py +++ b/vsengine/vpy.py @@ -52,7 +52,7 @@ from .loops import make_awaitable, to_thread from .policy import ManagedEnvironment, Policy -__all__ = ["ExecutionError", "load_code", "load_script"] +__all__ = ["ExecutionError", "Script", "load_code", "load_script"] type Runner[R] = Callable[[Callable[[], R]], Future[R]] type Executor[T] = Callable[[WrapAllErrors, ModuleType], T] From 71cd412f6e51b7ad1ee774dcb95e84f9811cfde1 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 02:42:15 +0100 Subject: [PATCH 52/60] test: remove conversion tests and test wrappers --- tests/fixtures/unittest_core_in_module.py | 10 -- .../fixtures/unittest_core_stored_in_test.py | 10 -- tests/fixtures/unittest_core_succeeds.py | 8 -- tests/test_convert.py | 98 ------------------- tests/test_tests_pytest.py | 40 -------- tests/test_tests_unittest.py | 40 -------- 6 files changed, 206 deletions(-) delete mode 100644 tests/fixtures/unittest_core_in_module.py delete mode 100644 tests/fixtures/unittest_core_stored_in_test.py delete mode 100644 tests/fixtures/unittest_core_succeeds.py delete mode 100644 tests/test_convert.py delete mode 100644 tests/test_tests_pytest.py delete mode 100644 tests/test_tests_unittest.py diff --git a/tests/fixtures/unittest_core_in_module.py b/tests/fixtures/unittest_core_in_module.py deleted file mode 100644 index 1059c51..0000000 --- a/tests/fixtures/unittest_core_in_module.py +++ /dev/null @@ -1,10 +0,0 @@ -import unittest - -from vapoursynth import core - -core.std.BlankClip - - -class TestCoreInModule(unittest.TestCase): - def test_something(self): - raise RuntimeError("We should not even get here.") diff --git a/tests/fixtures/unittest_core_stored_in_test.py b/tests/fixtures/unittest_core_stored_in_test.py deleted file mode 100644 index fb3cb2f..0000000 --- a/tests/fixtures/unittest_core_stored_in_test.py +++ /dev/null @@ -1,10 +0,0 @@ -import unittest - -from vapoursynth import core - -atom = [None] - - -class TestCoreStoredLongTerm(unittest.TestCase): - def test_something(self): - atom[0] = core.std.BlankClip diff --git a/tests/fixtures/unittest_core_succeeds.py b/tests/fixtures/unittest_core_succeeds.py deleted file mode 100644 index 8b55544..0000000 --- a/tests/fixtures/unittest_core_succeeds.py +++ /dev/null @@ -1,8 +0,0 @@ -import unittest - -from vapoursynth import core - - -class TestCoreSucceeds(unittest.TestCase): - def test_something(self): - core.std.BlankClip().get_frame(0) diff --git a/tests/test_convert.py b/tests/test_convert.py deleted file mode 100644 index 9d1ebcc..0000000 --- a/tests/test_convert.py +++ /dev/null @@ -1,98 +0,0 @@ -import json -import os -import unittest - -import vapoursynth as vs -from vapoursynth import core - -from tests._testutils import forcefully_unregister_policy, use_standalone_policy -from vsengine.convert import to_rgb, yuv_heuristic - -DIR = os.path.dirname(__file__) -# Generated with -# mediainfo -Output=JOSN -Full [Filenames] -# | jq '.media.track[] | select(."@type" == "Video") | {matrix: .matrix_coefficients, width: .Width, height: .Height, primaries: .colour_primaries, transfer: .transfer_characteristics, chromaloc: .ChromaSubsampling_Position} | select(.matrix)' | jq -s -# -# or (if the previous jq command does not work) -# -# mediainfo -Output=JOSN -Full [Filenames] -# | jq '.[].media.track[] | select(."@type" == "Video") | {matrix: .matrix_coefficients, width: .Width, height: .Height, primaries: .colour_primaries, transfer: .transfer_characteristics, chromaloc: .ChromaSubsampling_Position} | select(.matrix)' | jq -s -PATH = os.path.join(DIR, "fixtures", "heuristic_examples.json") -with open(PATH) as h: - HEURISTIC_EXAMPLES = json.load(h) - -MATRIX_MAPPING = {"BT.2020 non-constant": "2020ncl", "BT.709": "709", "BT.470 System B/G": "470bg", "BT.601": "170m"} -TRANSFER_MAPPING = {"PQ": "st2084", "BT.709": "709", "BT.470 System B/G": "470bg", "BT.601": "601"} -PRIMARIES_MAPPING = {"BT.2020": "2020", "BT.709": "709", "BT.601 PAL": "470bg", "BT.601 NTSC": "170m"} -CHROMALOC_MAPPING = {None: "left", "Type 2": "top_left"} - - -class TestToRGB(unittest.TestCase): - def setUp(self) -> None: - forcefully_unregister_policy() - use_standalone_policy() - - def tearDown(self) -> None: - forcefully_unregister_policy() - - def test_heuristics_provides_all_arguments(self) -> None: - yuv = core.std.BlankClip(format=vs.YUV420P8) - - def _pseudo_scaler(c, **args): - self.assertTrue("chromaloc_in_s" in args) - self.assertTrue("range_in_s" in args) - self.assertTrue("transfer_in_s" in args) - self.assertTrue("primaries_in_s" in args) - self.assertTrue("matrix_in_s" in args) - return core.resize.Point(c, **args) - - to_rgb(yuv, scaler=_pseudo_scaler) - - def test_heuristics_with_examples(self) -> None: - count_hits = 0 - count_misses = 0 - - for example in HEURISTIC_EXAMPLES: - w = int(example["width"]) - h = int(example["height"]) - - result = yuv_heuristic(w, h) - raw_primary = result["primaries_in_s"] - raw_transfer = result["transfer_in_s"] - raw_matrix = result["matrix_in_s"] - raw_chromaloc = result["chromaloc_in_s"] - - if ( - raw_primary != PRIMARIES_MAPPING[example["primaries"]] - or raw_transfer != TRANSFER_MAPPING[example["transfer"]] - or raw_matrix != MATRIX_MAPPING[example["matrix"]] - or raw_chromaloc != CHROMALOC_MAPPING[example["chromaloc"]] - ): - count_misses += 1 - else: - count_hits += 1 - - self.assertGreaterEqual(count_hits, count_misses) - - def test_converts_to_rgb24(self) -> None: - # Should be sufficiently untagged. lel - yuv8 = core.std.BlankClip(format=vs.YUV420P8) - gray = core.std.BlankClip(format=vs.GRAY8) - rgb = core.std.BlankClip(format=vs.RGB24) - - yuv16 = core.std.BlankClip(format=vs.YUV420P16) - - for clip in [yuv8, gray, rgb, yuv16]: - self.assertEqual(int(to_rgb(clip).format), vs.RGB24) - self.assertEqual(int(to_rgb(clip, bits_per_sample=16).format), vs.RGB48) - - def test_supports_float(self) -> None: - # Test regression: Floating images cannot be shown. - yuv_half = core.std.BlankClip(format=vs.YUV444PH) - yuv_single = core.std.BlankClip(format=vs.YUV444PS) - rgb_half = core.std.BlankClip(format=vs.RGBH) - rgb_single = core.std.BlankClip(format=vs.RGBS) - - for clip in [yuv_half, yuv_single, rgb_half, rgb_single]: - self.assertEqual(int(to_rgb(clip).format), vs.RGB24) - self.assertEqual(int(to_rgb(clip, bits_per_sample=16).format), vs.RGB48) diff --git a/tests/test_tests_pytest.py b/tests/test_tests_pytest.py deleted file mode 100644 index e5ffdf5..0000000 --- a/tests/test_tests_pytest.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import subprocess -import sys -import unittest - -DIR = os.path.dirname(__file__) -PATH = os.path.join(DIR, "fixtures") - - -def run_fixture(fixture: str, expect_status: int = 0): - path = os.path.join(PATH) - if "PYTHONPATH" in os.environ: - path += os.pathsep + os.environ["PYTHONPATH"] - else: - path += os.pathsep + os.path.abspath(os.path.join("..")) - - env = {**os.environ, "PYTHONPATH": path} - - process = subprocess.run( - [sys.executable, "-m", "pytest", os.path.join(PATH, f"{fixture}.py"), "-o", "cache_dir=/build/.cache"], - stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, - env=env, - ) - if process.returncode != expect_status: - print() - print(process.stdout.decode(sys.getdefaultencoding()), file=sys.stderr) - print() - assert False, f"Process exited with status {process.returncode}" - - -class TestUnittestWrapper(unittest.TestCase): - def test_core_in_module(self): - run_fixture("pytest_core_in_module", 2) - - def test_stored_in_test(self): - run_fixture("pytest_core_stored_in_test", 1) - - def test_succeeds(self): - run_fixture("pytest_core_succeeds", 0) diff --git a/tests/test_tests_unittest.py b/tests/test_tests_unittest.py deleted file mode 100644 index b48f430..0000000 --- a/tests/test_tests_unittest.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import subprocess -import sys -import unittest - -DIR = os.path.dirname(__file__) -PATH = os.path.join(DIR, "fixtures") - - -def run_fixture(fixture: str, expect_status: int = 0): - path = os.path.join(PATH) - if "PYTHONPATH" in os.environ: - path += os.pathsep + os.environ["PYTHONPATH"] - else: - path += os.pathsep + os.path.abspath(os.path.join("..")) - - env = {**os.environ, "PYTHONPATH": path} - - process = subprocess.run( - [sys.executable, "-m", "vsengine.tests.unittest", fixture], - stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, - env=env, - ) - if process.returncode != expect_status: - print() - print(process.stdout.decode(sys.getdefaultencoding()), file=sys.stderr) - print() - assert False, f"Process exited with status {process.returncode}" - - -class TestUnittestWrapper(unittest.TestCase): - def test_core_in_module(self): - run_fixture("unittest_core_in_module", 1) - - def test_stored_in_test(self): - run_fixture("unittest_core_stored_in_test", 2) - - def test_succeeds(self): - run_fixture("unittest_core_succeeds", 0) From 3faf218dd02291de465fe894b808d389aa4b7c7b Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 02:46:53 +0100 Subject: [PATCH 53/60] test: remove obsolete vapoursynth core fixtures --- tests/fixtures/pytest_core_in_module.py | 14 -------------- tests/fixtures/pytest_core_stored_in_test.py | 7 ------- tests/fixtures/pytest_core_succeeds.py | 5 ----- 3 files changed, 26 deletions(-) delete mode 100644 tests/fixtures/pytest_core_in_module.py delete mode 100644 tests/fixtures/pytest_core_stored_in_test.py delete mode 100644 tests/fixtures/pytest_core_succeeds.py diff --git a/tests/fixtures/pytest_core_in_module.py b/tests/fixtures/pytest_core_in_module.py deleted file mode 100644 index d45fb6d..0000000 --- a/tests/fixtures/pytest_core_in_module.py +++ /dev/null @@ -1,14 +0,0 @@ -from vapoursynth import core - -clip = core.std.BlankClip() - - -def test_should_never_be_run(): - import os - - try: - os._exit(3) - except AttributeError: - import sys - - sys.exit(3) diff --git a/tests/fixtures/pytest_core_stored_in_test.py b/tests/fixtures/pytest_core_stored_in_test.py deleted file mode 100644 index 8fb9334..0000000 --- a/tests/fixtures/pytest_core_stored_in_test.py +++ /dev/null @@ -1,7 +0,0 @@ -from vapoursynth import core - -test = [0] - - -def test_fails_core_stored_in_text(): - test[0] = core.std.BlankClip() diff --git a/tests/fixtures/pytest_core_succeeds.py b/tests/fixtures/pytest_core_succeeds.py deleted file mode 100644 index 3198664..0000000 --- a/tests/fixtures/pytest_core_succeeds.py +++ /dev/null @@ -1,5 +0,0 @@ -from vapoursynth import core - - -def test_core_succeeds(): - core.std.BlankClip().get_frame(0) From 130d9c8e16190436561acf0e310677cfefb48534 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 03:02:22 +0100 Subject: [PATCH 54/60] test: migrate suite to pytest and add type hints Refactor the entire test suite to use pytest, removing dependencies on unittest.TestCase. Introduce pytest-asyncio for asynchronous tests and replace the custom wrapper. --- pyproject.toml | 3 + tests/_testutils.py | 49 ++-- tests/conftest.py | 31 ++ tests/fixtures/test.vpy | 4 +- tests/test_futures.py | 564 ++++++++++++++++++++----------------- tests/test_helpers.py | 94 +++---- tests/test_loops.py | 185 ++++++------ tests/test_policy.py | 142 +++++----- tests/test_policy_store.py | 94 ++++--- tests/test_video.py | 214 +++++++------- tests/test_vpy.py | 101 ++++--- uv.lock | 15 + vsengine/_futures.py | 2 +- 13 files changed, 806 insertions(+), 692 deletions(-) create mode 100644 tests/conftest.py diff --git a/pyproject.toml b/pyproject.toml index fc1fca6..20905e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ trio = ["trio"] dev = [ "mypy>=1.19.0", "pytest>=9.0.1", + "pytest-asyncio>=0.24.0", "ruff>=0.14.7", "trio", "vsstubs ; python_version>='3.13'", @@ -68,6 +69,8 @@ include = ["vsengine", "tests"] [tool.pytest.ini_options] pythonpath = "." +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" [tool.mypy] mypy_path = "$MYPY_CONFIG_FILE_DIR/stubs" diff --git a/tests/_testutils.py b/tests/_testutils.py index 959f4de..31c7b70 100644 --- a/tests/_testutils.py +++ b/tests/_testutils.py @@ -30,9 +30,10 @@ import vapoursynth as vs from vapoursynth import Core, EnvironmentData, EnvironmentPolicy, EnvironmentPolicyAPI, core +from vsengine import policy as vsengine_policy from vsengine._hospice import admit_environment -__all__ = ["BLACKBOARD", "forcefully_unregister_policy", "use_standalone_policy", "wrap_test_for_asyncio"] +__all__ = ["BLACKBOARD", "forcefully_unregister_policy", "use_standalone_policy"] BLACKBOARD = dict[Any, Any]() @@ -58,7 +59,7 @@ def attach_policy_to_proxy(self, policy: EnvironmentPolicy) -> None: self._policy = policy try: - policy.on_policy_registered(EnvironmentPolicyAPIWrapper(self._api, self)) + policy.on_policy_registered(EnvironmentPolicyAPIWrapper(self._api, self)) # type: ignore[arg-type] except: self._policy = None raise @@ -77,7 +78,9 @@ def forcefully_unregister_policy(self) -> None: def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None: self._api = special_api + # Patch both vapoursynth.register_policy and vsengine.policy.register_policy vs.register_policy = self.attach_policy_to_proxy + vsengine_policy.register_policy = self.attach_policy_to_proxy # type: ignore[attr-defined] def on_policy_cleared(self) -> None: try: @@ -87,6 +90,7 @@ def on_policy_cleared(self) -> None: self._policy = None self._api = None vs.register_policy = orig_register_policy + vsengine_policy.register_policy = orig_register_policy # type: ignore[attr-defined] def get_current_environment(self) -> EnvironmentData | None: if self._policy is None: @@ -104,15 +108,13 @@ def is_alive(self, environment: EnvironmentData) -> bool: return self._policy.is_alive(environment) -class StandalonePolicy: - _current: EnvironmentData | None - _api: EnvironmentPolicyAPI | None - _core: Core | None - __slots__ = ("_api", "_core", "_current") +class StandalonePolicy(EnvironmentPolicy): + """A simple standalone policy that uses a single environment.""" - def __init__(self) -> None: - self._current = None - self._api = None + _current: EnvironmentData + _api: EnvironmentPolicyAPI + _core: Core + __slots__ = ("_api", "_core", "_current") def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None: self._api = special_api @@ -120,14 +122,12 @@ def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None: self._core = core.core def on_policy_cleared(self) -> None: - assert self._api is not None - admit_environment(self._current, self._core) - self._current = None - self._core = None + del self._current + del self._core - def get_current_environment(self) -> EnvironmentData | None: + def get_current_environment(self) -> EnvironmentData: return self._current def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None: @@ -166,20 +166,5 @@ def unregister_policy(self) -> None: def use_standalone_policy() -> None: - _policy.attach_policy_to_proxy(StandalonePolicy()) # type: ignore - - -def wrap_test_for_asyncio(func): # type: ignore - import asyncio - - from vsengine.adapters.asyncio import AsyncIOLoop - from vsengine.loops import set_loop - - def test_case(self) -> None: # type: ignore - async def _run() -> None: - set_loop(AsyncIOLoop()) - await func(self) - - asyncio.run(_run()) - - return test_case + """Register a standalone policy for tests that don't need custom policies.""" + _policy.attach_policy_to_proxy(StandalonePolicy()) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a841eb8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,31 @@ +# vs-engine +# Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy +# This project is licensed under the EUPL-1.2 +# SPDX-License-Identifier: EUPL-1.2 +""" +Global pytest configuration and fixtures for vs-engine tests. +""" + +from collections.abc import Iterator + +import pytest + +from tests._testutils import forcefully_unregister_policy +from vsengine.loops import NO_LOOP, set_loop + + +@pytest.fixture(autouse=True) +def clean_policy() -> Iterator[None]: + """ + Global fixture that runs before and after every test. + + Ensures clean policy state by: + - Unregistering any existing policy before the test + - Unregistering any policy after the test + - Resetting the event loop to NO_LOOP after the test + """ + forcefully_unregister_policy() + yield + forcefully_unregister_policy() + set_loop(NO_LOOP) diff --git a/tests/fixtures/test.vpy b/tests/fixtures/test.vpy index 5d87a2f..b2194d4 100644 --- a/tests/fixtures/test.vpy +++ b/tests/fixtures/test.vpy @@ -1,11 +1,9 @@ -# -*- encoding: utf-8 -*- - # vs-engine # Copyright (C) 2022 cid-chan # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -from vsengine._testutils import BLACKBOARD +from tests._testutils import BLACKBOARD BLACKBOARD["vpy_run_script"] = True BLACKBOARD["vpy_run_script_name"] = __name__ diff --git a/tests/test_futures.py b/tests/test_futures.py index 0f469f2..59a444e 100644 --- a/tests/test_futures.py +++ b/tests/test_futures.py @@ -1,340 +1,378 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 +"""Tests for the unified future system.""" import contextlib import threading -import unittest +from collections.abc import AsyncIterator, Iterator from concurrent.futures import Future +from typing import Any + +import pytest -from tests._testutils import wrap_test_for_asyncio from vsengine._futures import UnifiedFuture, UnifiedIterator, unified +from vsengine.adapters.asyncio import AsyncIOLoop from vsengine.loops import NO_LOOP, set_loop -def resolve(value): - fut = Future() +def resolve(value: Any) -> Future[Any]: + fut = Future[Any]() fut.set_result(value) return fut -def reject(err): - fut = Future() +def reject(err: BaseException) -> Future[Any]: + fut = Future[Any]() fut.set_exception(err) return fut -def contextmanager(): +def contextmanager_helper() -> Future[Any]: @contextlib.contextmanager - def noop(): + def noop() -> Iterator[int]: yield 1 return resolve(noop()) -def asynccontextmanager(): +def asynccontextmanager_helper() -> Future[Any]: @contextlib.asynccontextmanager - async def noop(): + async def noop() -> AsyncIterator[int]: yield 2 return resolve(noop()) -def succeeds(): +def succeeds() -> Future[int]: return resolve(1) -def fails(): +def fails() -> Future[Any]: return reject(RuntimeError()) -def fails_early(): +def fails_early() -> Future[Any]: raise RuntimeError() -def future_iterator(): +def future_iterator() -> Iterator[Future[int]]: n = 0 while True: yield resolve(n) n += 1 -class WrappedUnifiedFuture(UnifiedFuture): - pass +class WrappedUnifiedFuture(UnifiedFuture[Any]): ... -class WrappedUnifiedIterable(UnifiedIterator): - pass +class WrappedUnifiedIterable(UnifiedIterator[Any]): ... -class TestUnifiedFuture(unittest.TestCase): - @wrap_test_for_asyncio - async def test_is_await(self): - await UnifiedFuture.from_call(succeeds) +# UnifiedFuture tests - @wrap_test_for_asyncio - async def test_awaitable(self): - await UnifiedFuture.from_call(succeeds).awaitable() - @wrap_test_for_asyncio - async def test_async_context_manager_async(self): - async with UnifiedFuture.from_call(asynccontextmanager) as v: - self.assertEqual(v, 2) +@pytest.mark.asyncio +async def test_unified_future_is_await() -> None: + set_loop(AsyncIOLoop()) + await UnifiedFuture.from_call(succeeds) - @wrap_test_for_asyncio - async def test_context_manager_async(self): - async with UnifiedFuture.from_call(contextmanager) as v: - self.assertEqual(v, 1) - def test_context_manager(self): - with UnifiedFuture.from_call(contextmanager) as v: - self.assertEqual(v, 1) +@pytest.mark.asyncio +async def test_unified_future_awaitable() -> None: + set_loop(AsyncIOLoop()) + await UnifiedFuture.from_call(succeeds).awaitable() - def test_map(self): - def _crash(v): - raise RuntimeError(str(v)) - future = UnifiedFuture.from_call(succeeds) - new_future = future.map(lambda v: str(v)) - self.assertEqual(new_future.result(), "1") +@pytest.mark.asyncio +async def test_unified_future_async_context_manager_async() -> None: + set_loop(AsyncIOLoop()) + async with UnifiedFuture.from_call(asynccontextmanager_helper) as v: + assert v == 2 - new_future = future.map(_crash) - self.assertIsInstance(new_future.exception(), RuntimeError) - future = UnifiedFuture.from_call(fails) - new_future = future.map(lambda v: str(v)) - self.assertIsInstance(new_future.exception(), RuntimeError) +@pytest.mark.asyncio +async def test_unified_future_context_manager_async() -> None: + set_loop(AsyncIOLoop()) + async with UnifiedFuture.from_call(contextmanager_helper) as v: + assert v == 1 - def test_catch(self): - def _crash(_): - raise RuntimeError("test") - future = UnifiedFuture.from_call(fails) - new_future = future.catch(lambda e: e.__class__.__name__) - self.assertEqual(new_future.result(), "RuntimeError") +def test_unified_future_context_manager() -> None: + with UnifiedFuture.from_call(contextmanager_helper) as v: + assert v == 1 - new_future = future.catch(_crash) - self.assertIsInstance(new_future.exception(), RuntimeError) - future = UnifiedFuture.from_call(succeeds) - new_future = future.catch(lambda v: str(v)) - self.assertEqual(new_future.result(), 1) +def test_unified_future_map() -> None: + def _crash(v: Any) -> str: + raise RuntimeError(str(v)) - @wrap_test_for_asyncio - async def test_add_loop_callback(self): - def _init_thread(fut): - fut.set_result(threading.current_thread()) + future = UnifiedFuture.from_call(succeeds) + new_future = future.map(lambda v: str(v)) + assert new_future.result() == "1" - fut = Future() - thr = threading.Thread(target=lambda: _init_thread(fut)) + new_future = future.map(_crash) + assert isinstance(new_future.exception(), RuntimeError) - def _wrapper(): - return fut + future = UnifiedFuture.from_call(fails) + new_future = future.map(lambda v: str(v)) + assert isinstance(new_future.exception(), RuntimeError) - fut = UnifiedFuture.from_call(_wrapper) - loop_thread = None +def test_unified_future_catch() -> None: + def _crash(_: BaseException) -> str: + raise RuntimeError("test") - def _record_loop_thr(_): - nonlocal loop_thread - loop_thread = threading.current_thread() + future = UnifiedFuture.from_call(fails) + new_future = future.catch(lambda e: e.__class__.__name__) + assert new_future.result() == "RuntimeError" - fut.add_loop_callback(_record_loop_thr) - thr.start() - cb_thread = await fut + new_future = future.catch(_crash) + assert isinstance(new_future.exception(), RuntimeError) - self.assertNotEqual(cb_thread, loop_thread) + future = UnifiedFuture.from_call(succeeds) + new_future = future.catch(lambda v: str(v)) + # Result is 1 because the future succeeded (no exception to catch) + result = new_future.result() + assert result == 1 -class UnifiedIteratorTest(unittest.TestCase): - def test_run_as_completed_succeeds(self): - set_loop(NO_LOOP) - my_futures = [Future(), Future()] - results = [] +@pytest.mark.asyncio +async def test_unified_future_add_loop_callback() -> None: + from vsengine.adapters.asyncio import AsyncIOLoop + from vsengine.loops import set_loop - def _add_to_result(f): - results.append(f.result()) + set_loop(AsyncIOLoop()) + + def _init_thread(fut: Future[threading.Thread]) -> None: + fut.set_result(threading.current_thread()) + + fut: Future[threading.Thread] = Future() + thr = threading.Thread(target=lambda: _init_thread(fut)) + + def _wrapper() -> Future[threading.Thread]: + return fut + + unified_fut = UnifiedFuture.from_call(_wrapper) + + loop_thread: threading.Thread | None = None + + def _record_loop_thr(_: Any) -> None: + nonlocal loop_thread + loop_thread = threading.current_thread() + + unified_fut.add_loop_callback(_record_loop_thr) + thr.start() + cb_thread = await unified_fut + + assert cb_thread != loop_thread + + +# UnifiedIterator tests + + +def test_unified_iterator_run_as_completed_succeeds() -> None: + set_loop(NO_LOOP) + my_futures: list[Future[int]] = [Future(), Future()] + results: list[int] = [] + + def _add_to_result(f: Future[int]) -> None: + results.append(f.result()) + + state = UnifiedIterator(iter(my_futures)).run_as_completed(_add_to_result) + assert not state.done() + my_futures[1].set_result(2) + assert not state.done() + my_futures[0].set_result(1) + assert state.done() + assert state.result() is None + assert results == [1, 2] + + +def test_unified_iterator_run_as_completed_forwards_errors() -> None: + set_loop(NO_LOOP) + my_futures: list[Future[int]] = [Future(), Future()] + results: list[int] = [] + errors: list[BaseException] = [] - state = UnifiedIterator(iter(my_futures)).run_as_completed(_add_to_result) - self.assertFalse(state.done()) - my_futures[1].set_result(2) - self.assertFalse(state.done()) - my_futures[0].set_result(1) - self.assertTrue(state.done()) - self.assertIs(state.result(), None) - self.assertEqual(results, [1, 2]) - - def test_run_as_completed_forwards_errors(self): - set_loop(NO_LOOP) - my_futures = [Future(), Future()] - results = [] - errors = [] - - def _add_to_result(f): - if exc := f.exception(): - errors.append(exc) - else: - results.append(f.result()) - - iterator = iter(my_futures) - state = UnifiedIterator(iterator).run_as_completed(_add_to_result) - self.assertFalse(state.done()) - my_futures[0].set_exception(RuntimeError()) - self.assertFalse(state.done()) - my_futures[1].set_result(2) - self.assertTrue(state.done()) - self.assertIs(state.result(), None) - - self.assertEqual(results, [2]) - self.assertEqual(len(errors), 1) - - def test_run_as_completed_cancels(self): - set_loop(NO_LOOP) - my_futures = [Future(), Future()] - results = [] - - def _add_to_result(f): + def _add_to_result(f: Future[int]) -> None: + if exc := f.exception(): + errors.append(exc) + else: results.append(f.result()) - return False - - iterator = iter(my_futures) - state = UnifiedIterator(iterator).run_as_completed(_add_to_result) - self.assertFalse(state.done()) - my_futures[0].set_result(1) - self.assertTrue(state.done()) - self.assertIs(state.result(), None) - self.assertEqual(results, [1]) - - def test_run_as_completed_cancels_on_crash(self): - set_loop(NO_LOOP) - my_futures = [Future(), Future()] - err = RuntimeError("test") - - def _crash(_): - raise err - - iterator = iter(my_futures) - state = UnifiedIterator(iterator).run_as_completed(_crash) - self.assertFalse(state.done()) - my_futures[0].set_result(1) - self.assertTrue(state.done()) - self.assertIs(state.exception(), err) - self.assertIsNotNone(next(iterator)) - - def test_run_as_completed_requests_as_needed(self): - my_futures = [Future(), Future()] - requested = [] - continued = [] - - def _add_to_result(f): - pass - - def _it(): - for fut in my_futures: - requested.append(fut) - yield fut - continued.append(fut) - - state = UnifiedIterator(_it()).run_as_completed(_add_to_result) - self.assertFalse(state.done()) - self.assertEqual(requested, [my_futures[0]]) - self.assertEqual(continued, []) - - my_futures[0].set_result(1) - self.assertFalse(state.done()) - self.assertEqual(requested, [my_futures[0], my_futures[1]]) - self.assertEqual(continued, [my_futures[0]]) - - my_futures[1].set_result(1) - self.assertTrue(state.done()) - self.assertEqual(requested, [my_futures[0], my_futures[1]]) - self.assertEqual(continued, [my_futures[0], my_futures[1]]) - - def test_run_as_completed_cancels_on_iterator_crash(self): - err = RuntimeError("test") - - def _it(): - if False: - yield Future() - raise err - - def _noop(_): - pass - - state = UnifiedIterator(_it()).run_as_completed(_noop) - self.assertTrue(state.done()) - self.assertIs(state.exception(), err) - - def test_can_iter_futures(self): - n = 0 - for fut in UnifiedIterator.from_call(future_iterator).futures: - self.assertEqual(n, fut.result()) - n += 1 - if n > 100: - break - - def test_can_iter(self): - n = 0 - for n2 in UnifiedIterator.from_call(future_iterator): - self.assertEqual(n, n2) - n += 1 - if n > 100: - break - - @wrap_test_for_asyncio - async def test_can_aiter(self): - n = 0 - async for n2 in UnifiedIterator.from_call(future_iterator): - self.assertEqual(n, n2) - n += 1 - if n > 100: - break - - -class UnifiedFunctionTest(unittest.TestCase): - def test_unified_auto_future_return_a_unified_future(self): - @unified() - def test_func(): - return resolve(9999) - - f = test_func() - self.assertIsInstance(f, UnifiedFuture) - self.assertEqual(f.result(), 9999) - - def test_unified_auto_generator_return_a_unified_iterable(self): - @unified() - def test_func(): - yield resolve(1) - yield resolve(2) - - f = test_func() - self.assertIsInstance(f, UnifiedIterator) - self.assertEqual(next(f), 1) - self.assertEqual(next(f), 2) - - def test_unified_generator_accepts_other_iterables(self): - @unified(kind="generator") - def test_func(): - return iter((resolve(1), resolve(2))) - - f = test_func() - self.assertIsInstance(f, UnifiedIterator) - self.assertEqual(next(f), 1) - self.assertEqual(next(f), 2) - - def test_unified_custom_future(self): - @unified(future_class=WrappedUnifiedFuture) - def test_func(): - return resolve(9999) - - f = test_func() - self.assertIsInstance(f, WrappedUnifiedFuture) - - def test_unified_custom_generator(self): - @unified(iterable_class=WrappedUnifiedIterable) - def test_func(): - yield resolve(9999) - - f = test_func() - self.assertIsInstance(f, WrappedUnifiedIterable) + + iterator = iter(my_futures) + state = UnifiedIterator(iterator).run_as_completed(_add_to_result) + assert not state.done() + my_futures[0].set_exception(RuntimeError()) + assert not state.done() + my_futures[1].set_result(2) + assert state.done() + assert state.result() is None + + assert results == [2] + assert len(errors) == 1 + + +def test_unified_iterator_run_as_completed_cancels() -> None: + set_loop(NO_LOOP) + my_futures: list[Future[int]] = [Future(), Future()] + results: list[int] = [] + + def _add_to_result(f: Future[int]) -> bool: + results.append(f.result()) + return False + + iterator = iter(my_futures) + state = UnifiedIterator(iterator).run_as_completed(_add_to_result) + assert not state.done() + my_futures[0].set_result(1) + assert state.done() + assert state.result() is None + assert results == [1] + + +def test_unified_iterator_run_as_completed_cancels_on_crash() -> None: + set_loop(NO_LOOP) + my_futures: list[Future[int]] = [Future(), Future()] + err = RuntimeError("test") + + def _crash(_: Future[int]) -> None: + raise err + + iterator = iter(my_futures) + state = UnifiedIterator(iterator).run_as_completed(_crash) + assert not state.done() + my_futures[0].set_result(1) + assert state.done() + assert state.exception() is err + assert next(iterator) is not None + + +def test_unified_iterator_run_as_completed_requests_as_needed() -> None: + my_futures: list[Future[int]] = [Future(), Future()] + requested: list[Future[int]] = [] + continued: list[Future[int]] = [] + + def _add_to_result(f: Future[int]) -> None: + pass + + def _it() -> Iterator[Future[int]]: + for fut in my_futures: + requested.append(fut) + yield fut + continued.append(fut) + + state = UnifiedIterator(_it()).run_as_completed(_add_to_result) + assert not state.done() + assert requested == [my_futures[0]] + assert continued == [] + + my_futures[0].set_result(1) + assert not state.done() + assert requested == [my_futures[0], my_futures[1]] + assert continued == [my_futures[0]] + + my_futures[1].set_result(1) + assert state.done() + assert requested == [my_futures[0], my_futures[1]] + assert continued == [my_futures[0], my_futures[1]] + + +def test_unified_iterator_run_as_completed_cancels_on_iterator_crash() -> None: + err = RuntimeError("test") + + def _it() -> Iterator[Future[int]]: + raise err + + def _noop(_: Future[int]) -> None: + pass + + state = UnifiedIterator(_it()).run_as_completed(_noop) + assert state.done() + assert state.exception() is err + + +def test_unified_iterator_can_iter_futures() -> None: + n = 0 + for fut in UnifiedIterator.from_call(future_iterator).futures: + assert n == fut.result() + n += 1 + if n > 100: + break + + +def test_unified_iterator_can_iter() -> None: + n = 0 + for n2 in UnifiedIterator.from_call(future_iterator): + assert n == n2 + n += 1 + if n > 100: + break + + +@pytest.mark.asyncio +async def test_unified_iterator_can_aiter() -> None: + set_loop(AsyncIOLoop()) + n = 0 + async for n2 in UnifiedIterator.from_call(future_iterator): + assert n == n2 + n += 1 + if n > 100: + break + + +# unified decorator tests + + +def test_unified_auto_future_return_a_unified_future() -> None: + @unified() + def test_func() -> Future[int]: + return resolve(9999) + + f = test_func() + assert isinstance(f, UnifiedFuture) + assert f.result() == 9999 + + +def test_unified_auto_generator_return_a_unified_iterable() -> None: + @unified() + def test_func() -> Iterator[Future[int]]: + yield resolve(1) + yield resolve(2) + + f = test_func() + assert isinstance(f, UnifiedIterator) + assert next(f) == 1 + assert next(f) == 2 + + +def test_unified_generator_accepts_other_iterables() -> None: + @unified(kind="generator") + def test_func() -> Iterator[Future[int]]: + return iter((resolve(1), resolve(2))) + + f = test_func() + assert isinstance(f, UnifiedIterator) + assert next(f) == 1 + assert next(f) == 2 + + +def test_unified_custom_future() -> None: + @unified(future_class=WrappedUnifiedFuture) + def test_func() -> Future[int]: + return resolve(9999) + + f = test_func() + assert isinstance(f, WrappedUnifiedFuture) + + +def test_unified_custom_generator() -> None: + @unified(iterable_class=WrappedUnifiedIterable) + def test_func() -> Iterator[Future[int]]: + yield resolve(9999) + + f = test_func() + assert isinstance(f, WrappedUnifiedIterable) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3a797a6..ce7ed80 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,55 +1,55 @@ -from collections.abc import Iterator +# vs-engine +# Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy +# This project is licensed under the EUPL-1.2 +# SPDX-License-Identifier: EUPL-1.2 import pytest import vapoursynth as vs -from tests._testutils import forcefully_unregister_policy, use_standalone_policy +from tests._testutils import use_standalone_policy from vsengine._helpers import use_inline from vsengine.policy import GlobalStore, Policy -@pytest.fixture(autouse=True) -def clean_policy() -> Iterator[None]: - forcefully_unregister_policy() - yield - forcefully_unregister_policy() - - -class TestUseInline: - def test_with_standalone(self) -> None: - use_standalone_policy() - with use_inline("test_with_standalone", None): - pass - - def test_with_set_environment(self) -> None: - with ( - Policy(GlobalStore()) as p, - p.new_environment() as env, - env.use(), - use_inline("test_with_set_environment", None), - ): - pass - - def test_fails_without_an_environment(self) -> None: - with ( - Policy(GlobalStore()), - pytest.raises(OSError), - use_inline("test_fails_without_an_environment", None), - ): - pass - - def test_accepts_a_managed_environment(self) -> None: - with ( - Policy(GlobalStore()) as p, - p.new_environment() as env, - use_inline("test_accepts_a_managed_environment", env), - ): - assert env.vs_environment == vs.get_current_environment() - - def test_accepts_a_standard_environment(self) -> None: - with ( - Policy(GlobalStore()) as p, - p.new_environment() as env, - use_inline("test_accepts_a_standard_environment", env.vs_environment), - ): - assert env.vs_environment == vs.get_current_environment() +def test_use_inline_with_standalone() -> None: + use_standalone_policy() + with use_inline("test_with_standalone", None): + pass + + +def test_use_inline_with_set_environment() -> None: + with ( + Policy(GlobalStore()) as p, + p.new_environment() as env, + env.use(), + use_inline("test_with_set_environment", None), + ): + pass + + +def test_use_inline_fails_without_an_environment() -> None: + with ( + Policy(GlobalStore()), + pytest.raises(OSError), + use_inline("test_fails_without_an_environment", None), + ): + pass + + +def test_use_inline_accepts_a_managed_environment() -> None: + with ( + Policy(GlobalStore()) as p, + p.new_environment() as env, + use_inline("test_accepts_a_managed_environment", env), + ): + assert env.vs_environment == vs.get_current_environment() + + +def test_use_inline_accepts_a_standard_environment() -> None: + with ( + Policy(GlobalStore()) as p, + p.new_environment() as env, + use_inline("test_accepts_a_standard_environment", env.vs_environment), + ): + assert env.vs_environment == vs.get_current_environment() diff --git a/tests/test_loops.py b/tests/test_loops.py index aefcdef..d870724 100644 --- a/tests/test_loops.py +++ b/tests/test_loops.py @@ -1,44 +1,49 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 +"""Tests for the event loop API.""" +import contextlib import queue import threading -import unittest from concurrent.futures import CancelledError, Future +from typing import Any, NoReturn +import pytest import vapoursynth -from tests._testutils import forcefully_unregister_policy from vsengine.loops import Cancelled, EventLoop, _NoEventLoop, from_thread, get_loop, set_loop, to_thread from vsengine.policy import Policy, ThreadLocalStore class FailingEventLoop: - def attach(self): + """Event loop that fails on attach.""" + + def attach(self) -> NoReturn: raise RuntimeError() class SomeOtherLoop: - def attach(self): - pass + """A simple event loop for testing.""" - def detach(self): - pass + def attach(self) -> None: ... + + def detach(self) -> None: ... class SpinLoop(EventLoop): + """A spin-based event loop for testing.""" + def __init__(self) -> None: - self.queue = queue.Queue() + self.queue = queue.Queue[tuple[Future[Any], Any, tuple[Any, ...], dict[str, Any]] | None]() - def attach(self) -> None: - pass + def attach(self) -> None: ... - def detach(self) -> None: - pass + def detach(self) -> None: ... - def run(self): + def run(self) -> None: while (value := self.queue.get()) is not None: future, func, args, kwargs = value try: @@ -48,91 +53,93 @@ def run(self): else: future.set_result(result) - def stop(self): + def stop(self) -> None: self.queue.put(None) - def from_thread(self, func, *args, **kwargs): - fut = Future() + def from_thread(self, func: Any, *args: Any, **kwargs: Any) -> Future[Any]: + fut = Future[Any]() self.queue.put((fut, func, args, kwargs)) return fut -class NoLoopTest(unittest.TestCase): - def test_wrap_cancelled_converts_the_exception(self) -> None: - loop = _NoEventLoop() - with self.assertRaises(CancelledError), loop.wrap_cancelled(): - raise Cancelled +# NoLoop tests -class LoopApiTest(unittest.TestCase): - def tearDown(self) -> None: - forcefully_unregister_policy() +def test_no_loop_wrap_cancelled_converts_the_exception() -> None: + loop = _NoEventLoop() + with pytest.raises(CancelledError), loop.wrap_cancelled(): + raise Cancelled - def test_loop_can_override(self): - loop = _NoEventLoop() - set_loop(loop) - self.assertIs(get_loop(), loop) - def test_loop_reverts_to_no_on_error(self): - try: - set_loop(SomeOtherLoop()) - loop = FailingEventLoop() - try: - set_loop(loop) - except RuntimeError: - pass - - self.assertIsInstance(get_loop(), _NoEventLoop) - finally: - set_loop(_NoEventLoop()) - - def test_loop_from_thread_retains_environment(self): - loop = SpinLoop() - set_loop(loop) - thr = threading.Thread(target=loop.run) - thr.start() - - def test(): - return vapoursynth.get_current_environment() - - try: - with Policy(ThreadLocalStore()) as p, p.new_environment() as env1: - with env1.use(): - fut = from_thread(test) - self.assertEqual(fut.result(timeout=0.1), env1.vs_environment) - finally: - loop.stop() - thr.join() - set_loop(_NoEventLoop()) - - def test_loop_from_thread_does_not_require_environment(self): - loop = SpinLoop() - set_loop(loop) - thr = threading.Thread(target=loop.run) - thr.start() - - def test(): - pass - - try: - from_thread(test).result(timeout=0.1) - finally: - loop.stop() - thr.join() - set_loop(_NoEventLoop()) - - def test_loop_to_thread_retains_environment(self): - def test(): - return vapoursynth.get_current_environment() - - with Policy(ThreadLocalStore()) as p, p.new_environment() as env1: - with env1.use(): - fut = to_thread(test) - self.assertEqual(fut.result(timeout=0.1), env1.vs_environment) - - def test_loop_to_thread_does_not_require_environment(self): - def test(): - pass +# Loop API tests + + +def test_loop_can_override() -> None: + loop = _NoEventLoop() + set_loop(loop) + assert get_loop() is loop + + +def test_loop_reverts_to_no_on_error() -> None: + try: + set_loop(SomeOtherLoop()) # type: ignore[arg-type] + loop = FailingEventLoop() + with contextlib.suppress(RuntimeError): + set_loop(loop) # type: ignore[arg-type] + + assert isinstance(get_loop(), _NoEventLoop) + finally: + set_loop(_NoEventLoop()) + +def test_loop_from_thread_retains_environment() -> None: + loop = SpinLoop() + set_loop(loop) + thr = threading.Thread(target=loop.run) + thr.start() + + def test() -> vapoursynth.Environment: + return vapoursynth.get_current_environment() + + try: + with Policy(ThreadLocalStore()) as p, p.new_environment() as env1, env1.use(): + fut = from_thread(test) + assert fut.result(timeout=0.1) == env1.vs_environment + finally: + loop.stop() + thr.join() + set_loop(_NoEventLoop()) + + +def test_loop_from_thread_does_not_require_environment() -> None: + loop = SpinLoop() + set_loop(loop) + thr = threading.Thread(target=loop.run) + thr.start() + + def test() -> None: + pass + + try: + from_thread(test).result(timeout=0.1) + finally: + loop.stop() + thr.join() + set_loop(_NoEventLoop()) + + +def test_loop_to_thread_retains_environment() -> None: + def test() -> vapoursynth.Environment: + return vapoursynth.get_current_environment() + + with Policy(ThreadLocalStore()) as p, p.new_environment() as env1, env1.use(): fut = to_thread(test) - fut.result(timeout=0.1) + assert fut.result(timeout=0.1) == env1.vs_environment + + +def test_loop_to_thread_does_not_require_environment() -> None: + def test() -> None: + pass + + fut = to_thread(test) + fut.result(timeout=0.1) diff --git a/tests/test_policy.py b/tests/test_policy.py index d30bc7c..731ee83 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -1,116 +1,130 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import unittest +"""Tests for the policy system.""" +import contextlib +from collections.abc import Iterator + +import pytest import vapoursynth -from tests._testutils import forcefully_unregister_policy from vsengine.policy import GlobalStore, Policy -class PolicyTest(unittest.TestCase): - def setUp(self) -> None: - forcefully_unregister_policy() - self.policy = Policy(GlobalStore()) +@pytest.fixture +def policy() -> Iterator[Policy]: + """Fixture that provides a fresh Policy instance.""" + p = Policy(GlobalStore()) + yield p + with contextlib.suppress(RuntimeError): + p.unregister() + - def tearDown(self) -> None: - forcefully_unregister_policy() +class TestPolicy: + """Tests for basic Policy functionality.""" - def test_register(self): - self.policy.register() + def test_register(self, policy: Policy) -> None: + policy.register() try: - self.assertIsNotNone(self.policy.api) + assert policy.api is not None finally: - self.policy.unregister() + policy.unregister() - def test_unregister(self): - self.policy.register() - self.policy.unregister() + def test_unregister(self, policy: Policy) -> None: + policy.register() + policy.unregister() - with self.assertRaises(RuntimeError): - self.policy.api.create_environment() + with pytest.raises(RuntimeError): + policy.api.create_environment() - def test_context_manager(self): - with self.policy: - self.policy.api.create_environment() + def test_context_manager(self, policy: Policy) -> None: + with policy: + policy.api.create_environment() - with self.assertRaises(RuntimeError): - self.policy.api.create_environment() + with pytest.raises(RuntimeError): + policy.api.create_environment() - def test_context_manager_on_error(self): + def test_context_manager_on_error(self, policy: Policy) -> None: try: - with self.policy: + with policy: raise RuntimeError() except RuntimeError: pass - self.assertRaises(RuntimeError, lambda: self.policy.api.create_environment()) + with pytest.raises(RuntimeError): + policy.api.create_environment() - try: - self.policy.unregister() - except: - pass +class TestManagedEnvironment: + """Tests for ManagedEnvironment functionality.""" -class ManagedEnvironmentTest(unittest.TestCase): - def setUp(self) -> None: - forcefully_unregister_policy() - self.store = GlobalStore() - self.policy = Policy(self.store) - self.policy.register() + @pytest.fixture + def store(self) -> GlobalStore: + return GlobalStore() - def tearDown(self) -> None: - self.policy.unregister() + @pytest.fixture + def registered_policy(self, store: GlobalStore) -> Iterator[Policy]: + """Fixture that provides a registered Policy.""" + p = Policy(store) + p.register() + yield p + with contextlib.suppress(RuntimeError): + p.unregister() - def test_new_environment_warns_on_del(self): - env = self.policy.new_environment() - with self.assertWarns(ResourceWarning): + def test_new_environment_warns_on_del(self, registered_policy: Policy) -> None: + env = registered_policy.new_environment() + with pytest.warns(ResourceWarning): del env - def test_new_environment_can_dispose(self): - env = self.policy.new_environment() + def test_new_environment_can_dispose(self, registered_policy: Policy) -> None: + env = registered_policy.new_environment() env.dispose() - self.assertRaises(RuntimeError, lambda: env.use().__enter__()) + with pytest.raises(RuntimeError), env.use(): + pass - def test_new_environment_can_use_context(self): - with self.policy.new_environment() as env: - self.assertRaises(vapoursynth.Error, lambda: vapoursynth.core.std.BlankClip().set_output(0)) + def test_new_environment_can_use_context(self, registered_policy: Policy) -> None: + with registered_policy.new_environment() as env: + with pytest.raises(vapoursynth.Error): + vapoursynth.core.std.BlankClip().set_output(0) with env.use(): vapoursynth.core.std.BlankClip().set_output(0) - self.assertRaises(vapoursynth.Error, lambda: vapoursynth.core.std.BlankClip().set_output(0)) + with pytest.raises(vapoursynth.Error): + vapoursynth.core.std.BlankClip().set_output(0) - def test_environment_can_switch(self): - env = self.policy.new_environment() - self.assertRaises(vapoursynth.Error, lambda: vapoursynth.core.std.BlankClip().set_output(0)) + def test_environment_can_switch(self, registered_policy: Policy) -> None: + env = registered_policy.new_environment() + with pytest.raises(vapoursynth.Error): + vapoursynth.core.std.BlankClip().set_output(0) env.switch() vapoursynth.core.std.BlankClip().set_output(0) env.dispose() - def test_environment_can_capture_outputs(self): - with self.policy.new_environment() as env1, self.policy.new_environment() as env2: + def test_environment_can_capture_outputs(self, registered_policy: Policy) -> None: + with registered_policy.new_environment() as env1, registered_policy.new_environment() as env2: with env1.use(): vapoursynth.core.std.BlankClip().set_output(0) - self.assertEqual(len(env1.outputs), 1) - self.assertEqual(len(env2.outputs), 0) + assert len(env1.outputs) == 1 + assert len(env2.outputs) == 0 - def test_environment_can_capture_cores(self): - with self.policy.new_environment() as env1, self.policy.new_environment() as env2: - self.assertNotEqual(env1.core, env2.core) + def test_environment_can_capture_cores(self, registered_policy: Policy) -> None: + with registered_policy.new_environment() as env1, registered_policy.new_environment() as env2: + assert env1.core != env2.core - def test_inline_section_is_invisible(self): - with self.policy.new_environment() as env1, self.policy.new_environment() as env2: + def test_inline_section_is_invisible(self, store: GlobalStore, registered_policy: Policy) -> None: + with registered_policy.new_environment() as env1, registered_policy.new_environment() as env2: env1.switch() - env_before = self.store.get_current_environment() + env_before = store.get_current_environment() with env2.inline_section(): - self.assertNotEqual(vapoursynth.get_current_environment(), env1.vs_environment) - self.assertEqual(env_before, self.store.get_current_environment()) + assert vapoursynth.get_current_environment() != env1.vs_environment + assert env_before == store.get_current_environment() - self.assertEqual(vapoursynth.get_current_environment(), env1.vs_environment) - self.assertEqual(env_before, self.store.get_current_environment()) + assert vapoursynth.get_current_environment() == env1.vs_environment + assert env_before == store.get_current_environment() diff --git a/tests/test_policy_store.py b/tests/test_policy_store.py index 729afd1..1f0ba91 100644 --- a/tests/test_policy_store.py +++ b/tests/test_policy_store.py @@ -1,83 +1,99 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 +"""Tests for the policy environment stores.""" + import concurrent.futures as futures -import unittest +from collections.abc import Iterator from contextvars import copy_context +from typing import Any + +import pytest from vsengine.policy import ContextVarStore, EnvironmentStore, GlobalStore, ThreadLocalStore class BaseStoreTest: + """Base class for environment store tests.""" + + store: EnvironmentStore + def create_store(self) -> EnvironmentStore: raise NotImplementedError - def setUp(self) -> None: + @pytest.fixture(autouse=True) + def setup_store(self) -> Iterator[None]: self.store = self.create_store() - - def tearDown(self) -> None: + yield self.store.set_current_environment(None) - def test_basic_functionality(self): - self.assertEqual(self.store.get_current_environment(), None) + def test_basic_functionality(self) -> None: + assert self.store.get_current_environment() is None - self.store.set_current_environment(1) - self.assertEqual(self.store.get_current_environment(), 1) - self.store.set_current_environment(2) - self.assertEqual(self.store.get_current_environment(), 2) + self.store.set_current_environment(1) # type: ignore[arg-type] + assert self.store.get_current_environment() == 1 + self.store.set_current_environment(2) # type: ignore[arg-type] + assert self.store.get_current_environment() == 2 self.store.set_current_environment(None) - self.assertEqual(self.store.get_current_environment(), None) + assert self.store.get_current_environment() is None -class TestGlobalStore(BaseStoreTest, unittest.TestCase): - def create_store(self) -> EnvironmentStore: +class TestGlobalStore(BaseStoreTest): + """Tests for GlobalStore.""" + + def create_store(self) -> GlobalStore: return GlobalStore() -class TestThreadLocalStore(BaseStoreTest, unittest.TestCase): - def create_store(self) -> EnvironmentStore: +class TestThreadLocalStore(BaseStoreTest): + """Tests for ThreadLocalStore.""" + + def create_store(self) -> ThreadLocalStore: return ThreadLocalStore() - def test_threads_do_not_influence_each_other(self): - def thread(): - self.assertEqual(self.store.get_current_environment(), None) - self.store.set_current_environment(2) - self.assertEqual(self.store.get_current_environment(), 2) + def test_threads_do_not_influence_each_other(self) -> None: + def thread() -> None: + assert self.store.get_current_environment() is None + self.store.set_current_environment(2) # type: ignore[arg-type] + assert self.store.get_current_environment() == 2 with futures.ThreadPoolExecutor(max_workers=1) as e: - self.store.set_current_environment(1) + self.store.set_current_environment(1) # type: ignore[arg-type] e.submit(thread).result() - self.assertEqual(self.store.get_current_environment(), 1) + assert self.store.get_current_environment() == 1 -class TestContextVarStore(BaseStoreTest, unittest.TestCase): - def create_store(self) -> EnvironmentStore: +class TestContextVarStore(BaseStoreTest): + """Tests for ContextVarStore.""" + + def create_store(self) -> ContextVarStore: return ContextVarStore("store_test") - def test_threads_do_not_influence_each_other(self): - def thread(): - self.assertEqual(self.store.get_current_environment(), None) - self.store.set_current_environment(2) - self.assertEqual(self.store.get_current_environment(), 2) + def test_threads_do_not_influence_each_other(self) -> None: + def thread() -> None: + assert self.store.get_current_environment() is None + self.store.set_current_environment(2) # type: ignore[arg-type] + assert self.store.get_current_environment() == 2 with futures.ThreadPoolExecutor(max_workers=1) as e: - self.store.set_current_environment(1) + self.store.set_current_environment(1) # type: ignore[arg-type] e.submit(thread).result() - self.assertEqual(self.store.get_current_environment(), 1) + assert self.store.get_current_environment() == 1 - def test_contexts_do_not_influence_each_other(self): - def context(p, n): - self.assertEqual(self.store.get_current_environment(), p) + def test_contexts_do_not_influence_each_other(self) -> None: + def context(p: Any, n: Any) -> None: + assert self.store.get_current_environment() == p self.store.set_current_environment(n) - self.assertEqual(self.store.get_current_environment(), n) + assert self.store.get_current_environment() == n ctx = copy_context() ctx.run(context, None, 1) - self.assertEqual(self.store.get_current_environment(), None) + assert self.store.get_current_environment() is None - self.store.set_current_environment(2) - self.assertEqual(self.store.get_current_environment(), 2) + self.store.set_current_environment(2) # type: ignore[arg-type] + assert self.store.get_current_environment() == 2 ctx.run(context, 1, 3) - self.assertEqual(self.store.get_current_environment(), 2) + assert self.store.get_current_environment() == 2 diff --git a/tests/test_video.py b/tests/test_video.py index 7f3d49e..4288fd5 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -1,111 +1,121 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 -import unittest -from typing import * +"""Tests for the video module.""" +from collections.abc import Iterator + +import pytest from vapoursynth import GRAY8, RGB24, PresetVideoFormat, VideoFormat, VideoFrame, VideoNode, core -from tests._testutils import forcefully_unregister_policy, use_standalone_policy +from tests._testutils import use_standalone_policy from vsengine.video import frame, frames, planes, render -AnyFormat = Union[PresetVideoFormat, VideoFormat] - - -class TestVideo(unittest.TestCase): - def setUp(self) -> None: - forcefully_unregister_policy() - use_standalone_policy() - - def tearDown(self) -> None: - forcefully_unregister_policy() - - @staticmethod - def generate_video(length: int = 3, width: int = 1, height: int = 1, format: AnyFormat = GRAY8) -> VideoNode: - clip = core.std.BlankClip(length=length, width=width, height=height, format=format, fpsden=1001, fpsnum=24000) - - def _add_frameno(n: int, f: VideoFrame) -> VideoFrame: - fout = f.copy() - fout.props["FrameNumber"] = n - return fout - - clip = core.std.ModifyFrame(clip=clip, clips=clip, selector=_add_frameno) - return clip - - def test_planes(self): - clipA = core.std.BlankClip(length=1, color=[0, 1, 2], width=1, height=1, format=RGB24) - clipB = core.std.BlankClip(length=1, color=[3, 4, 5], width=1, height=1, format=RGB24) - - clip = core.std.Splice([clipA, clipB]) - - self.assertEqual(planes(clip, 0).result(), [b"\x00", b"\x01", b"\x02"]) - self.assertEqual(planes(clip, 0, planes=[0]).result(), [b"\x00"]) - self.assertEqual(planes(clip, 0, planes=[1]).result(), [b"\x01"]) - self.assertEqual(planes(clip, 0, planes=[2]).result(), [b"\x02"]) - self.assertEqual(planes(clip, 0, planes=[2, 1, 0]).result(), [b"\x02", b"\x01", b"\x00"]) - - self.assertEqual(planes(clip, 1).result(), [b"\x03", b"\x04", b"\x05"]) - self.assertEqual(planes(clip, 1, planes=[0]).result(), [b"\x03"]) - self.assertEqual(planes(clip, 1, planes=[1]).result(), [b"\x04"]) - self.assertEqual(planes(clip, 1, planes=[2]).result(), [b"\x05"]) - self.assertEqual(planes(clip, 1, planes=[2, 1, 0]).result(), [b"\x05", b"\x04", b"\x03"]) - - def test_planes_default_supports_multiformat_clips(self): - clipA = core.std.BlankClip(length=1, color=[0, 1, 2], width=1, height=1, format=RGB24) - clipB = core.std.BlankClip(length=1, color=[3], width=1, height=1, format=GRAY8) - - clip = core.std.Splice([clipA, clipB], mismatch=True) - self.assertEqual(planes(clip, 0).result(), [b"\x00", b"\x01", b"\x02"]) - self.assertEqual(planes(clip, 1).result(), [b"\x03"]) - - def test_single_frame(self): - clip = self.generate_video() - with frame(clip, 0).result(timeout=0.1) as f: - self.assertEqual(f.props["FrameNumber"], 0) - - with frame(clip, 1).result(timeout=0.1) as f: - self.assertEqual(f.props["FrameNumber"], 1) - - with frame(clip, 2).result(timeout=0.1) as f: - self.assertEqual(f.props["FrameNumber"], 2) - - def test_multiple_frames(self): - clip = self.generate_video() - for nf, f in enumerate(frames(clip)): - self.assertEqual(f.props["FrameNumber"], nf) - - def test_multiple_frames_closes_after_iteration(self): - clip = self.generate_video() - - it = iter(frames(clip)) - f1 = next(it) - - try: - f2 = next(it) - except: - f1.close() - raise - - try: - with self.assertRaises(RuntimeError): - f1.props - finally: - f2.close() - next(it).close() - - def test_multiple_frames_without_closing(self): - clip = self.generate_video() - for nf, f in enumerate(frames(clip, close=False)): - self.assertEqual(f.props["FrameNumber"], nf) - f.close() - - def test_render(self): - clip = self.generate_video() - data = b"".join(f[1] for f in render(clip)) - self.assertEqual(data, b"\0\0\0") - - def test_render_y4m(self): - clip = self.generate_video() - data = b"".join(f[1] for f in render(clip, y4m=True)) - self.assertEqual(data, b"YUV4MPEG2 Cmono W1 H1 F24000:1001 Ip A0:0 XLENGTH=3\nFRAME\n\0FRAME\n\0FRAME\n\0") +AnyFormat = PresetVideoFormat | VideoFormat + + +@pytest.fixture(autouse=True) +def standalone_policy() -> Iterator[None]: + """Set up a standalone policy for video tests.""" + use_standalone_policy() + yield + + +def generate_video(length: int = 3, width: int = 1, height: int = 1, format: AnyFormat = GRAY8) -> VideoNode: + """Generate a test video clip with frame numbers in props.""" + clip = core.std.BlankClip(length=length, width=width, height=height, format=format, fpsden=1001, fpsnum=24000) + + def _add_frameno(n: int, f: VideoFrame) -> VideoFrame: + fout = f.copy() + fout.props["FrameNumber"] = n + return fout + + clip = core.std.ModifyFrame(clip=clip, clips=clip, selector=_add_frameno) + return clip + + +def test_planes() -> None: + clip_a = core.std.BlankClip(length=1, color=[0, 1, 2], width=1, height=1, format=RGB24) + clip_b = core.std.BlankClip(length=1, color=[3, 4, 5], width=1, height=1, format=RGB24) + + clip = core.std.Splice([clip_a, clip_b]) + + assert list(planes(clip, 0).result()) == [b"\x00", b"\x01", b"\x02"] + assert list(planes(clip, 0, planes=[0]).result()) == [b"\x00"] + assert list(planes(clip, 0, planes=[1]).result()) == [b"\x01"] + assert list(planes(clip, 0, planes=[2]).result()) == [b"\x02"] + assert list(planes(clip, 0, planes=[2, 1, 0]).result()) == [b"\x02", b"\x01", b"\x00"] + + assert list(planes(clip, 1).result()) == [b"\x03", b"\x04", b"\x05"] + assert list(planes(clip, 1, planes=[0]).result()) == [b"\x03"] + assert list(planes(clip, 1, planes=[1]).result()) == [b"\x04"] + assert list(planes(clip, 1, planes=[2]).result()) == [b"\x05"] + assert list(planes(clip, 1, planes=[2, 1, 0]).result()) == [b"\x05", b"\x04", b"\x03"] + + +def test_planes_default_supports_multiformat_clips() -> None: + clip_a = core.std.BlankClip(length=1, color=[0, 1, 2], width=1, height=1, format=RGB24) + clip_b = core.std.BlankClip(length=1, color=[3], width=1, height=1, format=GRAY8) + + clip = core.std.Splice([clip_a, clip_b], mismatch=True) + assert list(planes(clip, 0).result()) == [b"\x00", b"\x01", b"\x02"] + assert list(planes(clip, 1).result()) == [b"\x03"] + + +def test_single_frame() -> None: + clip = generate_video() + with frame(clip, 0).result(timeout=0.1) as f: + assert f.props["FrameNumber"] == 0 + + with frame(clip, 1).result(timeout=0.1) as f: + assert f.props["FrameNumber"] == 1 + + with frame(clip, 2).result(timeout=0.1) as f: + assert f.props["FrameNumber"] == 2 + + +def test_multiple_frames() -> None: + clip = generate_video() + for nf, f in enumerate(frames(clip)): + assert f.props["FrameNumber"] == nf + + +def test_multiple_frames_closes_after_iteration() -> None: + clip = generate_video() + + it = iter(frames(clip)) + f1 = next(it) + + try: + f2 = next(it) + except Exception: + f1.close() + raise + + try: + with pytest.raises(RuntimeError): + _ = f1.props + finally: + f2.close() + next(it).close() + + +def test_multiple_frames_without_closing() -> None: + clip = generate_video() + for nf, f in enumerate(frames(clip, close=False)): + assert f.props["FrameNumber"] == nf + f.close() + + +def test_render() -> None: + clip = generate_video() + data = b"".join(f[1] for f in render(clip)) + assert data == b"\0\0\0" + + +def test_render_y4m() -> None: + clip = generate_video() + data = b"".join(f[1] for f in render(clip, y4m=True)) + assert data == b"YUV4MPEG2 Cmono W1 H1 F24000:1001 Ip A0:0 XLENGTH=3\nFRAME\n\0FRAME\n\0FRAME\n\0" diff --git a/tests/test_vpy.py b/tests/test_vpy.py index 2ddd227..8e4f6d7 100644 --- a/tests/test_vpy.py +++ b/tests/test_vpy.py @@ -1,7 +1,9 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 +"""Tests for the vpy module (script loading and execution).""" import ast import contextlib @@ -9,25 +11,25 @@ import textwrap import threading import types -from collections.abc import Callable, Generator +from collections.abc import Callable, Iterator from typing import Any import pytest import vapoursynth -from tests._testutils import BLACKBOARD, forcefully_unregister_policy -from vsengine.loops import NO_LOOP, set_loop +from tests._testutils import BLACKBOARD +from vsengine.adapters.asyncio import AsyncIOLoop +from vsengine.loops import set_loop from vsengine.policy import GlobalStore, Policy from vsengine.vpy import ( - ExecutionFailed, - ManagedScript, + ExecutionError, Script, WrapAllErrors, _load, chdir_runner, inline_runner, load_code, - load_file, + load_script, ) DIR: str = os.path.dirname(__file__) @@ -35,31 +37,24 @@ @contextlib.contextmanager -def noop() -> Generator[None, None, None]: +def noop() -> Iterator[None]: yield -class TestException(Exception): +class TestError(Exception): pass -def callback_script(func: Callable[[types.ModuleType], None]) -> Callable[[Any, types.ModuleType], None]: - def _script(ctx: Any, module: types.ModuleType) -> None: +def callback_script( + func: Callable[[types.ModuleType], None], +) -> Callable[[contextlib.AbstractContextManager[None], types.ModuleType], None]: + def _script(ctx: contextlib.AbstractContextManager[None], module: types.ModuleType) -> None: with ctx: func(module) return _script -@pytest.fixture(autouse=True) -def clean_policy() -> Generator[None, None, None]: - """Fixture to handle setup and teardown for policy and loops.""" - forcefully_unregister_policy() - yield - forcefully_unregister_policy() - set_loop(NO_LOOP) - - def test_run_executes_successfully() -> None: run = False @@ -78,15 +73,15 @@ def test_code(_: types.ModuleType) -> None: def test_run_wraps_exception() -> None: @callback_script def test_code(_: types.ModuleType) -> None: - raise TestException() + raise TestError() with Policy(GlobalStore()) as p, p.new_environment() as env: s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) fut = s.run() exc = fut.exception() - assert isinstance(exc, ExecutionFailed) - assert isinstance(exc.parent_error, TestException) + assert isinstance(exc, ExecutionError) + assert isinstance(exc.parent_error, TestError) def test_execute_resolves_immediately() -> None: @@ -117,14 +112,14 @@ def test_code(_: types.ModuleType) -> None: def test_execute_resolves_immediately_when_raising() -> None: @callback_script def test_code(_: types.ModuleType) -> None: - raise TestException + raise TestError with Policy(GlobalStore()) as p, p.new_environment() as env: s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner) try: s.result() - except ExecutionFailed as err: - assert isinstance(err.parent_error, TestException) + except ExecutionError as err: + assert isinstance(err.parent_error, TestError) except Exception as e: pytest.fail(f"Wrong exception: {e!r}") else: @@ -133,6 +128,7 @@ def test_code(_: types.ModuleType) -> None: @pytest.mark.asyncio async def test_run_async() -> None: + set_loop(AsyncIOLoop()) run = False @callback_script @@ -149,6 +145,7 @@ def test_code(_: types.ModuleType) -> None: @pytest.mark.asyncio async def test_await_directly() -> None: + set_loop(AsyncIOLoop()) run = False @callback_script @@ -169,7 +166,7 @@ def test_code(_: types.ModuleType) -> None: with Policy(GlobalStore()) as p: env = p.new_environment() - s = ManagedScript(test_code, types.ModuleType("__test__"), env, inline_runner) + s = Script(test_code, types.ModuleType("__test__"), env, inline_runner) try: s.dispose() @@ -185,7 +182,7 @@ def test_code(_: types.ModuleType) -> None: with Policy(GlobalStore()) as p: env = p.new_environment() - with ManagedScript(test_code, types.ModuleType("__test__"), env, inline_runner): + with Script(test_code, types.ModuleType("__test__"), env, inline_runner): pass try: @@ -204,7 +201,7 @@ def test_code(_: types.ModuleType) -> None: curdir = os.getcwd() wrapped = chdir_runner(DIR, inline_runner) - wrapped(test_code, noop(), 2) + wrapped(test_code, noop(), 2) # type: ignore[arg-type] assert curdir == DIR @@ -216,7 +213,7 @@ def test_code(_: types.ModuleType) -> None: wrapped = chdir_runner(DIR, inline_runner) before = os.getcwd() - wrapped(test_code, noop(), None) + wrapped(test_code, noop(), None) # type: ignore[arg-type] assert os.getcwd() == before @@ -229,7 +226,7 @@ def test_code(_: types.ModuleType) -> None: vpy_env = vapoursynth.get_current_environment() with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - _load(test_code, None, inline=False, chdir=None).result() + _load(test_code, None, "__vapoursynth__", inline=False, chdir=None).result() assert vpy_env == env.vs_environment @@ -242,7 +239,7 @@ def test_code(_: types.ModuleType) -> None: vpy_env = vapoursynth.get_current_environment() with Policy(GlobalStore()) as p: - s = _load(test_code, p, inline=True, chdir=None) + s = _load(test_code, p, "__vapoursynth__", inline=True, chdir=None) try: s.result() assert vpy_env == s.environment.vs_environment @@ -261,11 +258,11 @@ def test_code_2(module: types.ModuleType) -> None: assert module.test is True with Policy(GlobalStore()) as p: - script1 = _load(test_code_1, p, inline=True, chdir=None) + script1 = _load(test_code_1, p, "__test_1__", inline=True, chdir=None) env = script1.environment try: script1.result() - script2 = _load(test_code_2, script1, inline=True, chdir=None) + script2 = _load(test_code_2, script1, "__test_2__", inline=True, chdir=None) script2.result() finally: env.dispose() @@ -282,16 +279,16 @@ def test_code_2(module: types.ModuleType) -> None: with Policy(GlobalStore()) as p: try: - script1 = _load(test_code_1, p, module_name="__test_1__") + script1 = _load(test_code_1, p, "__test_1__", inline=True, chdir=None) script1.result() finally: - script1.dispose() + script1.dispose() # pyright: ignore[reportPossiblyUnboundVariable] try: - script2 = _load(test_code_2, p, module_name="__test_2__") + script2 = _load(test_code_2, p, "__test_2__", inline=True, chdir=None) script2.result() finally: - script2.dispose() + script2.dispose() # pyright: ignore[reportPossiblyUnboundVariable] def test_load_runs_chdir() -> None: @@ -304,7 +301,7 @@ def test_code(_: types.ModuleType) -> None: with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): previous = os.getcwd() - _load(test_code, None, inline=True, chdir=DIR).result() + _load(test_code, None, "__vapoursynth__", inline=True, chdir=DIR).result() assert curdir == DIR assert os.getcwd() == previous @@ -318,7 +315,7 @@ def test_code(_: types.ModuleType) -> None: thread = threading.current_thread() with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - _load(test_code, None, inline=False, chdir=None).result() + _load(test_code, None, "__vapoursynth__", inline=False, chdir=None).result() assert thread is not threading.current_thread() @@ -331,57 +328,57 @@ def test_code(_: types.ModuleType) -> None: thread = threading.current_thread() with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - _load(test_code, None, chdir=None).result() + _load(test_code, None, "__vapoursynth__", True, chdir=None).result() assert thread is threading.current_thread() def test_code_runs_string() -> None: - CODE = textwrap.dedent(""" - from vsengine._testutils import BLACKBOARD + code = textwrap.dedent(""" + from tests._testutils import BLACKBOARD BLACKBOARD["vpy_test_runs_raw_code_str"] = True """) with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - load_code(CODE).result() + load_code(code).result() assert BLACKBOARD.get("vpy_test_runs_raw_code_str") is True def test_code_runs_bytes() -> None: - CODE = textwrap.dedent(""" + code = textwrap.dedent(""" # encoding: latin-1 - from vsengine._testutils import BLACKBOARD + from tests._testutils import BLACKBOARD BLACKBOARD["vpy_test_runs_raw_code_bytes"] = True """).encode("latin-1") with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - load_code(CODE).result() + load_code(code).result() assert BLACKBOARD.get("vpy_test_runs_raw_code_bytes") is True def test_code_runs_ast() -> None: - CODE = ast.parse( + code = ast.parse( textwrap.dedent(""" - from vsengine._testutils import BLACKBOARD + from tests._testutils import BLACKBOARD BLACKBOARD["vpy_test_runs_raw_code_ast"] = True """) ) with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - load_code(CODE).result() + load_code(code).result() assert BLACKBOARD.get("vpy_test_runs_raw_code_ast") is True def test_script_runs() -> None: BLACKBOARD.clear() with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - load_code(PATH).result() + load_script(PATH).result() assert BLACKBOARD.get("vpy_run_script") is True def test_script_runs_with_custom_name() -> None: BLACKBOARD.clear() with Policy(GlobalStore()) as p, p.new_environment() as env, env.use(): - load_file(PATH, module="__test__").result() + load_script(PATH, module="__test__").result() assert BLACKBOARD.get("vpy_run_script_name") == "__test__" @@ -390,7 +387,7 @@ def test_wrap_exceptions_wraps_exception() -> None: try: with WrapAllErrors(): raise err - except ExecutionFailed as e: + except ExecutionError as e: assert e.parent_error is err else: pytest.fail("Wrap all errors swallowed the exception") diff --git a/uv.lock b/uv.lock index 2b69b38..53f4bfe 100644 --- a/uv.lock +++ b/uv.lock @@ -265,6 +265,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -397,6 +410,7 @@ trio = [ dev = [ { name = "mypy" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "ruff" }, { name = "trio" }, { name = "vsstubs", marker = "python_full_version >= '3.13'" }, @@ -413,6 +427,7 @@ provides-extras = ["trio"] dev = [ { name = "mypy", specifier = ">=1.19.0" }, { name = "pytest", specifier = ">=9.0.1" }, + { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "ruff", specifier = ">=0.14.7" }, { name = "trio" }, { name = "vsstubs", marker = "python_full_version >= '3.13'" }, diff --git a/vsengine/_futures.py b/vsengine/_futures.py index e258031..9202c46 100644 --- a/vsengine/_futures.py +++ b/vsengine/_futures.py @@ -97,7 +97,7 @@ def _done(fn: Future[T]) -> None: def map[V](self, cb: Callable[[T], V]) -> UnifiedFuture[V]: return self.then(cb, None) - def catch[V](self, cb: Callable[[BaseException], V]) -> UnifiedFuture[V]: + def catch[V](self, cb: Callable[[BaseException], V]) -> UnifiedFuture[T | V]: return self.then(None, cb) # Nicer Syntax From ea25d267dfa5c3cfd9d499332cc7e559d0743377 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 03:10:41 +0100 Subject: [PATCH 55/60] test: migrate loop adapter tests to pytest and add type hints --- tests/test_loop_adapters.py | 191 ++++++++++++++++++++---------------- 1 file changed, 106 insertions(+), 85 deletions(-) diff --git a/tests/test_loop_adapters.py b/tests/test_loop_adapters.py index 322efcc..694c0d4 100644 --- a/tests/test_loop_adapters.py +++ b/tests/test_loop_adapters.py @@ -1,35 +1,46 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 +"""Tests for event loop adapters.""" import asyncio import contextlib import threading -import unittest +from collections.abc import Generator, Iterator from concurrent.futures import CancelledError, Future +from typing import Any + +import pytest from vsengine.adapters.asyncio import AsyncIOLoop -from vsengine.loops import NO_LOOP, Cancelled, EventLoop, _NoEventLoop, set_loop +from vsengine.loops import NO_LOOP, Cancelled, EventLoop, _NoEventLoop, make_awaitable, set_loop + +def make_async(func: Any) -> Any: + """Decorator to run a generator-based test within a loop.""" -def make_async(func): - def _wrapped(self, *args, **kwargs): + def _wrapped(self: AdapterTest, *args: Any, **kwargs: Any) -> Any: return self.run_within_loop(func, args, kwargs) return _wrapped -def is_async(func): - def _wrapped(self, *args, **kwargs): +def is_async(func: Any) -> Any: + """Decorator to run an async test within a loop.""" + + def _wrapped(self: "AsyncAdapterTest", *args: Any, **kwargs: Any) -> Any: return self.run_within_loop_async(func, args, kwargs) return _wrapped class AdapterTest: + """Base class for event loop adapter tests.""" + @contextlib.contextmanager - def with_loop(self): + def with_loop(self) -> Iterator[EventLoop]: loop = self.make_loop() set_loop(loop) try: @@ -40,65 +51,67 @@ def with_loop(self): def make_loop(self) -> EventLoop: raise NotImplementedError - def run_within_loop(self, func, args, kwargs): + def run_within_loop(self, func: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: raise NotImplementedError - def resolve_to_thread_future(self, fut): + def resolve_to_thread_future(self, fut: Any) -> Generator[Any, None, Any]: raise NotImplementedError @contextlib.contextmanager - def assertCancelled(self): + def assert_cancelled(self) -> Iterator[None]: raise NotImplementedError @make_async - def test_wrap_cancelled_without_cancellation(self): + def test_wrap_cancelled_without_cancellation(self) -> None: with self.with_loop() as loop, loop.wrap_cancelled(): pass @make_async - def test_wrap_cancelled_with_cancellation(self): - with self.with_loop() as loop, self.assertCancelled(), loop.wrap_cancelled(): + def test_wrap_cancelled_with_cancellation(self) -> Iterator[None]: + with self.with_loop() as loop, self.assert_cancelled(), loop.wrap_cancelled(): raise Cancelled @make_async - def test_wrap_cancelled_with_other_exception(self): - with self.with_loop() as loop, self.assertRaises(RuntimeError), loop.wrap_cancelled(): + def test_wrap_cancelled_with_other_exception(self) -> Iterator[None]: + with self.with_loop() as loop, pytest.raises(RuntimeError), loop.wrap_cancelled(): raise RuntimeError() + yield @make_async - def test_next_cycle_doesnt_throw_when_not_cancelled(self): + def test_next_cycle_doesnt_throw_when_not_cancelled(self) -> Iterator[None]: with self.with_loop() as loop: fut = loop.next_cycle() yield - self.assertTrue(fut.done()) - self.assertIs(fut.result(), None) + assert fut.done() + assert fut.result() is None @make_async - def test_from_thread_with_success(self) -> None: - def test_func(): + def test_from_thread_with_success(self) -> Iterator[None]: + def test_func() -> "AdapterTest": return self with self.with_loop() as loop: fut = loop.from_thread(test_func) yield - self.assertIs(fut.result(timeout=0.5), self) + assert fut.result(timeout=0.5) is self @make_async - def test_from_thread_with_failure(self) -> None: - def test_func(): + def test_from_thread_with_failure(self) -> Iterator[None]: + def test_func() -> None: raise RuntimeError with self.with_loop() as loop: fut = loop.from_thread(test_func) yield - self.assertRaises(RuntimeError, lambda: fut.result(timeout=0.5)) + with pytest.raises(RuntimeError): + fut.result(timeout=0.5) @make_async - def test_from_thread_forwards_correctly(self) -> None: - a = None - k = None + def test_from_thread_forwards_correctly(self) -> Iterator[None]: + a: tuple[Any, ...] | None = None + k: dict[str, Any] | None = None - def test_func(*args, **kwargs): + def test_func(*args: Any, **kwargs: Any) -> None: nonlocal a, k a = args k = kwargs @@ -107,45 +120,47 @@ def test_func(*args, **kwargs): fut = loop.from_thread(test_func, 1, 2, 3, a="b", c="d") yield fut.result(timeout=0.5) - self.assertEqual(a, (1, 2, 3)) - self.assertEqual(k, {"a": "b", "c": "d"}) + assert a == (1, 2, 3) + assert k == {"a": "b", "c": "d"} @make_async - def test_to_thread_spawns_a_new_thread(self): - def test_func(): + def test_to_thread_spawns_a_new_thread(self) -> Iterator[None]: + def test_func() -> threading.Thread: return threading.current_thread() with self.with_loop() as loop: t2 = yield from self.resolve_to_thread_future(loop.to_thread(test_func)) - self.assertNotEqual(threading.current_thread(), t2) + assert threading.current_thread() != t2 @make_async - def test_to_thread_runs_inline_with_failure(self) -> None: - def test_func(): + def test_to_thread_runs_inline_with_failure(self) -> Iterator[None]: + def test_func() -> None: raise RuntimeError - with self.with_loop() as loop, self.assertRaises(RuntimeError): + with self.with_loop() as loop, pytest.raises(RuntimeError): yield from self.resolve_to_thread_future(loop.to_thread(test_func)) @make_async - def test_to_thread_forwards_correctly(self) -> None: - a = None - k = None + def test_to_thread_forwards_correctly(self) -> Iterator[None]: + a: tuple[Any, ...] | None = None + k: dict[str, Any] | None = None - def test_func(*args, **kwargs): + def test_func(*args: Any, **kwargs: Any) -> None: nonlocal a, k a = args k = kwargs with self.with_loop() as loop: yield from self.resolve_to_thread_future(loop.to_thread(test_func, 1, 2, 3, a="b", c="d")) - self.assertEqual(a, (1, 2, 3)) - self.assertEqual(k, {"a": "b", "c": "d"}) + assert a == (1, 2, 3) + assert k == {"a": "b", "c": "d"} class AsyncAdapterTest(AdapterTest): - def run_within_loop(self, func, args, kwargs): - async def wrapped(_): + """Base class for async event loop adapter tests.""" + + def run_within_loop(self, func: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + async def wrapped(_: Any) -> None: result = func(self, *args, **kwargs) if hasattr(result, "__iter__"): for _ in result: @@ -153,83 +168,85 @@ async def wrapped(_): self.run_within_loop_async(wrapped, (), {}) - def run_within_loop_async(self, func, args, kwargs): + def run_within_loop_async(self, func: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: raise NotImplementedError - async def wait_for(self, coro, timeout): + async def wait_for(self, coro: Any, timeout: float) -> Any: raise NotImplementedError - async def next_cycle(self): + async def next_cycle(self) -> None: pass @is_async - async def test_await_future_success(self): + async def test_await_future_success(self) -> None: with self.with_loop() as loop: - fut = Future() + fut: Future[int] = Future() - def _setter(): + def _setter() -> None: fut.set_result(1) threading.Thread(target=_setter).start() - self.assertEqual(await self.wait_for(loop.await_future(fut), 0.5), 1) + assert await self.wait_for(loop.await_future(fut), 0.5) == 1 @is_async - async def test_await_future_failure(self): + async def test_await_future_failure(self) -> None: with self.with_loop() as loop: - fut = Future() + fut: Future[int] = Future() - def _setter(): + def _setter() -> None: fut.set_exception(RuntimeError()) threading.Thread(target=_setter).start() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): await self.wait_for(loop.await_future(fut), 0.5) -class NoLoopTest(AdapterTest, unittest.TestCase): +class TestNoLoop(AdapterTest): + """Tests for the no-event-loop adapter.""" + def make_loop(self) -> EventLoop: return _NoEventLoop() - def run_within_loop(self, func, args, kwargs): + def run_within_loop(self, func: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: result = func(self, *args, **kwargs) if hasattr(result, "__iter__"): for _ in result: pass @contextlib.contextmanager - def assertCancelled(self): - with self.assertRaises(CancelledError): + def assert_cancelled(self) -> Iterator[None]: + with pytest.raises(CancelledError): yield - def resolve_to_thread_future(self, fut): - if False: - yield + def resolve_to_thread_future(self, fut: Future[Any]) -> Generator[None, None, Any]: return fut.result(timeout=0.5) + yield # type: ignore[unreachable] + +class TestAsyncIO(AsyncAdapterTest): + """Tests for the asyncio event loop adapter.""" -class AsyncIOTest(AsyncAdapterTest, unittest.TestCase): def make_loop(self) -> AsyncIOLoop: return AsyncIOLoop() - def run_within_loop_async(self, func, args, kwargs): - async def wrapped(): + def run_within_loop_async(self, func: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> None: + async def wrapped() -> None: await func(self, *args, **kwargs) asyncio.run(wrapped()) - async def next_cycle(self): + async def next_cycle(self) -> None: await asyncio.sleep(0.01) - async def wait_for(self, coro, timeout): + async def wait_for(self, coro: Any, timeout: float) -> Any: return await asyncio.wait_for(coro, timeout) @contextlib.contextmanager - def assertCancelled(self): - with self.assertRaises(asyncio.CancelledError): + def assert_cancelled(self) -> Iterator[None]: + with pytest.raises(asyncio.CancelledError): yield - def resolve_to_thread_future(self, fut): - fut = asyncio.ensure_future(fut) + def resolve_to_thread_future(self, fut: Any) -> Generator[None, None, Any]: while not fut.done(): yield return fut.result() @@ -238,34 +255,38 @@ def resolve_to_thread_future(self, fut): try: import trio except ImportError: - print("Skipping trio") + print("Skipping trio tests") else: from vsengine.adapters.trio import TrioEventLoop - class TrioTest(AsyncAdapterTest, unittest.TestCase): - def make_loop(self) -> AsyncIOLoop: + class TestTrio(AsyncAdapterTest): + """Tests for the trio event loop adapter.""" + + nursery: trio.Nursery + + def make_loop(self) -> TrioEventLoop: return TrioEventLoop(self.nursery) - async def next_cycle(self): + async def next_cycle(self) -> None: await trio.sleep(0.01) - def run_within_loop_async(self, func, args, kwargs): - async def wrapped(): + def run_within_loop_async(self, func: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> None: + async def wrapped() -> None: async with trio.open_nursery() as nursery: self.nursery = nursery await func(self, *args, **kwargs) trio.run(wrapped) - def resolve_to_thread_future(self, fut): + def resolve_to_thread_future(self, fut: Any) -> Generator[None, None, Any]: done = False - result = None - error = None + result: Any = None + error: BaseException | None = None - async def _awaiter(): + async def _awaiter() -> None: nonlocal done, error, result try: - result = await fut + result = await make_awaitable(fut) except BaseException as e: error = e finally: @@ -281,11 +302,11 @@ async def _awaiter(): else: return result - async def wait_for(self, coro, timeout): + async def wait_for(self, coro: Any, timeout: float) -> Any: with trio.fail_after(timeout): return await coro @contextlib.contextmanager - def assertCancelled(self): - with self.assertRaises(trio.Cancelled): + def assert_cancelled(self) -> Iterator[None]: + with pytest.raises(trio.Cancelled): yield From 68a843cfd4c25912c44ea246dbd6a58f95b05040 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 03:11:30 +0100 Subject: [PATCH 56/60] test: fix type inference for generator helper --- tests/test_futures.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_futures.py b/tests/test_futures.py index 59a444e..2b3235a 100644 --- a/tests/test_futures.py +++ b/tests/test_futures.py @@ -285,6 +285,8 @@ def test_unified_iterator_run_as_completed_cancels_on_iterator_crash() -> None: err = RuntimeError("test") def _it() -> Iterator[Future[int]]: + if False: + yield Future[int]() # type:ignore[unreachable] raise err def _noop(_: Future[int]) -> None: From aa2ca28d21ab5b69236f7a00b7164f1ea62414b5 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 03:26:46 +0100 Subject: [PATCH 57/60] test: migrate hospice tests to pytest and add types --- tests/test_hospice.py | 211 +++++++++++++++++++++++++++--------------- 1 file changed, 136 insertions(+), 75 deletions(-) diff --git a/tests/test_hospice.py b/tests/test_hospice.py index e9a76b5..ee67441 100644 --- a/tests/test_hospice.py +++ b/tests/test_hospice.py @@ -1,23 +1,75 @@ # vs-engine # Copyright (C) 2022 cid-chan +# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy # This project is licensed under the EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2 +"""Tests for the hospice module (delayed object cleanup).""" import contextlib import gc import logging -import unittest import weakref +from collections.abc import Iterator +from typing import Any +import pytest + +from vsengine import _hospice from vsengine._hospice import admit_environment, any_alive, freeze, unfreeze -class Obj: - pass +@pytest.fixture(autouse=True) +def reset_hospice_state() -> Iterator[None]: + """Reset hospice module state before each test to ensure isolation.""" + # Clear the mock timings registry + _mock_timings_registry.clear() + # Clear all hospice state before test + _hospice.stage1.clear() + _hospice.stage2.clear() + _hospice.stage2_to_add.clear() + _hospice.hold.clear() + _hospice.cores.clear() + _hospice.refnanny.clear() + yield + # Clean up after test as well + _mock_timings_registry.clear() + _hospice.stage1.clear() + _hospice.stage2.clear() + _hospice.stage2_to_add.clear() + _hospice.hold.clear() + _hospice.cores.clear() + _hospice.refnanny.clear() + + +# Global registry to simulate CoreTimings holding references to cores +# This adds the extra reference needed for the > 3 refcount check +_mock_timings_registry = list[Any]() + + +class MockCore: + """ + Mock Core object that simulates the CoreTimings reference behavior. + + Real VapourSynth Core has a CoreTimings object that holds a reference to it, + so getrefcount(core) is at least 3: + - 1 from cores dict in hospice + - 1 from CoreTimings + - 1 from getrefcount() temporary + + We simulate this by registering each MockCore in a global registry. + """ + + def __init__(self) -> None: + # Register self to simulate CoreTimings reference + _mock_timings_registry.append(self) + + +class MockEnv: + """Mock EnvironmentData object.""" @contextlib.contextmanager -def hide_logs(): +def hide_logs() -> Iterator[None]: logging.disable(logging.CRITICAL) try: yield @@ -25,87 +77,96 @@ def hide_logs(): logging.disable(logging.NOTSET) -class HospiceTest(unittest.TestCase): - def test_hospice_delays_connection(self): - o1 = Obj() - o2 = Obj() - o2r = weakref.ref(o2) +def test_hospice_delays_connection() -> None: + o1 = MockEnv() + o2 = MockCore() + o2r = weakref.ref(o2) - admit_environment(o1, o2) - del o2 - del o1 + admit_environment(o1, o2) # type:ignore[arg-type] - self.assertIsNotNone(o2r()) + # Remove local ref to o2, but registry still holds it + del o2 + del o1 - gc.collect() - self.assertIsNotNone(o2r()) + # Clear the mock registry to release the "CoreTimings" reference + _mock_timings_registry.clear() - # Stage-2 add-queue + Stage 2 proper - gc.collect() - gc.collect() + assert o2r() is not None - self.assertIsNone(o2r()) + gc.collect() + assert o2r() is not None - def test_hospice_is_delayed_on_alive_objects(self): - o1 = Obj() - o2 = Obj() - o2r = weakref.ref(o2) + # Stage-2 add-queue + Stage 2 proper + gc.collect() + gc.collect() - admit_environment(o1, o2) - del o1 + assert o2r() is None - with self.assertLogs("vsengine._hospice", level=logging.WARN): - gc.collect() - gc.collect() - del o2 - self.assertIsNotNone(o2r()) - gc.collect() - gc.collect() - gc.collect() +def test_hospice_is_delayed_on_alive_objects(caplog: pytest.LogCaptureFixture) -> None: + o1 = MockEnv() + o2 = MockCore() + o2r = weakref.ref(o2) - self.assertIsNone(o2r()) - - def test_hospice_reports_alive_objects_correctly(self): - o1 = Obj() - o2 = Obj() - admit_environment(o1, o2) - del o1 - - with hide_logs(): - self.assertTrue( - any_alive(), - "The hospice did report that all objects are not alive anymore. This is obviously not true.", - ) - del o2 - - self.assertFalse( - any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true." - ) - - def test_hospice_can_forget_about_cores_safely(self): - o1 = Obj() - o2 = Obj() - admit_environment(o1, o2) - del o1 - - with hide_logs(): - self.assertTrue( - any_alive(), - "The hospice did report that all objects are not alive anymore. This is obviously not true.", - ) - freeze() - self.assertFalse( - any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true." - ) - - unfreeze() - with hide_logs(): - self.assertTrue( - any_alive(), - "The hospice did report that all objects are not alive anymore. This is obviously not true.", - ) - del o2 + admit_environment(o1, o2) # type:ignore[arg-type] + del o1 + # o2 is still held by local var AND registry, so refcount > 3 + with caplog.at_level(logging.WARN, logger="vsengine._hospice"): gc.collect() gc.collect() + + assert len(caplog.records) > 0 + + # Delete local ref but keep registry - still should delay collection + del o2 + assert o2r() is not None + + # Now clear registry to allow collection + _mock_timings_registry.clear() + + gc.collect() + gc.collect() + gc.collect() + + assert o2r() is None + + +def test_hospice_reports_alive_objects_correctly() -> None: + o1 = MockEnv() + o2 = MockCore() + admit_environment(o1, o2) # type:ignore[arg-type] + del o1 + + # o2 is still alive (local var + registry) + with hide_logs(): + assert any_alive(), "The hospice did report that all objects are not alive anymore. This is obviously not true." + + # Delete local ref but keep registry - still delays as "alive" due to CoreTimings-like ref + del o2 + + # Now clear the registry to allow collection + _mock_timings_registry.clear() + + assert not any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true." + + +def test_hospice_can_forget_about_cores_safely() -> None: + o1 = MockEnv() + o2 = MockCore() + admit_environment(o1, o2) # type:ignore[arg-type] + del o1 + + with hide_logs(): + assert any_alive(), "The hospice did report that all objects are not alive anymore. This is obviously not true." + freeze() + assert not any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true." + + unfreeze() + with hide_logs(): + assert any_alive(), "The hospice did report that all objects are not alive anymore. This is obviously not true." + del o2 + _mock_timings_registry.clear() + + gc.collect() + gc.collect() From eda8146bb1a034848ab2dcf35b7ba60c318fdf80 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 03:44:44 +0100 Subject: [PATCH 58/60] add ci tests --- .github/workflows/linux.yml | 136 ------------------------------------ .github/workflows/test.yml | 63 +++++++++++++++++ pyproject.toml | 1 + uv.lock | 90 ++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 136 deletions(-) delete mode 100644 .github/workflows/linux.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml deleted file mode 100644 index f21f0c3..0000000 --- a/.github/workflows/linux.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: "Run Tests and Package" -on: - push: - pull_request: -jobs: - tests-linux: - runs-on: ubuntu-latest - strategy: - matrix: - vs: ["58", "59", "latest"] - py: ["39", "310"] - arch: ["i686", "x86_64"] - steps: - - uses: actions/checkout@v2.4.0 - - uses: cachix/install-nix-action@v15 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v10 - with: - name: vs-engine - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - run: nix build -L .#checks.${{ matrix.arch }}-linux.check-python${{ matrix.py }}-vapoursynth${{ matrix.vs }} - - tests-darwin: - runs-on: macos-latest - strategy: - matrix: - vs: ["58", "59", "latest"] - py: ["39", "310"] - steps: - - uses: actions/checkout@v2.4.0 - - uses: cachix/install-nix-action@v15 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v10 - with: - name: vs-engine - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - run: nix build -L .#checks.x86_64-darwin.check-python${{ matrix.py }}-vapoursynth${{ matrix.vs }} - - tests-windows: - runs-on: windows-latest - strategy: - matrix: - vs: ["58", "59"] - arch: ["x64", "x86"] - include: - - vs: "58" - python: "3.10" - - vs: "59" - python: "3.10" - steps: - - uses: actions/checkout@v2.4.0 - - name: Install Python ${{ matrix.python }} - uses: actions/setup-python@v3 - with: - python-version: "${{ matrix.python }}" - architecture: "${{ matrix.arch }}" - - name: Installing dependencies - run: | - pip install flit - pip install vapoursynth==${{ matrix.vs }} vapoursynth_portable==${{ matrix.vs }} - flit install --user --pth-file - - name: Running Tests - run: | - python -m unittest discover -s ./tests/ -v - - build: - runs-on: ubuntu-latest - needs: - - tests-linux - - tests-darwin - - tests-windows - steps: - # Set-up runner. - - uses: actions/checkout@v2.4.0 - - name: Set outputs - id: vars - run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" - - - name: Check outputs - run: echo ${{ steps.vars.outputs.sha_short }} - - uses: cachix/install-nix-action@v15 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v10 - with: - name: vs-engine - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - # Replace the dirty-tag with the commit-id. - - if: "!(github.event_name == 'push' && contains(github.ref, 'refs/tags/'))" - run: | - cat pyproject.toml | sed 's/\(version = "\)\(.*\)+dirty\("\)/\1\2.dev0+${{ steps.vars.outputs.sha_short }}\3/g' > pyproject.toml.tagged - cat pyproject.toml.tagged - mv pyproject.toml.tagged pyproject.toml - - # Remove the dirty-tag from the builder. - - if: "github.event_name == 'push' && contains(github.ref, 'refs/tags/')" - run: | - cat pyproject.toml | sed 's/\(version = "\)\(.*\)+dirty\("\)/\1\2\3/g' > pyproject.toml.tagged - cat pyproject.toml.tagged - mv pyproject.toml.tagged pyproject.toml - - # Build the distribution. - - run: nix build -L .#dist - - run: | - mkdir dist - cp result/* dist - ls -lAh dist - - name: Archive distribution - uses: actions/upload-artifact@v3 - with: - name: Packages - path: | - dist/**/* - - publish: - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - needs: build - steps: - - uses: actions/download-artifact@v3 - with: - name: Packages - path: dist - - name: Install twine - run: | - pip install twine - - name: Upload VapourSynth - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: | - twine upload dist/* - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..cb03c23 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,63 @@ +name: Tests + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test: + name: Test (Python ${{ matrix.python-version }}, VS ${{ matrix.vapoursynth-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + vapoursynth-version: [69, 70, 71, 72, 73] + python-version: ["3.12", "3.13", "3.14"] + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + + - name: Set up VapourSynth ${{ matrix.vapoursynth-version }} + uses: Jaded-Encoding-Thaumaturgy/setup-vapoursynth@v1 + with: + vapoursynth-version: ${{ matrix.vapoursynth-version }} + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --locked + + - name: Run tests with coverage + run: uv run pytest tests --cov=vsengine --cov-report=xml + + - name: Upload coverage to coveralls + uses: coverallsapp/github-action@v2.3.7 + with: + file: coverage.xml + format: cobertura + parallel: true + flag-name: ${{ join(matrix.*, ' - ') }} + fail-on-error: false + + coverage-finished: + name: Coverage Finished + needs: test + runs-on: ubuntu-latest + steps: + - name: Upload coverage to Coveralls (finish) + uses: coverallsapp/github-action@v2.3.7 + with: + parallel-finished: true + fail-on-error: false diff --git a/pyproject.toml b/pyproject.toml index 20905e7..d34fd71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ trio = ["trio"] dev = [ "mypy>=1.19.0", "pytest>=9.0.1", + "pytest-cov>=7.0.0", "pytest-asyncio>=0.24.0", "ruff>=0.14.7", "trio", diff --git a/uv.lock b/uv.lock index 53f4bfe..ef35dc4 100644 --- a/uv.lock +++ b/uv.lock @@ -59,6 +59,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -278,6 +352,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -411,6 +499,7 @@ dev = [ { name = "mypy" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov" }, { name = "ruff" }, { name = "trio" }, { name = "vsstubs", marker = "python_full_version >= '3.13'" }, @@ -428,6 +517,7 @@ dev = [ { name = "mypy", specifier = ">=1.19.0" }, { name = "pytest", specifier = ">=9.0.1" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.14.7" }, { name = "trio" }, { name = "vsstubs", marker = "python_full_version >= '3.13'" }, From d096510044d483be44470ac00c02578e0dcb4d8e Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 04:01:47 +0100 Subject: [PATCH 59/60] fix lint --- tests/__init__.py | 1 + tests/test_futures.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d4839a6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/tests/test_futures.py b/tests/test_futures.py index 2b3235a..a272933 100644 --- a/tests/test_futures.py +++ b/tests/test_futures.py @@ -298,19 +298,15 @@ def _noop(_: Future[int]) -> None: def test_unified_iterator_can_iter_futures() -> None: - n = 0 - for fut in UnifiedIterator.from_call(future_iterator).futures: + for n, fut in enumerate(UnifiedIterator.from_call(future_iterator).futures): assert n == fut.result() - n += 1 if n > 100: break def test_unified_iterator_can_iter() -> None: - n = 0 - for n2 in UnifiedIterator.from_call(future_iterator): + for n, n2 in enumerate(UnifiedIterator.from_call(future_iterator)): assert n == n2 - n += 1 if n > 100: break From f138eafdf98653f310aa07c83e39e734eecc5fa0 Mon Sep 17 00:00:00 2001 From: Ichunjo Date: Thu, 25 Dec 2025 04:05:40 +0100 Subject: [PATCH 60/60] use future annotations --- tests/test_loop_adapters.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_loop_adapters.py b/tests/test_loop_adapters.py index 694c0d4..d0cbbd3 100644 --- a/tests/test_loop_adapters.py +++ b/tests/test_loop_adapters.py @@ -5,6 +5,8 @@ # SPDX-License-Identifier: EUPL-1.2 """Tests for event loop adapters.""" +from __future__ import annotations + import asyncio import contextlib import threading @@ -30,7 +32,7 @@ def _wrapped(self: AdapterTest, *args: Any, **kwargs: Any) -> Any: def is_async(func: Any) -> Any: """Decorator to run an async test within a loop.""" - def _wrapped(self: "AsyncAdapterTest", *args: Any, **kwargs: Any) -> Any: + def _wrapped(self: AsyncAdapterTest, *args: Any, **kwargs: Any) -> Any: return self.run_within_loop_async(func, args, kwargs) return _wrapped @@ -87,7 +89,7 @@ def test_next_cycle_doesnt_throw_when_not_cancelled(self) -> Iterator[None]: @make_async def test_from_thread_with_success(self) -> Iterator[None]: - def test_func() -> "AdapterTest": + def test_func() -> AdapterTest: return self with self.with_loop() as loop: