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/.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/.gitignore b/.gitignore
index 3f7ce8f..d0d4bb3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -164,3 +164,9 @@ cython_debug/
result
result-*
!nix/lib/
+
+# 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/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);
-}
diff --git a/pyproject.toml b/pyproject.toml
index 52a631b..d34fd71 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,32 +1,122 @@
+[build-system]
+requires = ["hatchling>=1.27.0", "versioningit"]
+build-backend = "hatchling.build"
+
[project]
-name = "vsengine"
-version = "0.2.0+dirty"
-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" }
+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" },
]
-dynamic = ["description"]
-requires-python = ">=3.9"
-
-dependencies = [
- "vapoursynth>=57"
+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.scripts]
-vpy-unittest = "vsengine.tests.unittest:main"
-
-[project.entry-points.pytest11]
-vsengine = "vsengine.tests.pytest"
+[project.urls]
+"Source Code" = "https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine"
+"Contact" = "https://discord.gg/XTpc6Fa9eB"
[project.optional-dependencies]
-trio = [
- "trio"
+trio = ["trio"]
+
+[dependency-groups]
+dev = [
+ "mypy>=1.19.0",
+ "pytest>=9.0.1",
+ "pytest-cov>=7.0.0",
+ "pytest-asyncio>=0.24.0",
+ "ruff>=0.14.7",
+ "trio",
+ "vsstubs ; python_version>='3.13'",
]
-test = [
- "pytest"
+
+[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 = "."
+asyncio_mode = "auto"
+asyncio_default_fixture_loop_scope = "function"
+
+[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
+
+[tool.ruff.format]
+docstring-code-format = true
+
+[tool.ruff.lint]
+extend-select = [
+ "E",
+ "C4",
+ "I",
+ "PYI",
+ "Q",
+ "SIM",
+ "N",
+ "W",
+ "UP",
+ "FURB",
+ "RUF",
]
-[build-system]
-requires = ["flit_core >=3.2,<4"]
-build-backend = "flit_core.buildapi"
+
+[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..8bb64e7
--- /dev/null
+++ b/stubs/vapoursynth/__init__.pyi
@@ -0,0 +1,1565 @@
+# 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
+from fractions import Fraction
+from inspect import Signature
+from logging import Handler, LogRecord, StreamHandler
+from types import MappingProxyType, TracebackType
+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
+
+
+__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/__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/vsengine/_testutils.py b/tests/_testutils.py
similarity index 65%
rename from vsengine/_testutils.py
rename to tests/_testutils.py
index 71fc7da..31c7b70 100644
--- a/vsengine/_testutils.py
+++ b/tests/_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
"""
@@ -8,7 +9,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 +25,23 @@
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 import policy as vsengine_policy
+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"]
-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,21 +49,22 @@ 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")
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
- def forcefully_unregister_policy(self):
+ def forcefully_unregister_policy(self) -> None:
if self._policy is None:
return
if self._api is None:
@@ -84,10 +76,11 @@ 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
+ # 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:
@@ -97,13 +90,14 @@ 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) -> 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)
@@ -114,37 +108,34 @@ def is_alive(self, environment: EnvironmentData) -> bool:
return self._policy.is_alive(environment)
-class StandalonePolicy:
- _current: t.Optional[EnvironmentData]
- _api: t.Optional[EnvironmentPolicyAPI]
- _core: t.Optional[Core]
- __slots__ = ("_current", "_api", "_core")
+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
self._current = special_api.create_environment()
self._core = core.core
- def on_policy_cleared(self):
- assert self._api is not None
-
+ def on_policy_cleared(self) -> None:
admit_environment(self._current, self._core)
- self._current = None
- self._core = None
+ del self._current
+ del self._core
- def get_current_environment(self):
+ def get_current_environment(self) -> EnvironmentData:
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 +148,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 +165,6 @@ def unregister_policy(self):
forcefully_unregister_policy = _policy.forcefully_unregister_policy
-def use_standalone_policy():
+def use_standalone_policy() -> None:
+ """Register a standalone policy for tests that don't need custom policies."""
_policy.attach_policy_to_proxy(StandalonePolicy())
-
-
-def wrap_test_for_asyncio(func):
- import asyncio
- from vsengine.loops import set_loop
- from vsengine.adapters.asyncio import AsyncIOLoop
- def test_case(self):
- async def _run():
- set_loop(AsyncIOLoop())
- await func(self)
- asyncio.run(_run())
- return test_case
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/pytest_core_in_module.py b/tests/fixtures/pytest_core_in_module.py
deleted file mode 100644
index 581005e..0000000
--- a/tests/fixtures/pytest_core_in_module.py
+++ /dev/null
@@ -1,15 +0,0 @@
-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)
-
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 b678503..0000000
--- a/tests/fixtures/pytest_core_stored_in_test.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import pytest
-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 9efe584..0000000
--- a/tests/fixtures/pytest_core_succeeds.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import pytest
-from vapoursynth import core
-
-
-def test_core_succeeds():
- core.std.BlankClip().get_frame(0)
diff --git a/tests/fixtures/test.vpy b/tests/fixtures/test.vpy
index a0f8d10..b2194d4 100644
--- a/tests/fixtures/test.vpy
+++ b/tests/fixtures/test.vpy
@@ -1,10 +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/fixtures/unittest_core_in_module.py b/tests/fixtures/unittest_core_in_module.py
deleted file mode 100644
index 50f6938..0000000
--- a/tests/fixtures/unittest_core_in_module.py
+++ /dev/null
@@ -1,11 +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 e5da8d1..0000000
--- a/tests/fixtures/unittest_core_stored_in_test.py
+++ /dev/null
@@ -1,11 +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 38dd2d7..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 7391216..0000000
--- a/tests/test_convert.py
+++ /dev/null
@@ -1,117 +0,0 @@
-import os
-import json
-import unittest
-
-from vapoursynth import core
-import vapoursynth as vs
-
-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
-# 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, "r") 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):
- 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):
- 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"]]:
- 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"]]:
- count_misses += 1
- else:
- count_hits += 1
-
- self.assertGreaterEqual(count_hits, count_misses)
-
- def test_converts_to_rgb24(self):
- # 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):
- # 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_futures.py b/tests/test_futures.py
index 9dbb95a..a272933 100644
--- a/tests/test_futures.py
+++ b/tests/test_futures.py
@@ -1,325 +1,376 @@
# 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 unittest
-import threading
import contextlib
+import threading
+from collections.abc import AsyncIterator, Iterator
from concurrent.futures import Future
+from typing import Any
+
+import pytest
-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.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
+ 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))
- def _wrapper():
- return fut
+ new_future = future.map(_crash)
+ assert isinstance(new_future.exception(), RuntimeError)
- fut = UnifiedFuture.from_call(_wrapper)
+ future = UnifiedFuture.from_call(fails)
+ new_future = future.map(lambda v: str(v))
+ assert isinstance(new_future.exception(), RuntimeError)
- 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
- self.assertNotEqual(cb_thread, loop_thread)
+def test_unified_future_catch() -> None:
+ def _crash(_: BaseException) -> str:
+ raise RuntimeError("test")
+ future = UnifiedFuture.from_call(fails)
+ new_future = future.catch(lambda e: e.__class__.__name__)
+ assert new_future.result() == "RuntimeError"
-class UnifiedIteratorTest(unittest.TestCase):
+ new_future = future.catch(_crash)
+ assert isinstance(new_future.exception(), RuntimeError)
- 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)
- 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):
+ 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
+
+
+@pytest.mark.asyncio
+async def test_unified_future_add_loop_callback() -> None:
+ from vsengine.adapters.asyncio import AsyncIOLoop
+ from vsengine.loops import set_loop
+
+ 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] = []
+
+ 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(type="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]]:
+ if False:
+ yield Future[int]() # type:ignore[unreachable]
+ 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:
+ for n, fut in enumerate(UnifiedIterator.from_call(future_iterator).futures):
+ assert n == fut.result()
+ if n > 100:
+ break
+
+
+def test_unified_iterator_can_iter() -> None:
+ for n, n2 in enumerate(UnifiedIterator.from_call(future_iterator)):
+ assert n == n2
+ 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 975f5f1..ce7ed80 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -1,117 +1,55 @@
-import unittest
+# 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 vapoursynth import core
-from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy
-from vsengine.policy import Policy, GlobalStore
+from tests._testutils import use_standalone_policy
+from vsengine._helpers import use_inline
+from vsengine.policy import GlobalStore, Policy
-from vsengine._helpers import use_inline, wrap_variable_size
+def test_use_inline_with_standalone() -> None:
+ use_standalone_policy()
+ with use_inline("test_with_standalone", None):
+ pass
-class TestUseInline(unittest.TestCase):
- def setUp(self) -> None:
- forcefully_unregister_policy()
- def tearDown(self) -> None:
- forcefully_unregister_policy()
+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_with_standalone(self):
- use_standalone_policy()
- with use_inline("test_with_standalone", None):
- 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
+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_fails_without_an_environment(self):
- with Policy(GlobalStore()):
- with 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())
+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_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())
-
-
-class TestWrapVariable(unittest.TestCase):
- def setUp(self) -> None:
- forcefully_unregister_policy()
- use_standalone_policy()
-
- def tearDown(self) -> None:
- forcefully_unregister_policy()
-
- 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):
- 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():
- self.assertEqual(int(f.format), vs.RGB24)
-
- self.assertEqual(counter, 2)
- self.assertEqual(int(wrapped.format), vs.RGB24)
-
- def test_wrap_caches_different_sizes(self):
- 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):
- 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)
-
- def test_wrap_stops_caching_once_size_exceeded(self):
- 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):
- nonlocal counter
- counter += 1
- return c.resize.Point(format=vs.RGB24)
-
- wrapped = wrap_variable_size(sp, vs.RGB24, _wrapper)
- for _ in wrapped.frames():
- pass
-
- self.assertGreaterEqual(counter, 101)
-
+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_hospice.py b/tests/test_hospice.py
index 7241649..ee67441 100644
--- a/tests/test_hospice.py
+++ b/tests/test_hospice.py
@@ -1,97 +1,172 @@
# 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 weakref
import logging
-import contextlib
-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
finally:
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()
+def test_hospice_is_delayed_on_alive_objects(caplog: pytest.LogCaptureFixture) -> None:
+ o1 = MockEnv()
+ o2 = MockCore()
+ o2r = weakref.ref(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()
- self.assertIsNone(o2r())
+ assert len(caplog.records) > 0
- def test_hospice_reports_alive_objects_correctly(self):
- o1 = Obj()
- o2 = Obj()
- admit_environment(o1, o2)
- del o1
+ # Delete local ref but keep registry - still should delay collection
+ del o2
+ assert o2r() is not None
- 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
+ # Now clear registry to allow collection
+ _mock_timings_registry.clear()
- self.assertFalse(any_alive(), "The hospice did report that there are some objects left alive. This is obviously not true.")
+ gc.collect()
+ gc.collect()
+ gc.collect()
- def test_hospice_can_forget_about_cores_safely(self):
- o1 = Obj()
- o2 = Obj()
- admit_environment(o1, o2)
- del o1
+ assert o2r() is None
- 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
+def test_hospice_reports_alive_objects_correctly() -> None:
+ o1 = MockEnv()
+ o2 = MockCore()
+ admit_environment(o1, o2) # type:ignore[arg-type]
+ del o1
- gc.collect()
- gc.collect()
+ # 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()
diff --git a/tests/test_loop_adapters.py b/tests/test_loop_adapters.py
index dca73b4..d0cbbd3 100644
--- a/tests/test_loop_adapters.py
+++ b/tests/test_loop_adapters.py
@@ -1,35 +1,48 @@
# 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 contextlib
-import threading
-import unittest
+from __future__ import annotations
import asyncio
+import contextlib
+import threading
+from collections.abc import Generator, Iterator
+from concurrent.futures import CancelledError, Future
+from typing import Any
-from concurrent.futures import Future, CancelledError
+import pytest
-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, 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,69 +53,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):
- with self.with_loop() as loop:
- with loop.wrap_cancelled():
- pass
+ 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:
- with self.assertCancelled():
- with loop.wrap_cancelled():
- raise 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:
- with self.assertRaises(RuntimeError):
- with loop.wrap_cancelled():
- raise RuntimeError()
+ 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_func(*args, **kwargs):
+ def test_from_thread_forwards_correctly(self) -> Iterator[None]:
+ a: tuple[Any, ...] | None = None
+ k: dict[str, Any] | None = None
+
+ def test_func(*args: Any, **kwargs: Any) -> None:
nonlocal a, k
a = args
k = kwargs
@@ -111,47 +122,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:
- with self.assertRaises(RuntimeError):
- yield from self.resolve_to_thread_future(loop.to_thread(test_func))
+
+ 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_func(*args, **kwargs):
+ def test_to_thread_forwards_correctly(self) -> Iterator[None]:
+ a: tuple[Any, ...] | None = None
+ k: dict[str, Any] | None = None
+
+ 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):
+ """Base class for async event loop adapter tests."""
- def run_within_loop(self, func, args, kwargs):
- async def wrapped(_):
+ 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:
@@ -159,82 +170,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()
- def _setter():
+ fut: Future[int] = Future()
+
+ 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()
- def _setter():
+ fut: Future[int] = Future()
+
+ 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
+ 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()
@@ -243,31 +257,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
- async def _awaiter():
+ result: Any = None
+ error: BaseException | None = None
+
+ async def _awaiter() -> None:
nonlocal done, error, result
try:
- result = await fut
+ result = await make_awaitable(fut)
except BaseException as e:
error = e
finally:
@@ -283,12 +304,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
-
diff --git a/tests/test_loops.py b/tests/test_loops.py
index 3392c0e..d870724 100644
--- a/tests/test_loops.py
+++ b/tests/test_loops.py
@@ -1,46 +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 unittest
import threading
-from concurrent.futures import Future, CancelledError
+from concurrent.futures import CancelledError, Future
+from typing import Any, NoReturn
+import pytest
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):
+ """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 attach(self) -> None: ...
+
+ def detach(self) -> None: ...
- def detach(self):
- pass
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:
@@ -50,98 +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):
+# NoLoop tests
- def test_wrap_cancelled_converts_the_exception(self) -> None:
- loop = _NoEventLoop()
- with self.assertRaises(CancelledError):
- with loop.wrap_cancelled():
- raise Cancelled
+def test_no_loop_wrap_cancelled_converts_the_exception() -> None:
+ loop = _NoEventLoop()
+ with pytest.raises(CancelledError), loop.wrap_cancelled():
+ raise Cancelled
-class LoopApiTest(unittest.TestCase):
+# Loop API tests
- def tearDown(self) -> None:
- forcefully_unregister_policy()
- def test_loop_can_override(self):
- loop = _NoEventLoop()
- set_loop(loop)
- self.assertIs(get_loop(), loop)
+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_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:
- with 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:
- with 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
+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 e728352..731ee83 100644
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -1,123 +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 vsengine._testutils import forcefully_unregister_policy
+from vsengine.policy import GlobalStore, Policy
-from vsengine.policy import GlobalStore
-from vsengine.policy import Policy
+@pytest.fixture
+def policy() -> Iterator[Policy]:
+ """Fixture that provides a fresh Policy instance."""
+ p = Policy(GlobalStore())
+ yield p
+ with contextlib.suppress(RuntimeError):
+ p.unregister()
-class PolicyTest(unittest.TestCase):
- def setUp(self) -> None:
- forcefully_unregister_policy()
- self.policy = Policy(GlobalStore())
-
- 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())
-
- try:
- self.policy.unregister()
- except:
- pass
+ with pytest.raises(RuntimeError):
+ policy.api.create_environment()
-class ManagedEnvironmentTest(unittest.TestCase):
+class TestManagedEnvironment:
+ """Tests for ManagedEnvironment functionality."""
- 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:
- with self.policy.new_environment() as env2:
- with env1.use():
- vapoursynth.core.std.BlankClip().set_output(0)
+ 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:
- with 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:
- with self.policy.new_environment() as env2:
- env1.switch()
+ 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())
+ with env2.inline_section():
+ 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 91b203a..1f0ba91 100644
--- a/tests/test_policy_store.py
+++ b/tests/test_policy_store.py
@@ -1,89 +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
-import unittest
+"""Tests for the policy environment stores."""
import concurrent.futures as futures
+from collections.abc import Iterator
from contextvars import copy_context
+from typing import Any
-from vsengine.policy import GlobalStore, ThreadLocalStore, ContextVarStore
-from vsengine.policy import EnvironmentStore
+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):
+class TestGlobalStore(BaseStoreTest):
+ """Tests for GlobalStore."""
- def create_store(self) -> EnvironmentStore:
+ def create_store(self) -> GlobalStore:
return GlobalStore()
-class TestThreadLocalStore(BaseStoreTest, unittest.TestCase):
+class TestThreadLocalStore(BaseStoreTest):
+ """Tests for ThreadLocalStore."""
- def create_store(self) -> EnvironmentStore:
+ 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):
+class TestContextVarStore(BaseStoreTest):
+ """Tests for ContextVarStore."""
- def create_store(self) -> EnvironmentStore:
+ 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)
-
- self.store.set_current_environment(2)
- self.assertEqual(self.store.get_current_environment(), 2)
+ 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
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_tests_pytest.py b/tests/test_tests_pytest.py
deleted file mode 100644
index 6a7ed2b..0000000
--- a/tests/test_tests_pytest.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import os
-import sys
-import platform
-import unittest
-import subprocess
-
-
-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 a7c0c35..0000000
--- a/tests/test_tests_unittest.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import os
-import sys
-import unittest
-import subprocess
-
-
-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)
-
diff --git a/tests/test_video.py b/tests/test_video.py
index 97a21cb..4288fd5 100644
--- a/tests/test_video.py
+++ b/tests/test_video.py
@@ -1,113 +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 typing as t
-import unittest
-
-from vsengine._testutils import forcefully_unregister_policy, use_standalone_policy
-
-from vapoursynth import core, PresetFormat, VideoFormat, GRAY8, RGB24
-from vapoursynth import VideoNode, VideoFrame
-
-from vsengine.video import frame, planes, frames, render
-
-
-AnyFormat = t.Union[PresetFormat, 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")
+"""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 use_standalone_policy
+from vsengine.video import frame, frames, planes, render
+
+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 306eb52..8e4f6d7 100644
--- a/tests/test_vpy.py
+++ b/tests/test_vpy.py
@@ -1,393 +1,393 @@
# 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 os
import ast
-import types
-import unittest
+import contextlib
+import os
import textwrap
import threading
-import contextlib
+import types
+from collections.abc import Callable, Iterator
+from typing import Any
+import pytest
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.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
-
-
-DIR = os.path.dirname(__file__)
-PATH = os.path.join(DIR, "fixtures", "test.vpy")
+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 (
+ ExecutionError,
+ Script,
+ WrapAllErrors,
+ _load,
+ chdir_runner,
+ inline_runner,
+ load_code,
+ load_script,
+)
+
+DIR: str = os.path.dirname(__file__)
+PATH: str = os.path.join(DIR, "fixtures", "test.vpy")
@contextlib.contextmanager
-def noop():
+def noop() -> Iterator[None]:
yield
-class TestException(Exception): pass
+class TestError(Exception):
+ pass
-def callback_script(func):
- def _script(ctx, module):
+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
-class ScriptTest(unittest.TestCase):
-
- def setUp(self) -> None:
- forcefully_unregister_policy()
-
- def tearDown(self) -> None:
- forcefully_unregister_policy()
- set_loop(NO_LOOP)
-
- 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()
- self.assertTrue(run)
-
- def test_run_wraps_exception(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)
- 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()
- self.assertTrue(run)
-
- def test_execute_resolves_to_script(self):
- @callback_script
- def test_code(_):
- pass
+def test_run_executes_successfully() -> None:
+ run = False
- 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)
-
- 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.")
-
- @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()
- 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)
- 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()
-
- 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)
-
- try:
- script.dispose()
- except:
- env.dispose()
- raise
-
- 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)
-
- 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
- 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:
- 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)
-
- 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:
- 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)
-
- 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())
-
- 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())
-
- 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:
- with p.new_environment() as env:
- with 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:
- with p.new_environment() as env:
- with 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:
- with p.new_environment() as env:
- with 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)
-
- 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__")
-
- 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)
-
- def test_wrap_exceptions_wraps_exception(self):
- err = RuntimeError()
+ @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)
+ s.run()
+
+ assert run
+
+
+def test_run_wraps_exception() -> None:
+ @callback_script
+ def test_code(_: types.ModuleType) -> None:
+ 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, ExecutionError)
+ assert isinstance(exc.parent_error, TestError)
+
+
+def test_execute_resolves_immediately() -> 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)
+ s.result()
+
+ assert run
+
+
+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:
+ s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner)
+ s.result()
+
+
+def test_execute_resolves_immediately_when_raising() -> None:
+ @callback_script
+ def test_code(_: types.ModuleType) -> None:
+ 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:
- with WrapAllErrors():
- raise err
- except ExecutionFailed as e:
- self.assertIs(e.parent_error, err)
+ s.result()
+ except ExecutionError as err:
+ assert isinstance(err.parent_error, TestError)
+ 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:
+ set_loop(AsyncIOLoop())
+ 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:
+ set_loop(AsyncIOLoop())
+ 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 = Script(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 Script(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) # type: ignore[arg-type]
+ 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) # type: ignore[arg-type]
+ 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, "__vapoursynth__", 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, "__vapoursynth__", 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, "__test_1__", inline=True, chdir=None)
+ env = script1.environment
+ try:
+ script1.result()
+ script2 = _load(test_code_2, script1, "__test_2__", 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, "__test_1__", inline=True, chdir=None)
+ script1.result()
+ finally:
+ script1.dispose() # pyright: ignore[reportPossiblyUnboundVariable]
+
+ try:
+ script2 = _load(test_code_2, p, "__test_2__", inline=True, chdir=None)
+ script2.result()
+ finally:
+ script2.dispose() # pyright: ignore[reportPossiblyUnboundVariable]
+
+
+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, "__vapoursynth__", 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, "__vapoursynth__", 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, "__vapoursynth__", True, chdir=None).result()
+ assert thread is threading.current_thread()
+
+
+def test_code_runs_string() -> None:
+ 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()
+ assert BLACKBOARD.get("vpy_test_runs_raw_code_str") is True
+
+
+def test_code_runs_bytes() -> None:
+ code = textwrap.dedent("""
+ # encoding: latin-1
+ 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()
+ assert BLACKBOARD.get("vpy_test_runs_raw_code_bytes") is True
+
+
+def test_code_runs_ast() -> None:
+ code = ast.parse(
+ textwrap.dedent("""
+ 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()
+ 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_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_script(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 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
new file mode 100644
index 0000000..ef35dc4
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,538 @@
+version = 1
+revision = 3
+requires-python = ">=3.12"
+resolution-markers = [
+ "python_full_version >= '3.13'",
+ "python_full_version < '3.13'",
+]
+
+[[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 = "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"
+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 = "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"
+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.7.4"
+source = { registry = "https://pypi.org/simple" }
+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/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]]
+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.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "typing-extensions" },
+]
+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/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]]
+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.2"
+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/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/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 = "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"
+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.10"
+source = { registry = "https://pypi.org/simple" }
+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/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]]
+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"
+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 = "typer"
+version = "0.20.1"
+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/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/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]]
+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-jet"
+source = { editable = "." }
+dependencies = [
+ { name = "vapoursynth" },
+]
+
+[package.optional-dependencies]
+trio = [
+ { name = "trio" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
+ { name = "trio" },
+ { name = "vsstubs", marker = "python_full_version >= '3.13'" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "trio", marker = "extra == 'trio'" },
+ { name = "vapoursynth", specifier = ">=69" },
+]
+provides-extras = ["trio"]
+
+[package.metadata.requires-dev]
+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'" },
+]
+
+[[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" },
+]
diff --git a/vsengine/__init__.py b/vsengine/__init__.py
index 2c75348..0038ce8 100644
--- a/vsengine/__init__.py
+++ b/vsengine/__init__.py
@@ -1,13 +1,28 @@
# 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
"""
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).
"""
+
+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/_futures.py b/vsengine/_futures.py
index 3587c32..9202c46 100644
--- a/vsengine/_futures.py
+++ b/vsengine/_futures.py
@@ -1,77 +1,77 @@
# 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 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, Literal, Self, overload
from vsengine.loops import Cancelled, get_loop, keep_environment
-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[T, Any], AbstractAsyncContextManager[T, 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: Callable[P, Future[T]], *args: P.args, **kwargs: P.kwargs) -> Self:
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)
@classmethod
- def from_future(cls, future: Future[T]) -> 'UnifiedFuture[T]':
+ def from_future(cls, future: Future[T]) -> Self:
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) -> Self:
future = cls()
future.set_result(value)
return future
@classmethod
- def reject(cls, error: BaseException) -> 'UnifiedFuture[t.Any]':
+ def reject(cls, error: BaseException) -> Self:
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[[Future[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(cb: Callable[[Any], V], v: Any) -> None:
try:
r = cb(v)
except BaseException as e:
@@ -79,7 +79,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 +89,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[arg-type]
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[T | V]:
return self.then(None, cb)
# Nicer Syntax
- def __enter__(self):
+ def __enter__(self) -> T:
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 object")
+
+ 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 object")
+
+ 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 object")
+
+ 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 object")
-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: Callable[P, Iterator[Future[T]]], *args: P.args, **kwargs: P.kwargs) -> Self:
+ return cls(func(*args, **kwargs))
@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 +181,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 +208,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 +251,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 +269,100 @@ 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):
+@overload
+def unified[T, **P](
+ *,
+ kind: Literal["generator"],
+) -> Callable[
+ [Callable[P, Iterator[Future[T]]]],
+ Callable[P, UnifiedIterator[T]],
+]: ...
+
+
+@overload
+def unified[T, **P](
+ *,
+ kind: Literal["future"],
+) -> Callable[
+ [Callable[P, Future[T]]],
+ Callable[P, UnifiedFuture[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](
+ *,
+ kind: Literal["future"],
+ future_class: type[UnifiedFuture[T]],
+) -> Callable[
+ [Callable[P, Future[T]]],
+ Callable[P, UnifiedFuture[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](
+ *,
+ 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: P.args, **kwargs: P.kwargs) -> 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 _decorator_future(func: Callable[P, Future[T]]) -> Callable[P, UnifiedFuture[T]]:
+ @wraps(func)
+ 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]) -> UnifiedCallable:
- if type == "auto":
- if inspect.isgeneratorfunction(func):
- return _wrap_generator(func)
- else:
- return _wrap_future(func)
- elif type == "generator":
- return _wrap_generator(func)
- else:
- return _wrap_future(func)
+ 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 _decorator_generator(func)
+ return _decorator_future(func) # type:ignore[arg-type]
+
+ if kind == "generator":
+ return _decorator_generator(func) # type:ignore[arg-type]
+
+ if kind == "future":
+ return _decorator_future(func) # type:ignore[arg-type]
- return _wrapper
+ raise NotImplementedError
+ return decorator
diff --git a/vsengine/_helpers.py b/vsengine/_helpers.py
index 50e86c1..062bcb2 100644
--- a/vsengine/_helpers.py
+++ b/vsengine/_helpers.py
@@ -1,29 +1,25 @@
# 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
-import typing as t
+from collections.abc import Iterator
+
import vapoursynth as vs
from vsengine.policy import ManagedEnvironment
-T = t.TypeVar("T")
-
-
-EnvironmentTypes = t.Union[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: vs.Environment | ManagedEnvironment | 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
@@ -36,49 +32,3 @@ def use_inline(function_name: str, env: t.Optional[EnvironmentTypes]) -> t.Gener
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: t.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 = {}
- 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/_hospice.py b/vsengine/_hospice.py
index f890575..b58f3f3 100644
--- a/vsengine/_hospice.py
+++ b/vsengine/_hospice.py
@@ -1,12 +1,15 @@
# 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
-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,30 +17,31 @@
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
refnanny[ident] = ref
- logger.info(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():
+
+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 +51,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,36 +61,40 @@ def freeze():
stage2.clear()
stage2_to_add.clear()
-def unfreeze():
+
+def unfreeze() -> None:
stage1.update(hold)
hold.clear()
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
def _add_tostage1(ident: int) -> None:
- logger.info(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)
-def _collectstage1(phase, __):
+def _collectstage1(phase: Literal["start", "stop"], _: dict[str, int]) -> None:
if phase != "stop":
return
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)
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":
@@ -96,12 +104,12 @@ def _collectstage2(phase, __):
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("Core is still in use in stage 2. ID:%s", ident)
continue
stage2.remove(ident)
garbage.append(cores.pop(ident))
- logger.info(f"Marking core {ident!r} for collection")
+ logger.debug("Marking core %r for collection", ident)
stage2.update(stage2_to_add)
stage2_to_add = set()
@@ -111,4 +119,3 @@ def _collectstage2(phase, __):
gc.callbacks.append(_collectstage2)
gc.callbacks.append(_collectstage1)
-
diff --git a/vsengine/_nodes.py b/vsengine/_nodes.py
index 6ec3e23..b81974d 100644
--- a/vsengine/_nodes.py
+++ b/vsengine/_nodes.py
@@ -1,22 +1,22 @@
# 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 typing as t
+from collections.abc import Iterable, Iterator
from concurrent.futures import Future
from threading import RLock
-from vapoursynth import core
+from vapoursynth import RawFrame, core
-T = t.TypeVar("T")
-T_co = t.TypeVar("T_co", covariant=True)
-
-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[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:
- 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[FrameT]]()
- 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[FrameT]) -> None:
nonlocal finished, running
with lock:
running -= 1
@@ -54,24 +54,24 @@ 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 +88,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[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[FrameT]) -> None:
try:
r = fut.result()
except Exception as e:
@@ -103,10 +104,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[FrameT]) -> None:
+ def _do_close(_: Future[FrameT]) -> 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/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 b26bb98..1fffa8f 100644
--- a/vsengine/adapters/asyncio.py
+++ b/vsengine/adapters/asyncio.py
@@ -1,47 +1,34 @@
# 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 typing as t
import asyncio
import contextlib
import contextvars
+from collections.abc import Callable, Iterator
from concurrent.futures import Future
-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):
- pass
-
- def detach(self):
- pass
-
- def from_thread(
- self,
- func: t.Callable[..., T],
- *args: t.Any,
- **kwargs: t.Any
- ) -> Future[T]:
- future = Future()
+ 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()
- def _wrap():
+
+ def _wrap() -> None:
if not future.set_running_or_notify_cancel():
return
@@ -55,32 +42,44 @@ def _wrap():
self.loop.call_soon_threadsafe(_wrap)
return future
- def to_thread(self, func, *args, **kwargs):
+ def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
ctx = contextvars.copy_context()
- def _wrap():
+ future = Future[R]()
+
+ 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(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()
+ 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
+ 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):
+ 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..a44033f 100644
--- a/vsengine/adapters/trio.py
+++ b/vsengine/adapters/trio.py
@@ -1,66 +1,43 @@
# 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 concurrent.futures import Future
-import typing as t
import contextlib
+from collections.abc import Callable, Iterator
+from concurrent.futures import Future
-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
+import trio
from vsengine.loops import Cancelled, EventLoop
-T = t.TypeVar("T")
-
-
class TrioEventLoop(EventLoop):
- _scope: Nursery
+ """
+ Bridges vs-engine to Trio.
+ """
- def __init__(
- self,
- nursery: Nursery,
- limiter: t.Optional[CapacityLimiter]=None
- ) -> None:
+ def __init__(self, nursery: trio.Nursery, limiter: trio.CapacityLimiter | None = None) -> None:
if limiter is None:
- limiter = t.cast(CapacityLimiter, to_thread.current_default_thread_limiter())
+ limiter = trio.to_thread.current_default_thread_limiter()
self.nursery = nursery
self.limiter = limiter
- self._token = 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(
- self,
- func: t.Callable[..., T],
- *args: t.Any,
- **kwargs: t.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()
- def _executor():
+ fut = Future[R]()
+
+ def _executor() -> None:
if not fut.set_running_or_notify_cancel():
return
@@ -70,78 +47,61 @@ 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):
- """
- Run this function in a worker thread.
- """
- result = None
- error: BaseException|None = None
- def _executor():
- nonlocal result, error
- try:
- result = func(*args, **kwargs)
- except BaseException as e:
- error = e
+ def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
+ future = Future[R]()
- await to_thread.run_sync(_executor, limiter=self.limiter)
- if error is not None:
- assert isinstance(error, BaseException)
- raise t.cast(BaseException, error)
- else:
- return result
+ 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 trio.to_thread.run_sync(_executor, limiter=self.limiter)
+
+ self.nursery.start_soon(_run)
+ return future
def next_cycle(self) -> Future[None]:
- scope = CancelScope()
- future = Future()
- def continuation():
+ scope = trio.CancelScope()
+ future = Future[None]()
+
+ 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:
- """
- 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
- def _when_done(_):
- nonlocal error, result
- if (error := future.exception()) is not None:
- pass
- else:
- result = future.result()
+ async def await_future[T](self, future: Future[T]) -> T:
+ event = trio.Event()
+
+ def _when_done(_: Future[T]) -> None:
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 t.cast(BaseException, error)
- else:
- return t.cast(T, result)
+ raise exc
@contextlib.contextmanager
- def wrap_cancelled(self):
- """
- Wraps vsengine.loops.Cancelled into the native cancellation error.
- """
+ def wrap_cancelled(self) -> Iterator[None]:
try:
yield
except Cancelled:
- raise TrioCancelled.__new__(TrioCancelled) from None
+ raise trio.Cancelled.__new__(trio.Cancelled) from None
diff --git a/vsengine/convert.py b/vsengine/convert.py
deleted file mode 100644
index 0b1780f..0000000
--- a/vsengine/convert.py
+++ /dev/null
@@ -1,189 +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 functools
-import typing as t
-import vapoursynth as vs
-
-from vsengine._helpers import use_inline, wrap_variable_size, EnvironmentTypes
-
-
-# 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]:
- 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: t.Dict[str, t.Any],
- scaler: t.Union[str, t.Callable[..., 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: t.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: 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,
-) -> 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
- )
-
diff --git a/vsengine/loops.py b/vsengine/loops.py
index 293a7dd..55f3ef5 100644
--- a/vsengine/loops.py
+++ b/vsengine/loops.py
@@ -1,75 +1,87 @@
# 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 concurrent.futures import Future, CancelledError
-import contextlib
-import functools
-import typing as t
-import vapoursynth
+"""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
+from functools import wraps
-T = t.TypeVar("T")
-T_co = t.TypeVar("T_co", covariant=True)
+import vapoursynth as vs
+__all__ = ["Cancelled", "EventLoop", "from_thread", "get_loop", "keep_environment", "set_loop", "to_thread"]
-__all__ = [
- "EventLoop", "Cancelled",
- "get_loop", "set_loop",
- "to_thread", "from_thread", "keep_environment"
-]
+class Cancelled(Exception): # noqa: N818
+ """Exception raised when an operation has been cancelled."""
-class Cancelled(Exception): pass
-
-@contextlib.contextmanager
-def _noop():
+@contextmanager
+def _noop() -> Iterator[None]:
yield
-DONE = Future()
+DONE = Future[None]()
DONE.set_result(None)
-class EventLoop:
+class EventLoop(ABC):
"""
- These functions must be implemented to bridge VapourSynth
- with the event-loop of your choice.
+ 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).
"""
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(
- self,
- func: t.Callable[..., T],
- *args: t.Any,
- **kwargs: t.Any
- ) -> Future[T]:
+
+ @abstractmethod
+ 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.
"""
- ...
- def to_thread(self, func: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any) -> t.Any:
+ 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()
- def wrapper():
+ fut = Future[R]()
+
+ def wrapper() -> None:
if not fut.set_running_or_notify_cancel():
return
@@ -80,38 +92,46 @@ def wrapper():
else:
fut.set_result(result)
- import threading
threading.Thread(target=wrapper).start()
+
return fut
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()
+ 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.
+ 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
- @contextlib.contextmanager
- def wrap_cancelled(self):
+ @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
@@ -121,25 +141,13 @@ def wrap_cancelled(self):
class _NoEventLoop(EventLoop):
"""
- This is the default event-loop used by
- """
-
- def attach(self) -> None:
- pass
-
- def detach(self) -> None:
- pass
+ The default event-loop implementation.
- def next_cycle(self) -> Future[None]:
- return DONE
+ This is used when no specific loop is attached. It runs operations synchronously/inline.
+ """
- def from_thread(
- self,
- func: t.Callable[..., T],
- *args: t.Any,
- **kwargs: t.Any
- ) -> Future[T]:
- fut = Future()
+ 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:
@@ -148,100 +156,114 @@ def from_thread(
fut.set_result(result)
return fut
+ def next_cycle(self) -> Future[None]:
+ return DONE
+
NO_LOOP = _NoEventLoop()
-current_loop = NO_LOOP
+_current_loop: EventLoop = NO_LOOP
def get_loop() -> EventLoop:
"""
- :return: The currently running loop.
+ Retrieve the currently active event loop.
+
+ :return: The currently running EventLoop instance.
"""
- return current_loop
+ 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()
+ 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
-def keep_environment(func: t.Callable[..., T]) -> 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.
+ Decorate a function to preserve the VapourSynth environment.
- :param func: A function to decorate.
- :returns: A wrapped function that keeps the 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: The function to decorate.
+ :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
- @functools.wraps(func)
- def _wrapper(*args, **kwargs):
+ @wraps(func)
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
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[**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.
+
+ This preserves the currently running VapourSynth environment (if any).
- .. note:: Be aware that the function might be called inline!
+ .. note::
+ Depending on the loop implementation, the function might be called inline.
- :param func: A function to call inside the current event loop.
+ :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
- def _wrapper():
+ def _wrapper() -> R:
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[**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.
- :param func: A function to call inside the current event loop.
+ This preserves the currently running VapourSynth environment (if any).
+
+ :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 Future representing the execution result.
"""
+
@keep_environment
- def _wrapper():
+ def _wrapper() -> R:
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.
+ 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 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..2fbdc46 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
"""
@@ -7,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.
@@ -31,13 +30,13 @@
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,
- 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.
@@ -58,63 +57,61 @@
When reloading the application, you can call policy.unregister()
"""
-import typing as t
-import logging
-import weakref
-import threading
-import contextlib
-import contextvars
+from __future__ import annotations
-from vsengine._hospice import admit_environment
+import threading
+from abc import ABC, abstractmethod
+from collections.abc import Iterator
+from contextlib import AbstractContextManager, contextmanager
+from contextvars import ContextVar
+from logging import getLogger
+from types import MappingProxyType, TracebackType
+from typing import TYPE_CHECKING, Self
+from weakref import ReferenceType, ref
-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__)
+logger = getLogger(__name__)
-class EnvironmentStore(t.Protocol):
+class EnvironmentStore(ABC):
"""
Environment Stores manage which environment is currently active.
"""
- def set_current_environment(self, environment: t.Any):
+
+ @abstractmethod
+ def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
"""
Set the current environment in the store.
"""
- ...
- def get_current_environment(self) -> t.Any:
+ @abstractmethod
+ def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
"""
Retrieve the current environment from the store (if any)
"""
- ...
class GlobalStore(EnvironmentStore):
"""
This is the simplest store: It just stores the environment in a variable.
"""
- _current: t.Optional[EnvironmentData]
+
+ _current: ReferenceType[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: ReferenceType[EnvironmentData] | None) -> None:
self._current = environment
- def get_current_environment(self) -> t.Optional[EnvironmentData]:
- return self._current
+ def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
+ return getattr(self, "_current", None)
class ThreadLocalStore(EnvironmentStore):
@@ -129,10 +126,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: ReferenceType[EnvironmentData] | None) -> None:
self._current.environment = environment
- def get_current_environment(self) -> t.Optional[EnvironmentData]:
+ def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
return getattr(self._current, "environment", None)
@@ -140,15 +137,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:
- self._current = contextvars.ContextVar(name)
+ _current: ContextVar[ReferenceType[EnvironmentData] | None]
- def set_current_environment(self, environment: t.Optional[EnvironmentData]):
+ def __init__(self, name: str = "vapoursynth") -> None:
+ self._current = ContextVar(name)
+
+ def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
self._current.set(environment)
- def get_current_environment(self) -> t.Optional[EnvironmentData]:
+ def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
return self._current.get(None)
@@ -157,105 +155,114 @@ class _ManagedPolicy(EnvironmentPolicy):
This class directly interfaces with VapourSynth.
"""
- _api: t.Optional[EnvironmentPolicyAPI]
- _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
self._mutex = threading.Lock()
- self._api = None
self._local = threading.local()
# For engine-calls that require vapoursynth but
# 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):
- self._local.environment = None
+ def inline_section_end(self) -> None:
+ del self._local.environment
@property
- def api(self):
- if self._api is None:
- raise RuntimeError("Invalid state: No access to the current API")
- return self._api
+ def api(self) -> EnvironmentPolicyAPI:
+ 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
+ logger.debug("Environment policy %r successfully registered with VapourSynth.", special_api)
def on_policy_cleared(self) -> None:
- self._api = None
- logger.debug("Policy cleared.")
+ del self._api
+ logger.debug("Environment policy successfully 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}")
+ logger.warning("Environment reference from store resolved to dead object: %r", current_environment)
self._store.set_current_environment(None)
return 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}")
+ logger.warning(
+ "Received environment object is not alive (Garbage collected?): %r",
+ received_environment,
+ )
# Remove the environment.
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()
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:
- self._store.set_current_environment(weakref.ref(environment))
+ self._store.set_current_environment(ref(environment))
if previous_environment is not None:
return previous_environment()
+ return None
-class ManagedEnvironment:
- _environment: Environment
- _data: EnvironmentData
- _policy: 'Policy'
- __slots__ = ("_environment", "_data", "_policy")
- def __init__(self, environment: Environment, data: EnvironmentData, policy: 'Policy') -> None:
+class ManagedEnvironment(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:
self._environment = environment
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):
+ def vs_environment(self) -> Environment:
"""
Returns the vapoursynth.Environment-object representing this environment.
"""
@@ -270,15 +277,15 @@ def core(self) -> vs.Core:
return vs.core.core
@property
- def outputs(self) -> t.Mapping[int, vs.VideoOutputTuple]:
+ def outputs(self) -> MappingProxyType[int, vs.VideoOutputTuple | vs.AudioNode]:
"""
Returns the output within this environment.
"""
with self.inline_section():
return vs.get_outputs()
- @contextlib.contextmanager
- def inline_section(self) -> t.Generator[None, None, None]:
+ @contextmanager
+ def inline_section(self) -> Iterator[None]:
"""
Private API!
@@ -287,7 +294,7 @@ 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!
@@ -300,57 +307,54 @@ def inline_section(self) -> t.Generator[None, None, None]:
finally:
self._policy.managed.inline_section_end()
- @contextlib.contextmanager
- def use(self) -> t.Generator[None, None, None]:
+ @contextmanager
+ 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):
+ 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}.")
+ logger.debug("Starting disposal of environment: %r", self._data)
+
admit_environment(self._data, self.core)
self._policy.api.destroy_environment(self._data)
- self._data = None
+ del self._data
@property
def disposed(self) -> bool:
"""
Checks if the environment is disposed
"""
- return self._data is None
+ return not hasattr(self, "_data")
- def __enter__(self):
- return self
-
- def __exit__(self, _, __, ___):
- self.dispose()
-
- def __del__(self):
- if self._data is None:
+ def __del__(self) -> None:
+ if self.disposed:
return
import warnings
+
warnings.warn(f"Disposing {self!r} inside __del__. This might cause leaks.", ResourceWarning)
self.dispose()
-class Policy:
+class Policy(AbstractContextManager["Policy"]):
"""
A managed policy is a very simple policy that just stores the environment
data within the given store.
@@ -358,28 +362,30 @@ 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:
+ def __init__(self, store: EnvironmentStore, flags_creation: int = 0) -> None:
self._managed = _ManagedPolicy(store)
+ self.flags_creation = flags_creation
- 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:
@@ -392,13 +398,16 @@ 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)
+
+ try:
+ return ManagedEnvironment(env, data, self)
+ finally:
+ logger.debug("Successfully created new environment %r", data)
@property
- def api(self):
+ def api(self) -> EnvironmentPolicyAPI:
"""
Returns the API instance for more complex interactions.
@@ -407,11 +416,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/tests/__init__.py b/vsengine/tests/__init__.py
deleted file mode 100644
index dcb0bee..0000000
--- a/vsengine/tests/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# vs-engine
-# Copyright (C) 2022 cid-chan
-# This project is licensed under the EUPL-1.2
-# SPDX-License-Identifier: EUPL-1.2
-
diff --git a/vsengine/tests/pytest.py b/vsengine/tests/pytest.py
deleted file mode 100644
index b462350..0000000
--- a/vsengine/tests/pytest.py
+++ /dev/null
@@ -1,226 +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.policy import Policy, GlobalStore
-from vsengine._hospice import any_alive, freeze
-
-
-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/vsengine/tests/unittest.py b/vsengine/tests/unittest.py
deleted file mode 100644
index 7e98a07..0000000
--- a/vsengine/tests/unittest.py
+++ /dev/null
@@ -1,80 +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.policy import Policy, GlobalStore
-from vsengine._hospice import any_alive, freeze
-
-
-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:
- with 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:
- with 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()
-
diff --git a/vsengine/video.py b/vsengine/video.py
index 510eb9f..047680e 100644
--- a/vsengine/video.py
+++ b/vsengine/video.py
@@ -1,64 +1,93 @@
# 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
"""
vsengine.render renders video frames for you.
"""
-import typing as t
+
+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 use_inline
+from vsengine._nodes import buffer_futures, close_when_needed
+from vsengine.policy import ManagedEnvironment
+
+__all__ = ["frame", "frames", "planes", "render"]
-from vsengine._futures import unified, UnifiedFuture
-from vsengine._nodes import close_when_needed, buffer_futures
-from vsengine._helpers import use_inline, EnvironmentTypes
-@unified()
+@unified(kind="future")
def frame(
- node: vapoursynth.VideoNode,
- frameno: int,
- env: t.Optional[EnvironmentTypes]=None
-) -> Future[vapoursynth.VideoFrame]:
+ 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)
-@unified()
+@unified(kind="future")
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: vs.VideoNode,
+ frameno: int,
+ env: vs.Environment | ManagedEnvironment | None = None,
+ *,
+ 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.
# 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")
+@unified(kind="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: vs.VideoNode,
+ env: vs.Environment | ManagedEnvironment | 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[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)
@@ -70,65 +99,78 @@ def frames(
if close:
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,
+ return it
- y4m: bool = False
-) -> t.Iterable[Future[t.Tuple[int, bytes]]]:
+@unified(kind="generator")
+def render(
+ node: vs.VideoNode,
+ env: vs.Environment | ManagedEnvironment | None = None,
+ *,
+ prefetch: int = 0,
+ 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:
- 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 + ' '
-
- data = 'YUV4MPEG2 {y4mformat}W{width} H{height} F{fps_num}:{fps_den} Ip A0:0 XLENGTH={length}\n'.format(
+ 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 Y4M-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( # noqa: UP032
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]:
- buf = []
+
+ def render_single_frame(frame: vs.VideoFrame) -> tuple[int, bytes]:
+ buf = list[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)
@@ -136,4 +178,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..2efb17f 100644
--- a/vsengine/vpy.py
+++ b/vsengine/vpy.py
@@ -1,17 +1,18 @@
# 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
"""
vsengine.vpy runs vpy-scripts for you.
- >>> script("/path/to/my/script").result()
- >>> code("print('Hello, World!')").result()
+ >>> load_script("/path/to/my/script").result()
+ >>> load_code("print('Hello, World!')").result()
-script() and 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.
-script() takes a path as the first argument while 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.
@@ -27,68 +28,87 @@
execution.
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.
+reject with ExecutionError or with resolve with None.
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
+
+from __future__ import annotations
+
import ast
import os
+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, TracebackType
+from typing import Any, Concatenate, Self, overload
-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
-
+import vapoursynth as vs
-T = t.TypeVar("T")
-Runner = t.Callable[[t.Callable[[], T]], Future[T]]
-Executor = t.Callable[[t.ContextManager[None], types.ModuleType], None]
+from ._futures import UnifiedFuture, unified
+from .loops import make_awaitable, to_thread
+from .policy import ManagedEnvironment, Policy
+__all__ = ["ExecutionError", "Script", "load_code", "load_script"]
-__all__ = [
- "ExecutionFailed", "script", "code", "variables"
-]
+type Runner[R] = Callable[[Callable[[], R]], Future[R]]
+type Executor[T] = Callable[[WrapAllErrors, ModuleType], T]
-class ExecutionFailed(Exception):
+class ExecutionError(Exception):
+ """
+ Exception raised when script execution fails.
+ """
#: 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:
+ """
+ Initialize the ExecutionError 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:
- def __enter__(self):
- pass
+class WrapAllErrors(AbstractContextManager[None]):
+ """
+ Context manager that wraps exceptions in ExecutionError.
+ """
+
+ 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
+ raise ExecutionError(val) from None
-def inline_runner(func: t.Callable[[], T]) -> Future[T]:
- fut = Future()
+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()
except BaseException as e:
@@ -98,230 +118,312 @@ 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[**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()
os.chdir(dir)
+
try:
f = func(*args, **kwargs)
return f
- except Exception as e:
- print(e)
+ except Exception:
raise
finally:
os.chdir(current)
+
return parent(_wrapped)
+
return runner
-class Script:
- environment: t.Union[Environment, ManagedEnvironment]
+_missing = object()
+
- def __init__(self,
- what: Executor,
- module: types.ModuleType,
- environment: t.Union[Environment, ManagedEnvironment],
- runner: Runner[T]
- ) -> None:
- self.what = what
- self.environment = environment
+class Script[EnvT: (vs.Environment, ManagedEnvironment)](AbstractContextManager["Script[EnvT]"], Awaitable[None]):
+ """VapourSynth script wrapper."""
+
+ def __init__(self, executor: Executor[None], module: ModuleType, environment: EnvT, runner: Runner[None]) -> None:
+ self.executor = executor
+ self.environment: EnvT = environment
self.runner = runner
self.module = module
- self._future = None
- def _run_inline(self) -> 'Script':
- with self.environment.use():
- self.what(WrapAllErrors(), self.module)
+ def __enter__(self) -> Self:
+ self.result()
return self
- ###
- # Public API
+ def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
+ self.dispose()
- @unified()
- def get_variable(self, name: str, default: t.Optional[str]=None) -> Future[t.Optional[str]]:
- return UnifiedFuture.resolve(getattr(self.module, name, default))
+ def __await__(self) -> Generator[Any, None, None]:
+ """
+ Runs the script and waits until the script has completed.
+ """
+ return self.run_async().__await__()
- def run(self) -> Future['Script']:
+ 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.
"""
- if self._future is None:
- self._future = self.runner(self._run_inline)
- return self._future
+ self._future: Future[None]
- def result(self) -> 'Script':
- """
- Runs the script and blocks until the script has finished running.
- """
- return self.run().result()
+ if hasattr(self, "_future"):
+ return self._future
- def dispose(self):
- """
- 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):
- return self
+ self._future = self.runner(self._run_inline)
- def __exit__(self, _, __, ___):
- if isinstance(self.environment, ManagedEnvironment):
- self.dispose()
+ return self._future
- async def run_async(self):
+ async def run_async(self) -> None:
"""
Runs the script asynchronously, but it returns a coroutine.
"""
return await make_awaitable(self.run())
- def __await__(self):
+ def result(self) -> None:
"""
- Runs the script and waits until the script has completed.
+ Runs the script and blocks until the script has finished running.
"""
- return self.run_async().__await__()
-
+ return self.run().result()
+ def dispose(self) -> None:
+ """Disposes the managed environment."""
+ self.module.__dict__.clear()
-EnvironmentType = t.Union[Environment, ManagedEnvironment, Policy, Script]
+ if isinstance(self.environment, ManagedEnvironment):
+ self.environment.dispose()
+
+ @overload
+ @unified(kind="future")
+ 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 = _missing) -> 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) if default is _missing else getattr(self.module, name, default)
+ )
-def script(
- script: os.PathLike,
- environment: t.Optional[EnvironmentType]=None,
- *,
- module_name: str = "__vapoursynth__",
- inline: bool=True,
- chdir: t.Optional[os.PathLike] = None
-) -> Script:
+ def _run_inline(self) -> None:
+ with self.environment.use():
+ self.executor(WrapAllErrors(), self.module)
+
+
+@overload
+def load_script(
+ script: str | os.PathLike[str],
+ environment: vs.Environment | None = None,
+ *,
+ module: str | ModuleType = "__vapoursynth__",
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+) -> Script[vs.Environment]: ...
+
+
+@overload
+def load_script(
+ script: str | os.PathLike[str],
+ environment: Script[vs.Environment],
+ *,
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+) -> Script[vs.Environment]: ...
+
+
+@overload
+def load_script(
+ script: str | os.PathLike[str],
+ environment: Policy | ManagedEnvironment,
+ *,
+ module: str | ModuleType = "__vapoursynth__",
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+) -> Script[ManagedEnvironment]: ...
+
+
+@overload
+def load_script(
+ script: str | os.PathLike[str],
+ environment: Script[ManagedEnvironment],
+ *,
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+) -> Script[ManagedEnvironment]: ...
+
+
+def load_script(
+ script: str | os.PathLike[str],
+ environment: Policy | vs.Environment | ManagedEnvironment | Script[Any] | None = None,
+ *,
+ module: str | ModuleType = "__vapoursynth__",
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+) -> Script[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 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__.
- :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, module):
- 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: t.Mapping[str, str],
- environment: t.Optional[EnvironmentType]=None,
- *,
- module_name: str = "__vapoursynth__",
- inline: bool=True,
- chdir: t.Optional[os.PathLike] = None
-) -> Script:
- """
- Sets variables to the module.
-
- :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 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 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,
- or await it.
+ :returns: A script object. The script starts running when you call run() on it, or await it.
"""
- def _execute(ctx, module):
- with ctx:
- for k, v in variables.items():
- setattr(module, k, v)
- return _load(_execute, environment, module_name=module_name, inline=inline, chdir=chdir)
-
-
-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:
+ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None:
+ 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 | CodeType,
+ environment: vs.Environment | None = None,
+ *,
+ module: str | ModuleType = "__vapoursynth__",
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+ **kwargs: Any,
+) -> Script[vs.Environment]: ...
+
+
+@overload
+def load_code(
+ script: str | Buffer | ast.Module | CodeType,
+ environment: Script[vs.Environment],
+ *,
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+ **kwargs: Any,
+) -> Script[vs.Environment]: ...
+
+
+@overload
+def load_code(
+ script: str | Buffer | ast.Module | CodeType,
+ environment: Policy | ManagedEnvironment,
+ *,
+ module: str | ModuleType = "__vapoursynth__",
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+ **kwargs: Any,
+) -> Script[ManagedEnvironment]: ...
+
+
+@overload
+def load_code(
+ script: str | Buffer | ast.Module | CodeType,
+ environment: Script[ManagedEnvironment],
+ *,
+ inline: bool = True,
+ chdir: str | os.PathLike[str] | None = None,
+ **kwargs: Any,
+) -> Script[ManagedEnvironment]: ...
+
+
+def load_code(
+ 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,
+ **kwargs: Any,
+) -> Script[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 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 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 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,
- or await it.
+ :returns: A script object. The script starts running when you call run() on it, or await it.
"""
- def _execute(ctx, module):
+
+ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None:
+ nonlocal script, kwargs
+
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"
- )
+ 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__)
- return _load(_execute, environment, module_name=module_name, inline=inline, chdir=chdir)
+
+ return _load(_execute, environment, module, inline, chdir)
def _load(
- script: Executor,
- environment: t.Optional[EnvironmentType]=None,
- *,
- module_name: str = "__vapoursynth__",
- inline: bool=True,
- chdir: t.Optional[os.PathLike] = None
-) -> Script:
- if inline:
- runner = inline_runner
- else:
- runner = to_thread
+ executor: Executor[None],
+ 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 isinstance(environment, Script):
- module = environment.module
- else:
- module = types.ModuleType(module_name)
+ if chdir is not None:
+ runner = chdir_runner(chdir, runner)
if isinstance(environment, Script):
+ module = environment.module
environment = environment.environment
+ 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)
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)
+ return Script[Any](executor, module, environment, runner)