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)