From 0449d03f57f76e32baf39096e36fa7a2ee853659 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 12:17:04 +0100 Subject: [PATCH 01/28] use PyTVPaint instead --- README.md | 4 ++-- docs/contributing/how_it_works.md | 10 +++++----- docs/contributing/modify_objects.md | 2 +- docs/contributing/wrap_george.md | 2 +- docs/cpp/index.md | 2 +- docs/credits.md | 12 ++++++------ docs/index.md | 18 +++++++++--------- docs/installation.md | 4 ++-- docs/limitations.md | 4 ++-- docs/usage.md | 4 ++-- mkdocs.yml | 4 ++-- pytvpaint/__init__.py | 2 +- 12 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index de930f2..20776b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Pytvpaint 🐍 → 🦋 +# PyTVPaint 🐍 → 🦋 [![](https://img.shields.io/github/actions/workflow/status/brunchstudio/pytvpaint/docs-deploy.yml?label=docs)](https://brunchstudio.github.io/pytvpaint/) [![](https://img.shields.io/github/license/brunchstudio/pytvpaint)](https://github.com/brunchstudio/pytvpaint/blob/main/LICENSE.md) @@ -10,7 +10,7 @@

-**Pytvpaint** is a type-safe Python library that wraps the George programming language commands in order to interact with the 2D animation software TVPaint. +**PyTVPaint** is a type-safe Python library that wraps the George programming language commands in order to interact with the 2D animation software TVPaint. It communicates through WebSocket to a [custom C++ plugin](https://github.com/brunchstudio/tvpaint-rpc) running in an opened TVPaint instance. diff --git a/docs/contributing/how_it_works.md b/docs/contributing/how_it_works.md index 5b8a4d1..0f18a89 100644 --- a/docs/contributing/how_it_works.md +++ b/docs/contributing/how_it_works.md @@ -1,17 +1,17 @@ # How it works -The following diagram shows how each processes interact to make Pytvpaint work: +The following diagram shows how each processes interact to make PyTVPaint work: ```mermaid sequenceDiagram - participant Pytvpaint + participant PyTVPaint participant TVPaint RPC server participant TVPaint - Pytvpaint->>TVPaint RPC server: {"jsonrpc": "2.0", "method": "execute_george", "params": ["tv_version"], "id": 45} + PyTVPaint->>TVPaint RPC server: {"jsonrpc": "2.0", "method": "execute_george", "params": ["tv_version"], "id": 45} TVPaint RPC server->>TVPaint: TVSendCmd(_, "tv_Version", buf) TVPaint->>TVPaint RPC server: '"TVP Animation 11 Pro" 11.7.1 en' - TVPaint RPC server->>Pytvpaint: {'id': 45, 'jsonrpc': '2.0', 'result': '"TVP Animation 11 Pro" 11.7.1 en'} + TVPaint RPC server->>PyTVPaint: {'id': 45, 'jsonrpc': '2.0', 'result': '"TVP Animation 11 Pro" 11.7.1 en'} ``` 1. The end user calls [`tv_version()`](../api/george/misc.md#pytvpaint.george.grg_base.tv_version) from `pytvpaint.george` in Python. @@ -20,4 +20,4 @@ sequenceDiagram 4. The JSON-RPC client sends the serialized JSON payload to the server `self.ws_handle.send(json.dumps(payload))` 5. The C++ plugin receives the message and [store it in the George commands queue](https://github.com/brunchstudio/tvpaint-rpc/blob/main/src/server.cpp#L59). 6. George commands are executed in the main thread, so at each plugin tick we check if we have commands to execute, execute them with the C++ SDK function `TVSendCmd` and [send back the result](https://github.com/brunchstudio/tvpaint-rpc/blob/main/src/main.cpp#L110). -7. We get back the result in Pytvpaint and use the `tv_parse_list` function to parse the resulting string from George and return a tuple from `tv_version`. +7. We get back the result in PyTVPaint and use the `tv_parse_list` function to parse the resulting string from George and return a tuple from `tv_version`. diff --git a/docs/contributing/modify_objects.md b/docs/contributing/modify_objects.md index 1e31edc..170beaa 100644 --- a/docs/contributing/modify_objects.md +++ b/docs/contributing/modify_objects.md @@ -88,7 +88,7 @@ def scenes(self) -> Iterator[Scene]: !!! Tip - Most of the `Iterator` in Pytvpaint are generators which mean it will only get the data (and send requests to TVPaint) if you want the next element. + Most of the `Iterator` in PyTVPaint are generators which mean it will only get the data (and send requests to TVPaint) if you want the next element. So you can stop whenever you want: diff --git a/docs/contributing/wrap_george.md b/docs/contributing/wrap_george.md index b4e345f..aba163b 100644 --- a/docs/contributing/wrap_george.md +++ b/docs/contributing/wrap_george.md @@ -173,7 +173,7 @@ Notes: ## Command that get and set a value -Some George commands are a getter and a setter at the same time. In Pytvpaint, we split the command in two separate functions with `_get` and `_set` suffix. +Some George commands are a getter and a setter at the same time. In PyTVPaint, we split the command in two separate functions with `_get` and `_set` suffix. !!! Example diff --git a/docs/cpp/index.md b/docs/cpp/index.md index cdd60f2..11cb03d 100644 --- a/docs/cpp/index.md +++ b/docs/cpp/index.md @@ -1,4 +1,4 @@ -# Pytvpaint's C++ plugin +# PyTVPaint's C++ plugin To communicate with TVPaint, we developed a TVPaint plugin using C++ and their SDK. diff --git a/docs/credits.md b/docs/credits.md index e126fee..f1b60cf 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -1,17 +1,17 @@ # Credits -## 💻 Brunch Dev Team +## :computer: Brunch Dev Team - [:simple-github:](https://github.com/rlahmidi) Radouane Lahmidi - [:simple-github:](https://github.com/jhenrybrunch) Joseph Henry - [:simple-firefoxbrowser:](https://www.chloeoternaud.com/about) Chloe Oternaud - [:simple-github:](https://github.com/aprayez) Alexis Prayez -## 🙏 Special Thanks +## :pray: Special Thanks ### Brunch Studio for supporting the project -- Fabin Cellier : Head of Studio [:simple-linkedin:](https://www.linkedin.com/in/fabien-cellier-03545826/) +- Fabien Cellier : Head of Studio [:simple-linkedin:](https://www.linkedin.com/in/fabien-cellier-03545826/) - Jean-Charles Kerninon : Head of CG [:simple-linkedin:](https://www.linkedin.com/in/jean-charles-kerninon-14309b3/) - Emilie Revert : Head of 2D Productions [:simple-linkedin:](https://www.linkedin.com/in/emilie-revert-236279109/) @@ -21,13 +21,13 @@ - Also thanks to [Jakub Trllo](https://www.linkedin.com/in/jakub-trllo-751387a6/) from Ynput who helped with the C++ implementation on their Discord server. - The TVPaint dev team for their patience and help with our questions and the [George commands documentation](https://www.tvpaint.com/doc/tvpaint-animation-11/george-commands) from TVPaint. -### Logo +## :snake: Logo ![](./assets/pytvpaint_logo.png){ width="300" } -The official Pytvpaint logo was made by Juliette Danesi ( [:simple-linkedin:](https://www.linkedin.com/in/juliette-danesi-1427561b6/) [:link:](https://juliettedanesi.wixsite.com/monsite/layout-posing) ) at BRUNCH Studio. :sparkles: +The official PyTVPaint logo was made by Juliette Danesi ( [:simple-linkedin:](https://www.linkedin.com/in/juliette-danesi-1427561b6/) [:link:](https://juliettedanesi.wixsite.com/monsite/layout-posing) ) at BRUNCH Studio. :sparkles: -## 💎 Brunch gems +## :gem: Brunch gems A special thanks also to our wonderful artists at the studio who also submitted some very interesting logos for the project. diff --git a/docs/index.md b/docs/index.md index ef8ab84..11e0aec 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# Pytvpaint 🐍 → 🦋 +# PyTVPaint 🐍 → 🦋 [![](https://img.shields.io/github/actions/workflow/status/brunchstudio/pytvpaint/docs-deploy.yml?label=docs)](https://brunchstudio.github.io/pytvpaint/) [![](https://img.shields.io/github/license/brunchstudio/pytvpaint)](https://github.com/brunchstudio/pytvpaint/blob/main/LICENSE.md) @@ -6,20 +6,20 @@ [![Downloads](https://static.pepy.tech/badge/pytvpaint/month)](https://pepy.tech/project/pytvpaint) [![](https://img.shields.io/pypi/pyversions/pytvpaint)](https://pypi.org/project/pytvpaint/) -Pytvpaint is a library that allows you to script for [TVPaint](https://www.tvpaint.com/) in Python instead of [George](https://www.tvpaint.com/doc/tvp11/index.php?id=lesson-advanced-functions-george-introduction). +PyTVPaint is a library that allows you to script for [TVPaint](https://www.tvpaint.com/) in Python instead of [George](https://www.tvpaint.com/doc/tvp11/index.php?id=lesson-advanced-functions-george-introduction). -Python is the go-to language when it comes to scripting, Pytvpaint offers a high level object-oriented API as well as low-level George commands in a fully type-hinted library. +Python is the go-to language when it comes to scripting, PyTVPaint offers a high level object-oriented API as well as low-level George commands in a fully type-hinted library. -Pytvpaint communicates through WebSocket to a [custom C++ plugin](https://github.com/brunchstudio/tvpaint-rpc) running in an opened TVPaint instance. +PyTVPaint communicates through WebSocket to a [custom C++ plugin](https://github.com/brunchstudio/tvpaint-rpc) running in an opened TVPaint instance. ![](./assets/pytvpaint_code_banner.png) !!! warning - Pytvpaint only works on Windows for now (because of the C++ plugin, the python code is agnostic to the platform but hasn't yet been tested on other OSes). + PyTVPaint only works on Windows for now (because of the C++ plugin, the python code is agnostic to the platform but hasn't yet been tested on other OSes). Support for Linux and MacOS can be added later. If you're interested, please [submit an issue](https://github.com/brunchstudio/tvpaint-rpc/issues/new) or a pull request ! -## Why use Pytvpaint? +## Why use PyTVPaint? - **Coding in George is not optimal** - it produces hard to maintain code, has bugs and poor support in IDEs (except syntax highlighting in IDEs, for example [VSCode](https://marketplace.visualstudio.com/items?itemName=johhnry.vscode-george)). @@ -27,13 +27,13 @@ Pytvpaint communicates through WebSocket to a [custom C++ plugin](https://github - **Fully type-hinted and tested API** - the library uses [MyPy](https://mypy.readthedocs.io) to strictly check the Python code, [Ruff](https://docs.astral.sh/ruff/) to lint and detect errors and [Pytest](https://docs.pytest.org) with ~2500 unit tests and has a test coverage of more than 90%. -- **Seamless coding experience** - no need to manually connect or disconnect to the WebSocket server, you can start coding directly and Pytvpaint will do everything for you! Just code in your favourite language (Python) and it will work! +- **Seamless coding experience** - no need to manually connect or disconnect to the WebSocket server, you can start coding directly and PyTVPaint will do everything for you! Just code in your favourite language (Python) and it will work! - **Fully extensible** - a George function wasn't implemented? You can either submit an issue on the repository or [code it yourself](./contributing/wrap_george.md)! We provide tools to directly speak in George with TVPaint and parse the resulting values. -- **Used in production** - Pytvpaint comes from a frustration of coding in the George programming language which made our codebase really hard to maintain here at [BRUNCH Studio](https://brunchstudio.tv/). It's now used in production to support our pipeline. +- **Used in production** - PyTVPaint comes from a frustration of coding in the George programming language which made our codebase really hard to maintain here at [BRUNCH Studio](https://brunchstudio.tv/). It's now used in production to support our pipeline. -## Pytvpaint examples +## PyTVPaint examples Get the name of all the layers in the current clip: diff --git a/docs/installation.md b/docs/installation.md index 55d937c..97ac55d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,7 +2,7 @@ ## TVPaint plugin installation -Pytvpaint works by sending George commands to a custom C++ plugin running a WebSocket server in TVPaint. +PyTVPaint works by sending George commands to a custom C++ plugin running a WebSocket server in TVPaint. You need to manually install it in your TVPaint installation folder. @@ -19,7 +19,7 @@ You need to manually install it in your TVPaint installation folder. You may need Administrator rights to copy the DLL file. -## Pytvpaint package +## PyTVPaint package Simply install it with Pip: diff --git a/docs/limitations.md b/docs/limitations.md index 1101f5d..d8c0bac 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -1,12 +1,12 @@ # Limitations -This page list all the current limitations of Pytvpaint in its current state. +This page list all the current limitations of PyTVPaint in its current state. ## Windows only As stated on the homepage, the [`tvpaint-rpc`](https://github.com/brunchstudio/tvpaint-rpc) C++ plugin is currently compiled for Windows only. -We are interested in making it available for Linux and MacOS, but being a Windows Studio we have not needed nor have we had time to do so yet. If you want to contribute on this, please open an issue or do a pull request on the [plugin repository](https://github.com/brunchstudio/tvpaint-rpc/issues). +We are interested in making it available for Linux and MacOS, but since we only have Windows workstations we didn't have time to do it yet. If you want to contribute on this, please open an issue or do a pull request on the [plugin repository](https://github.com/brunchstudio/tvpaint-rpc/issues). ## Control characters in George results diff --git a/docs/usage.md b/docs/usage.md index f41aa91..2c3e0dc 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,6 +1,6 @@ # Usage -Pytvpaint offers **two ways** to interact with TVPaint. +PyTVPaint offers **two ways** to interact with TVPaint. The recommended one is to use the [**object-oriented API**](#object-oriented-api) which handles all the nitty-gritty details of George and provide an extra layer of features. The classes can be imported from `pytvpaint.*` @@ -23,7 +23,7 @@ For example in an interactive Python shell: ## Object-oriented API -Pytvpaint provides a high level object-oriented API that handles the George calls behind the scene. Every element in TVPaint has its own object (`Project`, `Scene`, `Clip`, `Layer`...). +PyTVPaint provides a high level object-oriented API that handles the George calls behind the scene. Every element in TVPaint has its own object (`Project`, `Scene`, `Clip`, `Layer`...). ### Getting the current data in TVPaint diff --git a/mkdocs.yml b/mkdocs.yml index 208926e..391dce7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: Pytvpaint +site_name: PyTVPaint site_description: Python scripting for TVPaint strict: true site_url: https://brunchstudio.github.io/pytvpaint/ @@ -35,7 +35,7 @@ extra: nav: - Get Started: - - Welcome to Pytvpaint: index.md + - Welcome to PyTVPaint: index.md - Installation: installation.md - Usage: usage.md - Limitations: limitations.md diff --git a/pytvpaint/__init__.py b/pytvpaint/__init__.py index d7fc5c6..9f38f33 100644 --- a/pytvpaint/__init__.py +++ b/pytvpaint/__init__.py @@ -1,4 +1,4 @@ -"""Pytvpaint package logger.""" +"""PyTVPaint package logger.""" from __future__ import annotations From ab75312c0103b736f2299e75b46054ddc283097b Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 17:03:42 +0100 Subject: [PATCH 02/28] fix auto reconnect --- pytvpaint/george/client/__init__.py | 5 +++-- pytvpaint/george/client/rpc.py | 22 ++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pytvpaint/george/client/__init__.py b/pytvpaint/george/client/__init__.py index 44c6ff6..4feb512 100644 --- a/pytvpaint/george/client/__init__.py +++ b/pytvpaint/george/client/__init__.py @@ -26,6 +26,7 @@ def _connect_client( port = int(os.getenv("PYTVPAINT_WS_PORT", port)) startup_connect = bool(os.getenv("PYTVPAINT_WS_STARTUP_CONNECT", 1)) timeout = int(os.getenv("PYTVPAINT_WS_TIMEOUT", timeout)) + if timeout == 0: timeout = -1 @@ -35,7 +36,7 @@ def _connect_client( wait_duration = 5 connection_successful = False - while ((time() - start_time) < timeout) and startup_connect: + while startup_connect and ((time() - start_time) < timeout): with contextlib.suppress(ConnectionRefusedError): rpc_client.connect() connection_successful = True @@ -44,7 +45,7 @@ def _connect_client( log.warning(f"Connection refused, trying again in {wait_duration} seconds...") sleep(wait_duration) - if not connection_successful and startup_connect: + if startup_connect and not connection_successful: # Connection could not be established after timeout if rpc_client.is_connected: rpc_client.disconnect() diff --git a/pytvpaint/george/client/rpc.py b/pytvpaint/george/client/rpc.py index 299bd1f..85d356b 100644 --- a/pytvpaint/george/client/rpc.py +++ b/pytvpaint/george/client/rpc.py @@ -1,8 +1,8 @@ """JSON-RPC client and data models.""" from __future__ import annotations -import contextlib +import contextlib import json import sys import threading @@ -74,7 +74,6 @@ def __init__(self, url: str, timeout: int = 60, version: str = "2.0") -> None: version: The JSON-RPC version. Defaults to "2.0". """ self.ws_handle = WebSocket() - self.ws_handle.settimeout(5) self.url = url self.rpc_id = 0 self.timeout = timeout @@ -88,12 +87,9 @@ def __init__(self, url: str, timeout: int = 60, version: str = "2.0") -> None: def _auto_reconnect(self) -> None: """Automatic WebSocket reconnection in a thread by pinging the server.""" while self.run_forever and not self.stop_ping.wait(1): - if self.is_connected: - continue - try: self.ws_handle.ping() - except (WebSocketException, ConnectionError): + except (WebSocketException, ConnectionError): # noqa: PERF203 self.ws_handle.close() with contextlib.suppress(ConnectionRefusedError): @@ -101,11 +97,11 @@ def _auto_reconnect(self) -> None: log.info(f"Reconnected automatically to endpoint: {self.url}") continue - # There's a timeout after which we stop reconnecting - if self.timeout and time() - self._ping_start_time > self.timeout: - raise ConnectionRefusedError( - "Could not establish connection with a tvpaint instance before timeout !" - ) + # There's a timeout after which we stop reconnecting + if self.timeout and time() - self._ping_start_time > self.timeout: + raise ConnectionRefusedError( + "Could not establish connection with a tvpaint instance before timeout !" + ) def __del__(self) -> None: """Called when the client goes out of scope.""" @@ -159,7 +155,9 @@ def execute_remote( JSONRPCResponse: the JSON-RPC response payload """ if not self.is_connected: - raise ConnectionError("Can't send rpc message") + raise ConnectionError( + f"Can't send rpc message because the client is not connected to {self.url}" + ) payload: JSONRPCPayload = { "jsonrpc": self.jsonrpc_version, From 7e4f64217fac3a7317681c3787e11805b46618db Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 17:03:55 +0100 Subject: [PATCH 03/28] docs: add environment variables --- docs/usage.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index 2c3e0dc..6638185 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,6 +6,18 @@ The recommended one is to use the [**object-oriented API**](#object-oriented-api The other way is to use the [**wrapped George functions**](#george-functions) in Python which behaves almost extactly the same as real George commands. Those can be imported from `pytvpaint.george.*`. +## Environment variables + +Here are the environment variables that you can set for PyTVPaint: + +| Name | Default value | Description | +| :----------------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------- | +| `PYTVPAINT_LOG_LEVEL` | `INFO` | Changes the log level of PyTVPaint. Use the `DEBUG` value to see the RPC requests and responses for debugging George commands. | +| `PYTVPAINT_WS_HOST` | `ws://localhost` | The hostname of the RPC over WebSocket server ([tvpaint-rpc](https://github.com/brunchstudio/tvpaint-rpc) plugin). | +| `PYTVPAINT_WS_PORT` | `3000` | The port of the RPC over WebSocket server ([tvpaint-rpc](https://github.com/brunchstudio/tvpaint-rpc) plugin). | +| `PYTVPAINT_WS_STARTUP_CONNECT` | `1` / `True` | Wether or not PyTVPaint should automatically connect to the WebSocket server at startup (module import). | +| `PYTVPAINT_WS_TIMEOUT` | `60` seconds | The timeout after which we stop reconnecting at startup and if the connection was lost. | + ## Automatic client connection When you first import `pytvpaint`, a WebSocket client is automatically created and connects to the server runned by the [C++ plugin you installed](./installation.md). @@ -21,6 +33,10 @@ For example in an interactive Python shell: Set the `PYTVPAINT_LOG_LEVEL` environment variable to `INFO` to see the log above. +!!! tip + + You can disable this automatic behavior by setting the `PYTVPAINT_WS_STARTUP_CONNECT` variable to `0` if you want to control it yourself. + ## Object-oriented API PyTVPaint provides a high level object-oriented API that handles the George calls behind the scene. Every element in TVPaint has its own object (`Project`, `Scene`, `Clip`, `Layer`...). From 20d0d5dd2e335cc8a8f88d628f8df6966a9e9f26 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 18:24:08 +0100 Subject: [PATCH 04/28] improve actions and add pypi publish --- .github/workflows/check.yml | 8 ++++---- .github/workflows/docs-deploy.yml | 6 +++--- .github/workflows/pypi.yml | 24 ++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/pypi.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index dd99caf..e68a309 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,6 +1,6 @@ name: Format, types and lint check -on: [push] +on: [push, pull_request] jobs: check: @@ -23,8 +23,8 @@ jobs: - name: Formatting check run: poetry run black --check . - - name: Type checking - run: poetry run mypy . - - name: Linting run: poetry run ruff . + + - name: Type checking + run: poetry run mypy . diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 3d3c1b1..dfdcf3f 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -1,9 +1,9 @@ name: Publish documentation on: - push: - branches: - - main + create: + tags: + - "*" permissions: contents: write diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..0e3c517 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,24 @@ +name: Publish package to PyPi + +on: + create: + tags: + - "*" + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: 3.9 + + - name: Install Poetry Action + uses: snok/install-poetry@v1.3.4 + with: + version: 1.7.0 + + - name: Publish to PyPi + run: poetry publish --build --username __token__ --password ${{ secrets.PYPI_TOKEN }} diff --git a/pyproject.toml b/pyproject.toml index 2dfb595..e9c4296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,3 +96,4 @@ warn_return_any = true warn_unused_configs = true warn_unused_ignores = true extra_checks = true +untyped_calls_exclude = "fileseq,websocket" From 7d7d918294d4be2977271473913a82ae6e6e18b1 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 18:26:01 +0100 Subject: [PATCH 05/28] fix: tags push --- .github/workflows/docs-deploy.yml | 2 +- .github/workflows/pypi.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index dfdcf3f..15d53b6 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -1,7 +1,7 @@ name: Publish documentation on: - create: + push: tags: - "*" diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 0e3c517..4dbf3b3 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,7 +1,7 @@ name: Publish package to PyPi on: - create: + push: tags: - "*" From 8e7ad8212cf72b7b8a6cc29683b67f366cf08316 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 18:33:41 +0100 Subject: [PATCH 06/28] fix black format --- .github/workflows/check.yml | 2 +- .github/workflows/docs-deploy.yml | 2 +- pyproject.toml | 3 +++ pytvpaint/layer.py | 9 ++++++--- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e68a309..c33e0c5 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -18,7 +18,7 @@ jobs: version: 1.7.0 - name: Installing dependencies... - run: poetry install --with dev,test + run: poetry install --no-interaction --with dev,test - name: Formatting check run: poetry run black --check . diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 15d53b6..bf4355a 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -37,5 +37,5 @@ jobs: restore-keys: | mkdocs-material- - - run: poetry install --with docs + - run: poetry install --no-interaction --with docs - run: poetry run mkdocs gh-deploy --force diff --git a/pyproject.toml b/pyproject.toml index e9c4296..0a8a9cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,9 @@ mkdocstrings = { extras = ["python"], version = "^0.24.0" } requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" +[tool.black] +target-version = ["py39", "py310", "py311", "py312"] + [tool.ruff] target-version = "py39" diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index dabd9e9..21baa51 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -645,7 +645,7 @@ def new_background_layer( clip: Clip | None = None, color: LayerColor | None = None, image: Path | str | None = None, - stretch: bool = False + stretch: bool = False, ) -> Layer: """Create a new background layer with hold as pre- and post-behavior. @@ -664,7 +664,7 @@ def new_background_layer( layer.post_behavior = george.LayerBehavior.HOLD layer.thumbnails_visible = True - image = Path(image or '') + image = Path(image or "") if image.is_file(): layer.load_image(image, stretch=stretch) @@ -698,7 +698,10 @@ def remove(self) -> None: @set_as_current def load_image( - self, image_path: str | Path, frame: int | None = None, stretch: bool = False + self, + image_path: str | Path, + frame: int | None = None, + stretch: bool = False, ) -> None: """Load an image in the current layer at a given frame. From 52a7abeb7074ec0243d25d3167dd819e05616de5 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 19:10:13 +0100 Subject: [PATCH 07/28] release on pypi on github release --- .github/workflows/pypi.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 4dbf3b3..2e0ccc9 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,9 +1,8 @@ name: Publish package to PyPi on: - push: - tags: - - "*" + release: + types: [published] jobs: publish: From 1bf706f8cfb978e2fb7e40808ae42c7ea2b7b0f9 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 20:02:11 +0100 Subject: [PATCH 08/28] unit tests --- .github/workflows/tests.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f49b1a8 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +name: Unit tests + +on: [push] + +jobs: + check: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + # - uses: actions/setup-python@v5 + # with: + # python-version: 3.9 + + # - name: Install Poetry Action + # uses: snok/install-poetry@v1.3.4 + # with: + # version: 1.7.0 + + - name: Installing dependencies... + run: poetry install --no-interaction --with test + + - name: Pytest + run: poetry run pytest From 191eb984dfd79838677d36b3c33834212733e809 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 15 Mar 2024 20:08:28 +0100 Subject: [PATCH 09/28] fix job name --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f49b1a8..50e83ff 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: Unit tests on: [push] jobs: - check: + unit-tests: runs-on: self-hosted steps: - uses: actions/checkout@v4 From 76ec7baa23b8713937b47dfe536d95fdad0776ce Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Mon, 18 Mar 2024 10:46:57 +0100 Subject: [PATCH 10/28] update actions --- .github/workflows/check.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c33e0c5..ec5ea57 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,6 +1,6 @@ name: Format, types and lint check -on: [push, pull_request] +on: [push] jobs: check: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 50e83ff..b28f43d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,4 +21,4 @@ jobs: run: poetry install --no-interaction --with test - name: Pytest - run: poetry run pytest + run: poetry run pytest --maxfail 5 From a3bf6cc07f025579d595264931c3fb7dab60d8c0 Mon Sep 17 00:00:00 2001 From: Radouane Lahmidi Date: Mon, 18 Mar 2024 10:55:38 +0100 Subject: [PATCH 11/28] FIX misc issues and update tests and docs --- docs/limitations.md | 15 +-- pytvpaint/clip.py | 160 ++++++++++++++++++++++++++------ pytvpaint/george/client/rpc.py | 21 +++-- pytvpaint/george/grg_base.py | 28 ++++++ pytvpaint/george/grg_clip.py | 4 +- pytvpaint/george/grg_project.py | 2 +- pytvpaint/layer.py | 58 ++++++++++-- pytvpaint/project.py | 14 ++- pytvpaint/sound.py | 1 + tests/test_clip.py | 151 ++++++++++++++++++++++++++++-- 10 files changed, 383 insertions(+), 71 deletions(-) diff --git a/docs/limitations.md b/docs/limitations.md index d8c0bac..89ba395 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -6,7 +6,7 @@ This page list all the current limitations of PyTVPaint in its current state. As stated on the homepage, the [`tvpaint-rpc`](https://github.com/brunchstudio/tvpaint-rpc) C++ plugin is currently compiled for Windows only. -We are interested in making it available for Linux and MacOS, but since we only have Windows workstations we didn't have time to do it yet. If you want to contribute on this, please open an issue or do a pull request on the [plugin repository](https://github.com/brunchstudio/tvpaint-rpc/issues). +We are interested in making it available for Linux and MacOS, but being a Windows Studio we have not needed nor had time to do so yet. If you want to contribute on this, please open an issue or do a pull request on the [plugin repository](https://github.com/brunchstudio/tvpaint-rpc/issues). ## Control characters in George results @@ -56,14 +56,15 @@ Therefore, it is currently impossible to determine if it's actually an antislash !!! Info - We contacted the TVPaint technical support and they'll see what they can do to fix it in the future. + The TVPaint dev team have been made aware of the issue, and we are hopeful that it will be fixed in the future. ## Misbehaving George functions Here is a list of the bugs/inconsistencies in the George commands: -| Method | Description | -| :------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------- | -| [`tv_ratio`](api/george/project.md#pytvpaint.george.grg_project.tv_ratio) | Always return an empty string (`""`) | -| [`tv_instance_name`](api/george/layer.md#pytvpaint.george.grg_layer.tv_instance_name) | Crashes if we give a wrong `layer_id` | -| `tv_camera_path` | Confusing arguments and seemingly incorrect results (see [this](https://forum.tvpaint.com/viewtopic.php?t=15677)) | +| Method | Description | +|:-------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------| +| [`tv_Ratio`](api/george/project.md#pytvpaint.george.grg_project.tv_ratio) | Always return an empty string (`""`) | +| [`tv_InstanceName`](api/george/layer.md#pytvpaint.george.grg_layer.tv_instance_name) | Crashes if we give a wrong `layer_id` | +| `tv_CameraPath` | Confusing arguments and seemingly incorrect results (see [this](https://forum.tvpaint.com/viewtopic.php?t=15677)) | +| `tv_SoundClipReload` | Doesn't accept a proper clip id, only `0` seem to work for the current clip | diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index af79d08..0a6578b 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -9,6 +9,7 @@ from fileseq.filesequence import FileSequence from fileseq.frameset import FrameSet +from pytvpaint import log from pytvpaint import george from pytvpaint.camera import Camera from pytvpaint.layer import Layer, LayerColor @@ -410,6 +411,109 @@ def load_media( return new_layer + def _handle_output_range( + self, + output_path: Path | str | FileSequence, + start: int = None, + end: int = None, + use_camera: bool = False, + force_range: bool = False + ) -> tuple[FileSequence, int, int, bool, bool]: + """ + Handle the different options for output paths and range, whether the user provided a range (start-end) or a + filesequence with a range or not, this functions ensures we always end up with a valid range to render + + Args: + output_path: user provided output path + start: user provided start frame + end: user provided end frame + use_camera: use the camera for rendering, otherwise render the whole canvas. Defaults to False. + force_range: force the provided range even if it isn't incorrect or might output wrong frames + + Raises: + ValueError: if output range (start/end) are inferior to clip start frame + + Bug: + TVPaint will not render the requested/correct range in some cases. Having no control over this inconsistent + behaviour and to avoid issues we will raise a ValueError if an invalid range is detected. You can however + force an incorrect range using `force_range=True`, we will then log a warning when this happens (to help + with debugging) and the function will render with your range without checking the output at the end. + For more details on the different issues with frame ranges and the timeline in TVPaint, please check the + `Limitations` section of the documentation which explains this in more detail. + + Returns: + file_sequence: output path as a FileSequence object + start: computed start frame + end: computed end frame + is_sequence: whether the output is a sequence or not + is_image: whether the output is an image or not (a movie) + """ + # we handle all outputs as a FileSequence, makes it a bit easier to handle ranges and padding + if not isinstance(output_path, FileSequence): + file_sequence = FileSequence(Path(output_path).as_posix()) + else: + file_sequence = output_path + + frame_set = file_sequence.frameSet() + is_image = george.SaveFormat.is_image(file_sequence.extension()) + + # if the provided sequence has a range, and we don't, use the sequence range + if frame_set and len(frame_set) >= 1 and is_image: + start = start or file_sequence.start() + end = end or file_sequence.end() + + # check characteristics of output path + fseq_has_range = (frame_set and len(frame_set) > 1) + fseq_is_single_image = (frame_set and len(frame_set) == 1) + fseq_no_range_padding = (not frame_set and file_sequence.padding()) + range_is_seq = (start and end and start != end) + range_is_single_image = (start and end and start == end) + + is_single_image = bool(is_image and (fseq_is_single_image or not frame_set) and range_is_single_image) + is_sequence = bool(is_image and (fseq_has_range or fseq_no_range_padding or range_is_seq)) + + # if no range provided, use clip mark in/out, if none, use clip start/end + start = start or self.mark_in or self.start + if is_single_image and not end: + end = start + else: + end = end or self.mark_out or self.end + + frame_set = FrameSet(f"{start}-{end}") + + if not file_sequence.padding() and is_image and len(frame_set) > 1: + file_sequence.setPadding('#') + + # we should have a range by now, set it in the sequence + if (is_image and not is_single_image) or file_sequence.padding(): + file_sequence.setFrameSet(frame_set) + + err_msg = '' + clip_start = self.start + clip_end = self.end + if use_camera: + if start < clip_start or end > clip_end: + err_msg = f"TVPaint will not render the full range requested ({start}-{end})" + else: + if start < clip_start or end < clip_start: + err_msg = f"TVPaint will not render frames before clip start ({clip_start})" + elif start == clip_start and end > clip_end: + err_msg = f"TVPaint will not render the full range requested ({start}-{end})" + elif clip_start <= start < end and end > clip_end: + err_msg = f"TVPaint will render the full range requested ({start}-{end}) but frames will be empty" + + if not is_image and start == end: + err_msg = 'TVPaint will not render a movie that contains a single frame' + + if err_msg: + err_msg += ", check the documentation for more details !" + if force_range: + log.warning(err_msg) + else: + raise ValueError(err_msg) + + return file_sequence, start, end, is_sequence, is_image + @set_as_current def render( self, @@ -420,6 +524,7 @@ def render( layer_selection: list[Layer] | None = None, alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, format_opts: list[str] | None = None, + force_range: bool = False, ) -> None: """Render the clip to a single frame or frame sequence. @@ -431,39 +536,38 @@ def render( layer_selection: list of layers to render, if None render all of them. Defaults to None. alpha_mode: the alpha mode for rendering. Defaults to george.AlphaSaveMode.PREMULTIPLY. format_opts: custom format options. Defaults to None. + force_range: force the provided range even if it isn't incorrect or might output wrong frames Raises: + ValueError: if output range (start/end) are inferior to clip start frame FileNotFoundError: if the render failed and no files were found on disk - """ - start = start or self.mark_in or self.start - end = end or self.mark_out or self.end - - file_sequence = ( - output_path - if isinstance(output_path, FileSequence) - else FileSequence(Path(output_path).as_posix()) - ) - frame_set = file_sequence.frameSet() - is_sequence = (frame_set and len(frame_set) > 1) or ( - not frame_set and file_sequence.padding() - ) - - frame_set = FrameSet(f"{start}-{end}") - file_sequence.setFrameRange(frame_set) + Bug: + TVPaint will not render the requested/correct range in some cases. Having no control over this inconsistent + behaviour and to avoid issues we will raise a ValueError if an invalid range is detected. You can however + force an incorrect range using `force_range=True`, we will then log a warning when this happens (to help + with debugging) and the function will render with your range without checking the output at the end. + For more details on the different issues with frame ranges and the timeline in TVPaint, please check the + `Limitations` section of the documentation which explains this in more detail. + """ + file_sequence, start, end, is_sequence, is_image = self._handle_output_range(output_path, start, end, use_camera, force_range) - # get real frame numbers and clamp them with clip start frame - project_start_frame = self.project.start_frame - real_start = self.start - project_start_frame + # get project start to get real values, note that using the camera changes the way we handle ranges + project_start_frame = self.project.start_frame if not use_camera else 0 + # get clip real start to clamp start and end frames + clip_real_start = self.start - project_start_frame - start = max(real_start, (start - project_start_frame)) - end = max(real_start, (end - project_start_frame)) + start = max(clip_real_start, (start - project_start_frame)) + end = max(clip_real_start, (end - project_start_frame)) save_format = george.SaveFormat.from_extension( file_sequence.extension().lower() ) # render to output - first_frame = Path(file_sequence.frame(file_sequence.start())) + if is_image or file_sequence.padding(): + first_frame = Path(file_sequence.frame(file_sequence.start())) + else: + first_frame = Path(output_path) first_frame.parent.mkdir(exist_ok=True, parents=True) with render_context(alpha_mode, save_format, format_opts, layer_selection): @@ -479,18 +583,18 @@ def render( else: george.tv_save_sequence(first_frame, mark_in=start, mark_out=end) + if force_range: + return # user is forcing a possibly invalid range, let them handle output check + # make sure the output exists otherwise raise an error if is_sequence: + # raises error if sequence not found found_sequence = FileSequence.findSequenceOnDisk(str(file_sequence)) frame_set = found_sequence.frameSet() - if not frame_set: - raise ValueError() - - # raises error if sequence not found if not frame_set.issuperset(file_sequence.frameSet()): # not all frames found - missing_frames = frame_set.difference(found_sequence.frameSet()) + missing_frames = file_sequence.frameSet().difference(frame_set) raise FileNotFoundError( f"Not all frames found, missing frames ({missing_frames}) " f"in sequence : {output_path}" @@ -678,6 +782,8 @@ def export_sprites( with render_context(alpha_mode, save_format, format_opts, layer_selection): george.tv_clip_save_structure_sprite(export_path, layout, space) + # TODO check whether output was successful and files exist or not for this function and the others + @set_as_current def export_flix( self, diff --git a/pytvpaint/george/client/rpc.py b/pytvpaint/george/client/rpc.py index 85d356b..4c2b1dc 100644 --- a/pytvpaint/george/client/rpc.py +++ b/pytvpaint/george/client/rpc.py @@ -2,10 +2,10 @@ from __future__ import annotations -import contextlib import json import sys import threading +import contextlib from time import time from typing import Any, Union, cast @@ -89,19 +89,20 @@ def _auto_reconnect(self) -> None: while self.run_forever and not self.stop_ping.wait(1): try: self.ws_handle.ping() + continue except (WebSocketException, ConnectionError): # noqa: PERF203 self.ws_handle.close() - with contextlib.suppress(ConnectionRefusedError): - self.connect() - log.info(f"Reconnected automatically to endpoint: {self.url}") - continue + with contextlib.suppress(ConnectionRefusedError): + self.connect() + log.info(f"Reconnected automatically to endpoint: {self.url}") + continue - # There's a timeout after which we stop reconnecting - if self.timeout and time() - self._ping_start_time > self.timeout: - raise ConnectionRefusedError( - "Could not establish connection with a tvpaint instance before timeout !" - ) + # There's a timeout after which we stop reconnecting + if self.timeout and (time() - self._ping_start_time) > self.timeout: + raise ConnectionRefusedError( + "Could not establish connection with a tvpaint instance before timeout !" + ) def __del__(self) -> None: """Called when the client goes out of scope.""" diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index 6cf3f1f..8e55894 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -346,6 +346,29 @@ def from_extension(cls, extension: str) -> SaveFormat: ) return cast(SaveFormat, getattr(cls, extension.upper())) + @classmethod + def is_image(cls, extension: str) -> bool: + extension = extension.replace(".", "").lower() + image_formats = [ + 'bmp', + 'cin', + 'deep', + 'dpx', + 'ilbm', + 'jpg', + 'jpeg', + 'pcx', + 'png', + 'psd', + 'sgi', + 'pic', + 'ras', + 'sun', + 'tga', + 'tiff', + ] + return extension in image_formats + @dataclass(frozen=True) class RGBColor: @@ -609,6 +632,11 @@ def tv_version() -> tuple[str, str, str]: return software_name, version, language +def tv_quit() -> None: + """Closes the TVPaint instance.""" + send_cmd("tv_Quit") + + def tv_host2back() -> None: """Minimize the TVPaint window.""" send_cmd("tv_Host2Back") diff --git a/pytvpaint/george/grg_clip.py b/pytvpaint/george/grg_clip.py index c07afbe..17f6dae 100644 --- a/pytvpaint/george/grg_clip.py +++ b/pytvpaint/george/grg_clip.py @@ -625,14 +625,14 @@ def tv_sound_clip_remove(track_index: int) -> None: def tv_sound_clip_reload(clip_id: int, track_index: int) -> None: - """Reload a sound track from its file. + """Reload a soundtrack from its file. Args: clip_id: the clip id (only works with `0` being the current clip) track_index: the sound clip track index Warning: - It doesn't accept a proper clip id, only `0` seem to work for the current clip + this doesn't accept a proper clip id, only `0` seem to work for the current clip """ send_cmd("tv_SoundClipReload", clip_id, track_index, error_values=[-1, -2, -3]) diff --git a/pytvpaint/george/grg_project.py b/pytvpaint/george/grg_project.py index 3ddfaeb..244dc83 100644 --- a/pytvpaint/george/grg_project.py +++ b/pytvpaint/george/grg_project.py @@ -229,7 +229,7 @@ def tv_ratio() -> float: """Get the current project pixel aspect ratio. Bug: - Doesn't work and always return an empty string + Doesn't work and always returns an empty string """ return float(send_cmd("tv_GetRatio", error_values=[GrgErrorValue.EMPTY])) diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index 21baa51..ad727af 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -27,19 +27,22 @@ @contextlib.contextmanager def restore_current_frame( - clip: Clip, frame: int | None = None + clip: Clip, frame: int ) -> Generator[None, None, None]: - """Context that changes the current frame temporarily and restores the previous value. + """Context that temporarily changes the current frame to the one provided and restores it when done. Args: clip: clip to change frame: frame to set. Defaults to None. """ previous_frame = clip.current_frame - if frame: + if frame != previous_frame: clip.current_frame = frame + yield - clip.current_frame = previous_frame + + if clip.current_frame != previous_frame: + clip.current_frame = previous_frame @dataclass @@ -80,6 +83,37 @@ def duplicate(self) -> None: with restore_current_frame(self.layer.clip, self.start): george.tv_layer_insert_image(duplicate=True) + @classmethod + def new( + cls, + layer, + start: int | None, + nb_frames: int = 1, + direction: george.InsertDirection | None = None + ) -> LayerInstance: + """ + Crates a new instance + + Args: + layer: parent layer instance + start: start frame + nb_frames: number of frames in the new instance + direction: direction where new frames will be added/inserted + + Returns: + LayerInstance: new layer instance + """ + if not nb_frames: + raise ValueError("Instance number of frames must be at least 1") + start = start if start is not None else layer.clip.current_frame + + layer.make_current() + with restore_current_frame(layer.clip, start): + george.tv_layer_insert_image(count=nb_frames, direction=direction) + + return cls(layer, start) + + @property def next(self) -> LayerInstance | None: """Returns the next instance. @@ -87,6 +121,7 @@ def next(self) -> LayerInstance | None: Returns: the next instance or None if at the end of the layer """ + self.layer.make_current() with restore_current_frame(self.layer.clip, self.start): next_frame = george.tv_exposure_next() @@ -99,6 +134,7 @@ def previous(self) -> LayerInstance | None: Returns: the previous instance, None if there isn't """ + self.layer.make_current() with restore_current_frame(self.layer.clip, self.start): prev_frame = george.tv_exposure_prev() @@ -645,7 +681,7 @@ def new_background_layer( clip: Clip | None = None, color: LayerColor | None = None, image: Path | str | None = None, - stretch: bool = False, + stretch: bool = False ) -> Layer: """Create a new background layer with hold as pre- and post-behavior. @@ -701,7 +737,7 @@ def load_image( self, image_path: str | Path, frame: int | None = None, - stretch: bool = False, + stretch: bool = False ) -> None: """Load an image in the current layer at a given frame. @@ -712,16 +748,18 @@ def load_image( Raises: FileNotFoundError: if the file doesn't exist at provided path - """ image_path = Path(image_path) if not image_path.exists(): raise FileNotFoundError(f"Image not found at : {image_path}") - if frame is not None: - self.clip.current_frame = frame + frame = frame or self.clip.current_frame + with restore_current_frame(self.clip, frame): + # if no instance at the specified frame, then create a new one + if not self.get_instance(frame): + LayerInstance.new(self, frame) - george.tv_load_image(image_path.as_posix(), stretch) + george.tv_load_image(image_path.as_posix(), stretch) @set_as_current def render_frame( diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 9828bb1..10312ed 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -508,12 +508,18 @@ def close(self) -> None: george.tv_project_close(self._id) @classmethod - def close_all(cls) -> None: - """Closes all the projects.""" - projects = list(cls.open_projects()) - for project in projects: + def close_all(cls, close_tvp: bool = False) -> None: + """Closes all open projects. + + Args: + close_tvp: close the TVPaint instance as well + """ + for project in cls.open_projects(): project.close() + if close_tvp: + george.tv_quit() + @classmethod def load(cls, project_path: Path | str, silent: bool = True) -> Project: """Load an existing .tvpp/.tvp project or .tvpx file.""" diff --git a/pytvpaint/sound.py b/pytvpaint/sound.py index 1976f27..889555d 100644 --- a/pytvpaint/sound.py +++ b/pytvpaint/sound.py @@ -239,6 +239,7 @@ def remove(self) -> None: george.tv_sound_clip_remove(self.track_index) self.mark_removed() + @set_as_current def reload(self) -> None: """Reload the sound from file.""" george.tv_sound_clip_reload(self._parent.id, self.track_index) diff --git a/tests/test_clip.py b/tests/test_clip.py index 112a54c..a0a46f9 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest +from fileseq import FileSequence from pytvpaint import george from pytvpaint.clip import Clip @@ -11,6 +12,7 @@ from pytvpaint.layer import Layer from pytvpaint.project import Project from pytvpaint.scene import Scene + from tests.conftest import FixtureYield from tests.george.test_grg_clip import TEST_TEXTS @@ -247,38 +249,167 @@ def test_clip_load_media(test_clip_obj: Clip, ppm_sequence: list[Path]) -> None: assert layer.name == "images" -def test_clip_render( +@pytest.mark.parametrize( + "out, start, end, expected", + [ + ("render.png", None, None, 'render1-5#.png'), + ("render.png", 2, 2, 'render.png'), + ("render.#.png", 2, 2, 'render.0002.png'), + ("render.0010.png", None, None, 'render.0010.png'), + ], +) +def test_clip_render_single_img( test_clip_obj: Clip, with_loaded_sequence: Layer, tmp_path: Path, + out: str, + start: int | None, + end: int | None, + expected: str, ) -> None: - test_clip_obj.render(tmp_path / "render.png", start=1, end=1) + test_clip_obj.render(tmp_path / out, start, end) + + expected = tmp_path.joinpath(expected) + if '#' in expected.stem: + expected_seq = FileSequence(expected.as_posix()) + found_seq = FileSequence.findSequenceOnDisk(expected.as_posix(), strictPadding=True) + assert expected_seq.frameSet() == found_seq.frameSet() + else: + assert expected.exists() @pytest.mark.parametrize( - "args", + "out,start,end,force_range,expected,error", [ - ("render.#.png", None, None), - ("render.1-5#.png", None, None), - ("render.1-5#.png", 2, 7), + ("render.png", 2, 7, True, 'render2-7#.png', None), + ("render.png", 2, 7, False, '', ValueError), + ("render.0010.png", 2, 7, True, 'render.2-7#.png', None), + ("render2-7@.png", 2, 7, True, 'render2-7@.png', None), + ("render.2-7@.png", 2, 7, True, 'render.2-7@.png', None), + ("render.#.png", 1, 7, True, 'render.1-5#.png', None), + ("render.#.png", 1, 7, False, '', ValueError), + ("render.#.png", 2, 7, True, 'render.2-7#.png', None), + ("render.#.png", None, None, False, 'render.1-5#.png', None), + ("render.1-5#.png", None, None, False, 'render.1-5#.png', None), + ("render.2-4#.png", None, None, False, 'render.2-4#.png', None), + ("render.1-5#.png", 2, 7, True, 'render.2-7#.png', None), + ("render.1-5#.png", 2, None, False, 'render.2-5#.png', None), + ("render.1-5#.png", 2, 4, False, 'render.2-4#.png', None), + ("render.1-5#.png", 1, 7, False, '', ValueError), + ("render.1-5#.png", 1, 7, True, 'render.1-5#.png', None), + ("render.#.png", -6, 7, True, 'render.-10--6#.png', None), + ("render.#.png", -6, 7, False, '', ValueError), + ("render.1-5#.png", -6, 7, False, '', ValueError), ], ) def test_clip_render_sequence( test_clip_obj: Clip, with_loaded_sequence: Layer, tmp_path: Path, - args: tuple[str, int | None, int | None], + out: str, + start: int | None, + end: int | None, + force_range: bool, + expected: str, + error: Exception | None, ) -> None: - out, start, end = args - test_clip_obj.render(tmp_path / out, start, end) + if error: + with pytest.raises(error): + test_clip_obj.render(tmp_path / out, start, end, force_range=force_range) + else: + test_clip_obj.render(tmp_path / out, start, end, force_range=force_range) + + if expected: + expected = tmp_path.joinpath(expected) + expected_seq = FileSequence(expected.as_posix()) + found_seq = FileSequence.findSequenceOnDisk(expected.as_posix(), strictPadding=True) + assert expected_seq.frameSet() == found_seq.frameSet() + + +@pytest.mark.parametrize( + "out,start,end,force_range,expected,error", + [ + ("render.png", 2, 7, False, '', ValueError), + ("render.png", 2, 7, True, 'render2-5#.png', None), # will render range (2-5) incorrectly [x] + ("render.0010.png", 2, 7, True, 'render.2-5#.png', None), # will render range (2-5) incorrectly [x] + ("render2-7@.png", 2, 7, True, 'render2-5@.png', None), # will render range (2-5) incorrectly [x] + ("render.2-7@.png", 2, 7, True, 'render.2-5@.png', None), # will render range (2-5) incorrectly [x] + ("render.#.png", 1, 5, False, 'render.1-5#.png', None), # will render range (1-5) incorrectly [x] + ("render.#.png", 1, 7, True, 'render.1-5#.png', None), # will render range (1-5) incorrectly [x] + ("render.#.png", 2, 7, True, 'render.2-5#.png', None), # will render range (2-5) incorrectly [x] + ("render.#.png", None, None, False, 'render.1-5#.png', None), # will render range (1-5) correctly [v] + ("render.1-5#.png", None, None, False, 'render.1-5#.png', None), # will render range (1-5) correctly [v] + ("render.2-4#.png", None, None, False, 'render.2-4#.png', None), # will render range (2-4) correctly [v] + ("render.1-5#.png", 2, 7, False, '', ValueError), # will render range (2-5) incorrectly [x] + ("render.1-5#.png", 2, 7, True, 'render.2-5#.png', None), # will render range (2-5) incorrectly [x] + ("render.1-5#.png", 2, None, False, 'render.2-5#.png', None), # will render range (2-5) correctly [v] + ("render.1-5#.png", 2, 4, False, 'render.2-4#.png', None), # will render range (2-4) correctly [v] + ("render.1-5#.png", 1, 7, False, '', ValueError), # will render range (1-5) incorrectly [x] + ("render.1-5#.png", 1, 7, True, 'render.1-5#.png', None), # will render range (1-5) incorrectly [x] + ("render.#.png", -6, 7, True, 'render.-10--6#.png', None), # will render range (-6 to -10) incorrectly [x] + ("render.#.png", -6, 7, False, '', ValueError), + ("render.1-5#.png", -6, 7, False, '', ValueError), + ], +) +def test_clip_render_sequence_camera( + test_clip_obj: Clip, + with_loaded_sequence: Layer, + tmp_path: Path, + out: str, + start: int | None, + end: int | None, + force_range: bool, + expected: str, + error: Exception | None, +) -> None: + if error: + with pytest.raises(error): + test_clip_obj.render(tmp_path / out, start, end, use_camera=True, force_range=force_range) + else: + test_clip_obj.render(tmp_path / out, start, end, use_camera=True, force_range=force_range) + + if expected: + expected = tmp_path.joinpath(expected) + expected_seq = FileSequence(expected.as_posix()) + found_seq = FileSequence.findSequenceOnDisk(expected.as_posix(), strictPadding=True) + assert expected_seq.frameSet() == found_seq.frameSet() +@pytest.mark.parametrize("use_camera", [True, False]) +@pytest.mark.parametrize( + "out,start,end,expected,error", + [ + ("render.001.mp4", None, None, 'render.001.mp4', None), + ("render.001.mp4", 1, 5, 'render.001.mp4', None), + ("render.mp4", None, None, 'render.mp4', None), + ("render.1-5#.mp4", None, None, 'render.0001.mp4', None), + ("render.#.mp4", 1, 5, 'render.0001.mp4', None), + ("render.#.mp4", 2, 5, 'render.0002.mp4', None), + ("render.#.mp4", 2, 7, '', ValueError), + ("render.#.mp4", 1, 7, '', ValueError), + ("render.mp4", 1, 1, '', ValueError), + ], +) def test_clip_render_mp4( test_clip_obj: Clip, with_loaded_sequence: Layer, tmp_path: Path, + use_camera: bool, + out: str, + start: int | None, + end: int | None, + expected: str, + error: Exception | None, ) -> None: - test_clip_obj.render(tmp_path / "render.001.mp4") + + if error: + with pytest.raises(error): + test_clip_obj.render(tmp_path / out, start, end, use_camera=use_camera) + else: + test_clip_obj.render(tmp_path / out, start, end, use_camera=use_camera) + + if expected: + assert tmp_path.joinpath(expected).exists() def test_export_tvp( From 1dfe06e2684248084341fa37c529ba19d200d7cf Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Mon, 18 Mar 2024 12:49:49 +0100 Subject: [PATCH 12/28] fix tests on 11.5 --- docs/index.md | 2 +- pytvpaint/layer.py | 25 ++++++++++++++++--------- tests/george/test_grg_clip.py | 4 +--- tests/george/test_grg_layer.py | 6 +++--- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/index.md b/docs/index.md index 11e0aec..09207b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,7 +25,7 @@ PyTVPaint communicates through WebSocket to a [custom C++ plugin](https://github - **Fully documented** - all modules are fully documented and the george docstring is up-to-date, clearer and fixes some of the mistakes in TVPaints George documentation. -- **Fully type-hinted and tested API** - the library uses [MyPy](https://mypy.readthedocs.io) to strictly check the Python code, [Ruff](https://docs.astral.sh/ruff/) to lint and detect errors and [Pytest](https://docs.pytest.org) with ~2500 unit tests and has a test coverage of more than 90%. +- **Fully type-hinted and tested API** - the library uses [MyPy](https://mypy.readthedocs.io) to strictly check the Python code, [Ruff](https://docs.astral.sh/ruff/) to lint and detect errors and [Pytest](https://docs.pytest.org) with 2000+ unit tests and has a test coverage of more than 90%. - **Seamless coding experience** - no need to manually connect or disconnect to the WebSocket server, you can start coding directly and PyTVPaint will do everything for you! Just code in your favourite language (Python) and it will work! diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index ad727af..51b8213 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -910,15 +910,22 @@ def instances(self) -> Iterator[LayerInstance]: # Exposure frames starts at 0 instance_frame = self.start - self.clip.current_frame = self.start - - while True: - instance = self.get_instance(instance_frame) - if instance is None: - break - yield instance - with restore_current_frame(self.clip, instance_frame): - instance_frame = george.tv_exposure_next() + project_start_frame + + with restore_current_frame(self.clip, self.start): + while True: + instance = self.get_instance(instance_frame) + if instance is None: + break + yield instance + + self.clip.current_frame = instance_frame + new_instance_frame = george.tv_exposure_next() + project_start_frame + + # In TVPaint 11.5 and before, tv_exposure_next returns the same frame if it's the last one + if new_instance_frame == instance_frame: + break + + instance_frame = new_instance_frame def get_instance(self, frame: int) -> LayerInstance | None: """Get the instance at that frame. diff --git a/tests/george/test_grg_clip.py b/tests/george/test_grg_clip.py index 5b41154..58a6687 100644 --- a/tests/george/test_grg_clip.py +++ b/tests/george/test_grg_clip.py @@ -733,13 +733,11 @@ def test_tv_clip_save_structure_sprite( @pytest.mark.parametrize("mark_in_out", [None, (0, 0), (0, 5), (2, 5)]) -@pytest.mark.parametrize("send", [None, False, True]) def test_tv_clip_save_structure_flix( test_project: TVPProject, ppm_sequence: list[Path], tmp_path: Path, mark_in_out: tuple[int, int] | None, - send: bool | None, ) -> None: load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=3) @@ -757,7 +755,7 @@ def test_tv_clip_save_structure_flix( out_flix = tmp_path / "out.xml" - tv_clip_save_structure_flix(out_flix, mark_in, mark_out, send=send) + tv_clip_save_structure_flix(out_flix, mark_in, mark_out, send=False) assert out_flix.exists() diff --git a/tests/george/test_grg_layer.py b/tests/george/test_grg_layer.py index aa24b4c..e17617b 100644 --- a/tests/george/test_grg_layer.py +++ b/tests/george/test_grg_layer.py @@ -716,6 +716,9 @@ def can_be_parsed_as_int(value: str) -> bool: return True +@pytest.mark.skip( + "this test is overly complicated because I couldn't find a way to correctly grasp the logic" +) @pytest.mark.parametrize("mode", InstanceNamingMode) @pytest.mark.parametrize("prefix", [None, "pre_"]) @pytest.mark.parametrize("suffix", [None, "_suf"]) @@ -729,9 +732,6 @@ def test_tv_instance_name( process: InstanceNamingProcess | None, initial_name: str, ) -> None: - """ - TODO: this test is overly complicated because I couldn't find a way to correctly grasp the logic - """ instance = 0 # Assign an initial name to the instance From 136b42412d15172067abc2e8d33e1103294ca09d Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Mon, 18 Mar 2024 14:47:40 +0100 Subject: [PATCH 13/28] format, fix tests --- .github/workflows/check.yml | 3 + pytvpaint/clip.py | 71 +++++---- pytvpaint/george/client/rpc.py | 4 +- pytvpaint/george/grg_base.py | 33 ++-- pytvpaint/layer.py | 19 +-- pytvpaint/project.py | 2 +- tests/conftest.py | 3 +- tests/test_clip.py | 266 ++++++++++++++++++++++++--------- tests/test_layer.py | 8 +- 9 files changed, 278 insertions(+), 131 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ec5ea57..e4cf4df 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,10 +21,13 @@ jobs: run: poetry install --no-interaction --with dev,test - name: Formatting check + if: always() run: poetry run black --check . - name: Linting + if: always() run: poetry run ruff . - name: Type checking + if: always() run: poetry run mypy . diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 0a6578b..02efd85 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -9,8 +9,7 @@ from fileseq.filesequence import FileSequence from fileseq.frameset import FrameSet -from pytvpaint import log -from pytvpaint import george +from pytvpaint import george, log from pytvpaint.camera import Camera from pytvpaint.layer import Layer, LayerColor from pytvpaint.sound import ClipSound @@ -414,14 +413,12 @@ def load_media( def _handle_output_range( self, output_path: Path | str | FileSequence, - start: int = None, - end: int = None, + start: int | None = None, + end: int | None = None, use_camera: bool = False, - force_range: bool = False + force_range: bool = False, ) -> tuple[FileSequence, int, int, bool, bool]: - """ - Handle the different options for output paths and range, whether the user provided a range (start-end) or a - filesequence with a range or not, this functions ensures we always end up with a valid range to render + """Handle the different options for output paths and range, whether the user provided a range (start-end) or a filesequence with a range or not, this functions ensures we always end up with a valid range to render. Args: output_path: user provided output path @@ -463,47 +460,61 @@ def _handle_output_range( end = end or file_sequence.end() # check characteristics of output path - fseq_has_range = (frame_set and len(frame_set) > 1) - fseq_is_single_image = (frame_set and len(frame_set) == 1) - fseq_no_range_padding = (not frame_set and file_sequence.padding()) - range_is_seq = (start and end and start != end) - range_is_single_image = (start and end and start == end) - - is_single_image = bool(is_image and (fseq_is_single_image or not frame_set) and range_is_single_image) - is_sequence = bool(is_image and (fseq_has_range or fseq_no_range_padding or range_is_seq)) + fseq_has_range = frame_set and len(frame_set) > 1 + fseq_is_single_image = frame_set and len(frame_set) == 1 + fseq_no_range_padding = not frame_set and file_sequence.padding() + range_is_seq = start and end and start != end + range_is_single_image = start and end and start == end + + is_single_image = bool( + is_image + and (fseq_is_single_image or not frame_set) + and range_is_single_image + ) + is_sequence = bool( + is_image and (fseq_has_range or fseq_no_range_padding or range_is_seq) + ) # if no range provided, use clip mark in/out, if none, use clip start/end start = start or self.mark_in or self.start - if is_single_image and not end: - end = start - else: - end = end or self.mark_out or self.end + + end = ( + start + if (is_single_image and not end) + else (end or self.mark_out or self.end) + ) frame_set = FrameSet(f"{start}-{end}") if not file_sequence.padding() and is_image and len(frame_set) > 1: - file_sequence.setPadding('#') + file_sequence.setPadding("#") # we should have a range by now, set it in the sequence if (is_image and not is_single_image) or file_sequence.padding(): file_sequence.setFrameSet(frame_set) - err_msg = '' + err_msg = "" clip_start = self.start clip_end = self.end if use_camera: if start < clip_start or end > clip_end: - err_msg = f"TVPaint will not render the full range requested ({start}-{end})" + err_msg = ( + f"TVPaint will not render the full range requested ({start}-{end})" + ) else: if start < clip_start or end < clip_start: - err_msg = f"TVPaint will not render frames before clip start ({clip_start})" + err_msg = ( + f"TVPaint will not render frames before clip start ({clip_start})" + ) elif start == clip_start and end > clip_end: - err_msg = f"TVPaint will not render the full range requested ({start}-{end})" + err_msg = ( + f"TVPaint will not render the full range requested ({start}-{end})" + ) elif clip_start <= start < end and end > clip_end: err_msg = f"TVPaint will render the full range requested ({start}-{end}) but frames will be empty" if not is_image and start == end: - err_msg = 'TVPaint will not render a movie that contains a single frame' + err_msg = "TVPaint will not render a movie that contains a single frame" if err_msg: err_msg += ", check the documentation for more details !" @@ -550,7 +561,9 @@ def render( For more details on the different issues with frame ranges and the timeline in TVPaint, please check the `Limitations` section of the documentation which explains this in more detail. """ - file_sequence, start, end, is_sequence, is_image = self._handle_output_range(output_path, start, end, use_camera, force_range) + file_sequence, start, end, is_sequence, is_image = self._handle_output_range( + output_path, start, end, use_camera, force_range + ) # get project start to get real values, note that using the camera changes the way we handle ranges project_start_frame = self.project.start_frame if not use_camera else 0 @@ -563,11 +576,13 @@ def render( save_format = george.SaveFormat.from_extension( file_sequence.extension().lower() ) + # render to output if is_image or file_sequence.padding(): first_frame = Path(file_sequence.frame(file_sequence.start())) else: - first_frame = Path(output_path) + first_frame = Path(str(output_path)) + first_frame.parent.mkdir(exist_ok=True, parents=True) with render_context(alpha_mode, save_format, format_opts, layer_selection): diff --git a/pytvpaint/george/client/rpc.py b/pytvpaint/george/client/rpc.py index 4c2b1dc..6fefb44 100644 --- a/pytvpaint/george/client/rpc.py +++ b/pytvpaint/george/client/rpc.py @@ -2,10 +2,10 @@ from __future__ import annotations +import contextlib import json import sys import threading -import contextlib from time import time from typing import Any, Union, cast @@ -90,7 +90,7 @@ def _auto_reconnect(self) -> None: try: self.ws_handle.ping() continue - except (WebSocketException, ConnectionError): # noqa: PERF203 + except (WebSocketException, ConnectionError): self.ws_handle.close() with contextlib.suppress(ConnectionRefusedError): diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index 8e55894..f14eaa0 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -348,24 +348,25 @@ def from_extension(cls, extension: str) -> SaveFormat: @classmethod def is_image(cls, extension: str) -> bool: + """Returns True if the extension correspond to an image format.""" extension = extension.replace(".", "").lower() image_formats = [ - 'bmp', - 'cin', - 'deep', - 'dpx', - 'ilbm', - 'jpg', - 'jpeg', - 'pcx', - 'png', - 'psd', - 'sgi', - 'pic', - 'ras', - 'sun', - 'tga', - 'tiff', + "bmp", + "cin", + "deep", + "dpx", + "ilbm", + "jpg", + "jpeg", + "pcx", + "png", + "psd", + "sgi", + "pic", + "ras", + "sun", + "tga", + "tiff", ] return extension in image_formats diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index 51b8213..fad36cf 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -26,9 +26,7 @@ @contextlib.contextmanager -def restore_current_frame( - clip: Clip, frame: int -) -> Generator[None, None, None]: +def restore_current_frame(clip: Clip, frame: int) -> Generator[None, None, None]: """Context that temporarily changes the current frame to the one provided and restores it when done. Args: @@ -86,13 +84,12 @@ def duplicate(self) -> None: @classmethod def new( cls, - layer, + layer: Layer, start: int | None, nb_frames: int = 1, - direction: george.InsertDirection | None = None + direction: george.InsertDirection | None = None, ) -> LayerInstance: - """ - Crates a new instance + """Crates a new instance. Args: layer: parent layer instance @@ -113,7 +110,6 @@ def new( return cls(layer, start) - @property def next(self) -> LayerInstance | None: """Returns the next instance. @@ -681,7 +677,7 @@ def new_background_layer( clip: Clip | None = None, color: LayerColor | None = None, image: Path | str | None = None, - stretch: bool = False + stretch: bool = False, ) -> Layer: """Create a new background layer with hold as pre- and post-behavior. @@ -734,10 +730,7 @@ def remove(self) -> None: @set_as_current def load_image( - self, - image_path: str | Path, - frame: int | None = None, - stretch: bool = False + self, image_path: str | Path, frame: int | None = None, stretch: bool = False ) -> None: """Load an image in the current layer at a given frame. diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 10312ed..62ff4fd 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -514,7 +514,7 @@ def close_all(cls, close_tvp: bool = False) -> None: Args: close_tvp: close the TVPaint instance as well """ - for project in cls.open_projects(): + for project in list(cls.open_projects()): project.close() if close_tvp: diff --git a/tests/conftest.py b/tests/conftest.py index 59a9ec6..908033d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -213,7 +213,8 @@ def create_some_projects(tmp_path: Path) -> FixtureYield[list[Project]]: yield projects for project in projects: - tv_project_close(project.id) + if not project.is_closed: + tv_project_close(project.id) @pytest.fixture diff --git a/tests/test_clip.py b/tests/test_clip.py index a0a46f9..a024683 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -12,7 +12,6 @@ from pytvpaint.layer import Layer from pytvpaint.project import Project from pytvpaint.scene import Scene - from tests.conftest import FixtureYield from tests.george.test_grg_clip import TEST_TEXTS @@ -252,10 +251,10 @@ def test_clip_load_media(test_clip_obj: Clip, ppm_sequence: list[Path]) -> None: @pytest.mark.parametrize( "out, start, end, expected", [ - ("render.png", None, None, 'render1-5#.png'), - ("render.png", 2, 2, 'render.png'), - ("render.#.png", 2, 2, 'render.0002.png'), - ("render.0010.png", None, None, 'render.0010.png'), + ("render.png", None, None, "render1-5#.png"), + ("render.png", 2, 2, "render.png"), + ("render.#.png", 2, 2, "render.0002.png"), + ("render.0010.png", None, None, "render.0010.png"), ], ) def test_clip_render_single_img( @@ -269,37 +268,39 @@ def test_clip_render_single_img( ) -> None: test_clip_obj.render(tmp_path / out, start, end) - expected = tmp_path.joinpath(expected) - if '#' in expected.stem: - expected_seq = FileSequence(expected.as_posix()) - found_seq = FileSequence.findSequenceOnDisk(expected.as_posix(), strictPadding=True) + expected_path = tmp_path.joinpath(expected) + if "#" in expected_path.stem: + expected_seq = FileSequence(expected_path.as_posix()) + found_seq = FileSequence.findSequenceOnDisk( + expected_path.as_posix(), strictPadding=True + ) assert expected_seq.frameSet() == found_seq.frameSet() else: - assert expected.exists() + assert expected_path.exists() @pytest.mark.parametrize( "out,start,end,force_range,expected,error", [ - ("render.png", 2, 7, True, 'render2-7#.png', None), - ("render.png", 2, 7, False, '', ValueError), - ("render.0010.png", 2, 7, True, 'render.2-7#.png', None), - ("render2-7@.png", 2, 7, True, 'render2-7@.png', None), - ("render.2-7@.png", 2, 7, True, 'render.2-7@.png', None), - ("render.#.png", 1, 7, True, 'render.1-5#.png', None), - ("render.#.png", 1, 7, False, '', ValueError), - ("render.#.png", 2, 7, True, 'render.2-7#.png', None), - ("render.#.png", None, None, False, 'render.1-5#.png', None), - ("render.1-5#.png", None, None, False, 'render.1-5#.png', None), - ("render.2-4#.png", None, None, False, 'render.2-4#.png', None), - ("render.1-5#.png", 2, 7, True, 'render.2-7#.png', None), - ("render.1-5#.png", 2, None, False, 'render.2-5#.png', None), - ("render.1-5#.png", 2, 4, False, 'render.2-4#.png', None), - ("render.1-5#.png", 1, 7, False, '', ValueError), - ("render.1-5#.png", 1, 7, True, 'render.1-5#.png', None), - ("render.#.png", -6, 7, True, 'render.-10--6#.png', None), - ("render.#.png", -6, 7, False, '', ValueError), - ("render.1-5#.png", -6, 7, False, '', ValueError), + ("render.png", 2, 7, True, "render2-7#.png", None), + ("render.png", 2, 7, False, "", ValueError), + ("render.0010.png", 2, 7, True, "render.2-7#.png", None), + ("render2-7@.png", 2, 7, True, "render2-7@.png", None), + ("render.2-7@.png", 2, 7, True, "render.2-7@.png", None), + ("render.#.png", 1, 7, True, "render.1-5#.png", None), + ("render.#.png", 1, 7, False, "", ValueError), + ("render.#.png", 2, 7, True, "render.2-7#.png", None), + ("render.#.png", None, None, False, "render.1-5#.png", None), + ("render.1-5#.png", None, None, False, "render.1-5#.png", None), + ("render.2-4#.png", None, None, False, "render.2-4#.png", None), + ("render.1-5#.png", 2, 7, True, "render.2-7#.png", None), + ("render.1-5#.png", 2, None, False, "render.2-5#.png", None), + ("render.1-5#.png", 2, 4, False, "render.2-4#.png", None), + ("render.1-5#.png", 1, 7, False, "", ValueError), + ("render.1-5#.png", 1, 7, True, "render.1-5#.png", None), + ("render.#.png", -6, 7, True, "render.-10--6#.png", None), + ("render.#.png", -6, 7, False, "", ValueError), + ("render.1-5#.png", -6, 7, False, "", ValueError), ], ) def test_clip_render_sequence( @@ -311,7 +312,7 @@ def test_clip_render_sequence( end: int | None, force_range: bool, expected: str, - error: Exception | None, + error: type[Exception] | None, ) -> None: if error: with pytest.raises(error): @@ -320,35 +321,156 @@ def test_clip_render_sequence( test_clip_obj.render(tmp_path / out, start, end, force_range=force_range) if expected: - expected = tmp_path.joinpath(expected) - expected_seq = FileSequence(expected.as_posix()) - found_seq = FileSequence.findSequenceOnDisk(expected.as_posix(), strictPadding=True) + expected_path = tmp_path.joinpath(expected) + expected_seq = FileSequence(expected_path.as_posix()) + found_seq = FileSequence.findSequenceOnDisk( + expected_path.as_posix(), strictPadding=True + ) assert expected_seq.frameSet() == found_seq.frameSet() @pytest.mark.parametrize( "out,start,end,force_range,expected,error", [ - ("render.png", 2, 7, False, '', ValueError), - ("render.png", 2, 7, True, 'render2-5#.png', None), # will render range (2-5) incorrectly [x] - ("render.0010.png", 2, 7, True, 'render.2-5#.png', None), # will render range (2-5) incorrectly [x] - ("render2-7@.png", 2, 7, True, 'render2-5@.png', None), # will render range (2-5) incorrectly [x] - ("render.2-7@.png", 2, 7, True, 'render.2-5@.png', None), # will render range (2-5) incorrectly [x] - ("render.#.png", 1, 5, False, 'render.1-5#.png', None), # will render range (1-5) incorrectly [x] - ("render.#.png", 1, 7, True, 'render.1-5#.png', None), # will render range (1-5) incorrectly [x] - ("render.#.png", 2, 7, True, 'render.2-5#.png', None), # will render range (2-5) incorrectly [x] - ("render.#.png", None, None, False, 'render.1-5#.png', None), # will render range (1-5) correctly [v] - ("render.1-5#.png", None, None, False, 'render.1-5#.png', None), # will render range (1-5) correctly [v] - ("render.2-4#.png", None, None, False, 'render.2-4#.png', None), # will render range (2-4) correctly [v] - ("render.1-5#.png", 2, 7, False, '', ValueError), # will render range (2-5) incorrectly [x] - ("render.1-5#.png", 2, 7, True, 'render.2-5#.png', None), # will render range (2-5) incorrectly [x] - ("render.1-5#.png", 2, None, False, 'render.2-5#.png', None), # will render range (2-5) correctly [v] - ("render.1-5#.png", 2, 4, False, 'render.2-4#.png', None), # will render range (2-4) correctly [v] - ("render.1-5#.png", 1, 7, False, '', ValueError), # will render range (1-5) incorrectly [x] - ("render.1-5#.png", 1, 7, True, 'render.1-5#.png', None), # will render range (1-5) incorrectly [x] - ("render.#.png", -6, 7, True, 'render.-10--6#.png', None), # will render range (-6 to -10) incorrectly [x] - ("render.#.png", -6, 7, False, '', ValueError), - ("render.1-5#.png", -6, 7, False, '', ValueError), + ("render.png", 2, 7, False, "", ValueError), + ( + "render.png", + 2, + 7, + True, + "render2-5#.png", + None, + ), # will render range (2-5) incorrectly [x] + ( + "render.0010.png", + 2, + 7, + True, + "render.2-5#.png", + None, + ), # will render range (2-5) incorrectly [x] + ( + "render2-7@.png", + 2, + 7, + True, + "render2-5@.png", + None, + ), # will render range (2-5) incorrectly [x] + ( + "render.2-7@.png", + 2, + 7, + True, + "render.2-5@.png", + None, + ), # will render range (2-5) incorrectly [x] + ( + "render.#.png", + 1, + 5, + False, + "render.1-5#.png", + None, + ), # will render range (1-5) incorrectly [x] + ( + "render.#.png", + 1, + 7, + True, + "render.1-5#.png", + None, + ), # will render range (1-5) incorrectly [x] + ( + "render.#.png", + 2, + 7, + True, + "render.2-5#.png", + None, + ), # will render range (2-5) incorrectly [x] + ( + "render.#.png", + None, + None, + False, + "render.1-5#.png", + None, + ), # will render range (1-5) correctly [v] + ( + "render.1-5#.png", + None, + None, + False, + "render.1-5#.png", + None, + ), # will render range (1-5) correctly [v] + ( + "render.2-4#.png", + None, + None, + False, + "render.2-4#.png", + None, + ), # will render range (2-4) correctly [v] + ( + "render.1-5#.png", + 2, + 7, + False, + "", + ValueError, + ), # will render range (2-5) incorrectly [x] + ( + "render.1-5#.png", + 2, + 7, + True, + "render.2-5#.png", + None, + ), # will render range (2-5) incorrectly [x] + ( + "render.1-5#.png", + 2, + None, + False, + "render.2-5#.png", + None, + ), # will render range (2-5) correctly [v] + ( + "render.1-5#.png", + 2, + 4, + False, + "render.2-4#.png", + None, + ), # will render range (2-4) correctly [v] + ( + "render.1-5#.png", + 1, + 7, + False, + "", + ValueError, + ), # will render range (1-5) incorrectly [x] + ( + "render.1-5#.png", + 1, + 7, + True, + "render.1-5#.png", + None, + ), # will render range (1-5) incorrectly [x] + ( + "render.#.png", + -6, + 7, + True, + "render.-10--6#.png", + None, + ), # will render range (-6 to -10) incorrectly [x] + ("render.#.png", -6, 7, False, "", ValueError), + ("render.1-5#.png", -6, 7, False, "", ValueError), ], ) def test_clip_render_sequence_camera( @@ -360,18 +482,24 @@ def test_clip_render_sequence_camera( end: int | None, force_range: bool, expected: str, - error: Exception | None, + error: type[Exception] | None, ) -> None: if error: with pytest.raises(error): - test_clip_obj.render(tmp_path / out, start, end, use_camera=True, force_range=force_range) + test_clip_obj.render( + tmp_path / out, start, end, use_camera=True, force_range=force_range + ) else: - test_clip_obj.render(tmp_path / out, start, end, use_camera=True, force_range=force_range) + test_clip_obj.render( + tmp_path / out, start, end, use_camera=True, force_range=force_range + ) if expected: - expected = tmp_path.joinpath(expected) - expected_seq = FileSequence(expected.as_posix()) - found_seq = FileSequence.findSequenceOnDisk(expected.as_posix(), strictPadding=True) + expected_path = tmp_path.joinpath(expected) + expected_seq = FileSequence(expected_path.as_posix()) + found_seq = FileSequence.findSequenceOnDisk( + expected_path.as_posix(), strictPadding=True + ) assert expected_seq.frameSet() == found_seq.frameSet() @@ -379,15 +507,15 @@ def test_clip_render_sequence_camera( @pytest.mark.parametrize( "out,start,end,expected,error", [ - ("render.001.mp4", None, None, 'render.001.mp4', None), - ("render.001.mp4", 1, 5, 'render.001.mp4', None), - ("render.mp4", None, None, 'render.mp4', None), - ("render.1-5#.mp4", None, None, 'render.0001.mp4', None), - ("render.#.mp4", 1, 5, 'render.0001.mp4', None), - ("render.#.mp4", 2, 5, 'render.0002.mp4', None), - ("render.#.mp4", 2, 7, '', ValueError), - ("render.#.mp4", 1, 7, '', ValueError), - ("render.mp4", 1, 1, '', ValueError), + ("render.001.mp4", None, None, "render.001.mp4", None), + ("render.001.mp4", 1, 5, "render.001.mp4", None), + ("render.mp4", None, None, "render.mp4", None), + ("render.1-5#.mp4", None, None, "render.0001.mp4", None), + ("render.#.mp4", 1, 5, "render.0001.mp4", None), + ("render.#.mp4", 2, 5, "render.0002.mp4", None), + ("render.#.mp4", 2, 7, "", ValueError), + ("render.#.mp4", 1, 7, "", ValueError), + ("render.mp4", 1, 1, "", ValueError), ], ) def test_clip_render_mp4( @@ -399,7 +527,7 @@ def test_clip_render_mp4( start: int | None, end: int | None, expected: str, - error: Exception | None, + error: type[Exception] | None, ) -> None: if error: diff --git a/tests/test_layer.py b/tests/test_layer.py index 80f69a2..ae08907 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -273,6 +273,10 @@ def test_layer_remove(test_clip_obj: Clip) -> None: layer.name = "other" +def test_layer_load_image(test_layer_obj: Layer, ppm_sequence: list[Path]) -> None: + test_layer_obj.load_image(image_path=ppm_sequence[0], frame=5) + + def test_layer_render_frame(with_loaded_sequence: Layer, tmp_path: Path) -> None: with_loaded_sequence.render_frame(tmp_path / "out.jpg", frame=3) @@ -332,7 +336,9 @@ def test_layer_select_frames(test_layer_obj: Layer, with_images: int) -> None: def test_layer_instances( - test_project_obj: Project, test_anim_layer_obj: Layer, with_images: int + test_project_obj: Project, + test_anim_layer_obj: Layer, + with_images: int, ) -> None: start_frame = test_project_obj.start_frame end_frame = start_frame + with_images From 58eece56642cd068c0ccf8f337700a247145d1e0 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Mon, 18 Mar 2024 15:55:44 +0100 Subject: [PATCH 14/28] fix project duplicate with current --- pytvpaint/project.py | 10 +++------- tests/george/test_grg_project.py | 10 +++++----- tests/test_project.py | 5 +++-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 62ff4fd..1f4de26 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -494,13 +494,9 @@ def new_from_camera(self, export_path: Path | str | None = None) -> Project: def duplicate(self) -> Project: """Duplicate the project and return the new one.""" george.tv_project_duplicate() - - duplicated_name = f"{self.name}Copy" - for project in Project.open_projects(): - if project.name == duplicated_name: - return project - - raise Exception(f"Couldn't find project {duplicated_name}") + duplicated = Project.current_project() + self.make_current() + return duplicated def close(self) -> None: """Closes the project.""" diff --git a/tests/george/test_grg_project.py b/tests/george/test_grg_project.py index 3e0916e..514cb54 100644 --- a/tests/george/test_grg_project.py +++ b/tests/george/test_grg_project.py @@ -176,16 +176,16 @@ def projects_equal(p1: TVPProject, p2: TVPProject) -> bool: p1_dict = asdict(p1) p2_dict = asdict(p2) - del p1_dict["id"] - del p1_dict["path"] - del p2_dict["id"] - del p2_dict["path"] + for attr in ["id", "path"]: + del p1_dict[attr] + del p2_dict[attr] return p1_dict == p2_dict def test_tv_project_duplicate( - test_project: TVPProject, cleanup_current_project: None + test_project: TVPProject, + cleanup_current_project: None, ) -> None: tv_project_duplicate() dup_project = tv_project_info(tv_project_current_id()) diff --git a/tests/test_project.py b/tests/test_project.py index c48e81d..9d6d906 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -330,9 +330,10 @@ def test_project_new_from_camera(test_project_obj: Project) -> None: def test_project_duplicate( test_project_obj: Project, - cleanup_current_project: None, ) -> None: - assert test_project_obj.duplicate() != test_project_obj + dup = test_project_obj.duplicate() + assert dup != test_project_obj + dup.close() def test_project_close(test_project_obj: Project) -> None: From 38ebb550fd3acd215a75121b56d116c179946f83 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Mon, 18 Mar 2024 16:21:04 +0100 Subject: [PATCH 15/28] close all other projects --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 908033d..68e1eb8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -201,6 +201,8 @@ def wav_file(tmp_path_factory: pytest.TempPathFactory) -> Path: @pytest.fixture def create_some_projects(tmp_path: Path) -> FixtureYield[list[Project]]: """Create some projects in a test project and yields them""" + Project.close_all() + projects: list[Project] = [] for i in range(5): From c63c639cd95dcd608e865ec51f3b3c73b34cfd7b Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Mon, 18 Mar 2024 19:00:42 +0100 Subject: [PATCH 16/28] developer page, fix tests --- .github/workflows/tests.yml | 18 +- docs/contributing/developer_setup.md | 145 ++++++++++++++ .../{how_it_works.md => internals.md} | 2 +- mkdocs.yml | 3 +- poetry.lock | 177 +++++++++--------- pytvpaint/george/grg_base.py | 19 ++ tests/test_clip.py | 2 +- 7 files changed, 264 insertions(+), 102 deletions(-) create mode 100644 docs/contributing/developer_setup.md rename docs/contributing/{how_it_works.md => internals.md} (98%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b28f43d..99c1393 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,23 +2,21 @@ name: Unit tests on: [push] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: unit-tests: runs-on: self-hosted + concurrency: + group: uses-tvpaint + cancel-in-progress: true steps: - uses: actions/checkout@v4 - # - uses: actions/setup-python@v5 - # with: - # python-version: 3.9 - - # - name: Install Poetry Action - # uses: snok/install-poetry@v1.3.4 - # with: - # version: 1.7.0 - - name: Installing dependencies... run: poetry install --no-interaction --with test - name: Pytest - run: poetry run pytest --maxfail 5 + run: poetry run pytest --maxfail 5 --cov=pytvpaint diff --git a/docs/contributing/developer_setup.md b/docs/contributing/developer_setup.md new file mode 100644 index 0000000..9a04c83 --- /dev/null +++ b/docs/contributing/developer_setup.md @@ -0,0 +1,145 @@ +# Developer setup + +This guide will explain how to setup your environment in order to contribute to PyTVPaint. + +## Requirements + +- [Python](https://www.python.org/) 3.9 or greater is the supported version for PyTVPaint. + +- We use [Poetry](https://python-poetry.org/) which is the packaging and dependency management tool. It handles your dev virtualenv with your working dependencies. + +## PyTVPaint + +First clone the repository: + +```shell +❯ git clone https://github.com/brunchstudio/pytvpaint.git + +# or if you use SSH auth +❯ git clone git@github.com:brunchstudio/pytvpaint.git +``` + +Then install the dependencies in a virtualenv with Poetry: + +```shell +❯ poetry install +``` + +Note that this will only install the library dependency. To install optional [dependency groups](https://python-poetry.org/docs/managing-dependencies/#dependency-groups) (to build the documentation, run tests, etc...) you can use the `--with` parameter: + +```shell +❯ poetry install --with dev,docs,test +``` + +### Code formatting + +We use [Black](https://black.readthedocs.io/en/stable/) to ensure that the code format is always the same. Black has strong defaults and is easy to use: + +```shell +# Will format all the .py files in the current directory +(venv) ❯ black . + +# To only check if the format is correct +(venv) ❯ black --check . +``` + +!!! Tip + + Use `poetry shell` to enter a new shell in the virtualenv. In this page commands marked `(venv) ❯` can also be run with `poetry run ` + +### Linting + +We use [Ruff](https://docs.astral.sh/ruff/) which is a super powerful and fast Python linter. It combines a lot of rules from other projects like Flake8, pyupgrade, pydocstyle, isort, etc... + +```shell +(venv) ❯ ruff . + +# Will apply autofixes +(venv) ❯ ruff --fix . +``` + +### Type checking + +Mypy is the go-to static type checker for Python. It ensures that variables and functions are used correctly and can catch refactor errors when modify some code. + +```shell +(venv) ❯ mypy . +``` + +!!! info + + We currently exclude [Fileseq](https://github.com/justinfx/fileseq) and [websocket-client](https://github.com/websocket-client/websocket-client) untyped calls in [`pyproject.toml`](https://github.com/brunchstudio/pytvpaint/blob/main/pyproject.toml) with [`untyped_calls_exclude`](https://mypy.readthedocs.io/en/stable/config_file.html#untyped-definitions-and-calls) + +### Documentation + +The documentation is built using [MkDocs](https://www.mkdocs.org/) which is a static site generator that uses Markdown as the source format. + +On top of that we use [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) which provide the beautiful Material look and other nice features. + +You can either run the development server or build the entire documentation: + +```shell +# Will serve the doc on http://127.0.0.1:8000 with hot reload +(venv) ❯ mkdocs serve + +# Build the doc as static files +(venv) ❯ mkdocs build +``` + +The [Python API documentation](https://brunchstudio.github.io/pytvpaint/api/objects/project/) is auto-generated from the docstrings in the code by using [mkdocstrings](https://mkdocstrings.github.io/). We use the [Google style](https://mkdocstrings.github.io/griffe/docstrings/#google-style) for docstrings. + +For example: + +```python +def tv_request(msg: str, confirm_text: str = "Yes", cancel_text: str = "No") -> bool: + """Open a confirmation prompt with a message. + + Args: + msg: the message to display + confirm_text: the confirm button text. Defaults to "Yes". + cancel_text: the cancel button text. Defaults to "No". + + Returns: + bool: True if clicked on "Yes" + """ + return bool(int(send_cmd("tv_Request", msg, confirm_text, cancel_text))) +``` + +### Unit tests + +For the unit tests, we use [Pytest](https://docs.pytest.org/). Fixtures are located in the `conftest.py` file. + +To run the tests you'll need an opened TVPaint instance with the [tvpaint-rpc plugin](https://github.com/brunchstudio/tvpaint-rpc) installed. + +To run them, use the following commands: + +```shell +# Will run all the tests +(venv) ❯ pytest + +# Run with verbosity enabled (use PYTVPAINT_LOG_LEVEL to DEBUG) to see George commands +(venv) ❯ pytest -v -s + +# Only run specific tests with pattern matching +(venv) ❯ pytest -k test_tv_clip + +# See the coverage statistics with pytest-cov +(venv) ❯ pytest --cov=pytvpaint +``` + +### Publishing to PyPi + +There's two ways to publish the package to PyPi. + +The best way is to use the PyPi [API token](https://pypi.org/help/#apitoken): + +```shell +❯ poetry config pypi-token.pypi +❯ poetry publish --build +``` + +In CI, we use the token from secrets directly: + +```shell +❯ poetry publish --build --username __token__ --password ${{ secrets.PYPI_TOKEN }} +``` diff --git a/docs/contributing/how_it_works.md b/docs/contributing/internals.md similarity index 98% rename from docs/contributing/how_it_works.md rename to docs/contributing/internals.md index 0f18a89..e6ef877 100644 --- a/docs/contributing/how_it_works.md +++ b/docs/contributing/internals.md @@ -1,4 +1,4 @@ -# How it works +# How it works internally The following diagram shows how each processes interact to make PyTVPaint work: diff --git a/mkdocs.yml b/mkdocs.yml index 391dce7..9f0a3ca 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,7 +40,8 @@ nav: - Usage: usage.md - Limitations: limitations.md - Contributing: - - How it works: contributing/how_it_works.md + - Developer setup: contributing/developer_setup.md + - Internals: contributing/internals.md - Wrapping George commands: contributing/wrap_george.md - Modifying high-level classes: contributing/modify_objects.md - Credits: credits.md diff --git a/poetry.lock b/poetry.lock index 5327b52..d3e313d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,33 +16,33 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "black" -version = "24.2.0" +version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -197,63 +197,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.3" +version = "7.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, - {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, - {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, - {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, - {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, - {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, - {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, - {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, - {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, - {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [package.dependencies] @@ -381,13 +381,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markdown" -version = "3.5.2" +version = "3.6" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, - {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, ] [package.dependencies] @@ -526,13 +526,13 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-material" -version = "9.5.13" +version = "9.5.14" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.13-py3-none-any.whl", hash = "sha256:5cbe17fee4e3b4980c8420a04cc762d8dc052ef1e10532abd4fce88e5ea9ce6a"}, - {file = "mkdocs_material-9.5.13.tar.gz", hash = "sha256:d8e4caae576312a88fd2609b81cf43d233cdbe36860d67a68702b018b425bd87"}, + {file = "mkdocs_material-9.5.14-py3-none-any.whl", hash = "sha256:a45244ac221fda46ecf8337f00ec0e5cb5348ab9ffb203ca2a0c313b0d4dbc27"}, + {file = "mkdocs_material-9.5.14.tar.gz", hash = "sha256:2a1f8e67cda2587ab93ecea9ba42d0ca61d1d7b5fad8cf690eeaeb39dcd4b9af"}, ] [package.dependencies] @@ -595,18 +595,17 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.9.0" +version = "1.8.0" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings_python-1.9.0-py3-none-any.whl", hash = "sha256:fad27d7314b4ec9c0359a187b477fb94c65ef561fdae941dca1b717c59aae96f"}, - {file = "mkdocstrings_python-1.9.0.tar.gz", hash = "sha256:6e1a442367cf75d30cf69774cbb1ad02aebec58bfff26087439df4955efecfde"}, + {file = "mkdocstrings_python-1.8.0-py3-none-any.whl", hash = "sha256:4209970cc90bec194568682a535848a8d8489516c6ed4adbe58bbc67b699ca9d"}, + {file = "mkdocstrings_python-1.8.0.tar.gz", hash = "sha256:1488bddf50ee42c07d9a488dddc197f8e8999c2899687043ec5dd1643d057192"}, ] [package.dependencies] griffe = ">=0.37" -markdown = ">=3.3,<3.6" mkdocstrings = ">=0.20" [[package]] @@ -1164,13 +1163,13 @@ test = ["websockets"] [[package]] name = "zipp" -version = "3.18.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.18.0-py3-none-any.whl", hash = "sha256:c1bb803ed69d2cce2373152797064f7e79bc43f0a3748eb494096a867e0ebf79"}, - {file = "zipp-3.18.0.tar.gz", hash = "sha256:df8d042b02765029a09b157efd8e820451045890acc30f8e37dd2f94a060221f"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index f14eaa0..c845c4d 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -653,6 +653,11 @@ def tv_menu_hide() -> None: send_cmd("tv_MenuHide") +def add_some_magic() -> None: + """Makes your life sweeter (maybe).""" + send_cmd("tv_MagicNumber", 23) + + def tv_menu_show( menu_element: MenuElement | None = None, *menu_options: Any, current: bool = False ) -> None: @@ -1120,3 +1125,17 @@ def tv_rect_fill( if tool_mode: args.insert(0, "toolmode") send_cmd("tv_RectFill", *args) + + +def tv_fast_line( + x1: float, + y1: float, + x2: float, + y2: float, + r: int = 255, + b: int = 255, + g: int = 0, + a: int = 255, +) -> None: + """Draw a line (1 pixel size and not antialiased).""" + send_cmd("tv_fastline", x1, y1, x2, y2, r, g, b, a) diff --git a/tests/test_clip.py b/tests/test_clip.py index a024683..490c6de 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from fileseq import FileSequence +from fileseq.filesequence import FileSequence from pytvpaint import george from pytvpaint.clip import Clip From 78b4f323fe523765755caa58e8ae4daf275e66d0 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Tue, 19 Mar 2024 18:58:39 +0100 Subject: [PATCH 17/28] docs additions --- README.md | 20 ++++++++++++++++---- docs/assets/logo_brunch_black.svg | 1 + docs/assets/logo_brunch_white.svg | 1 + docs/index.md | 7 +++++++ docs/overrides/partials/copyright.html | 16 ++++++++++++++++ mkdocs.yml | 1 + 6 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 docs/assets/logo_brunch_black.svg create mode 100644 docs/assets/logo_brunch_white.svg create mode 100644 docs/overrides/partials/copyright.html diff --git a/README.md b/README.md index 20776b9..85b165c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![](https://img.shields.io/pypi/v/pytvpaint)](https://pypi.org/project/pytvpaint/) [![Downloads](https://static.pepy.tech/badge/pytvpaint/month)](https://pepy.tech/project/pytvpaint) [![](https://img.shields.io/pypi/pyversions/pytvpaint)](https://pypi.org/project/pytvpaint/) +[![](https://custom-icon-badges.demolab.com/badge/custom-11.5+-blue.svg?logo=butterfly_1f98b&label=TVPaint)](https://www.tvpaint.com/doc/tvp11/)

@@ -18,9 +19,14 @@ You can check the [documentation](https://brunchstudio.github.io/pytvpaint/) for ## Installation -First install the [TVPaint RPC plugin](https://brunchstudio.github.io/pytvpaint/installation/#tvpaint-plugin-installation). +### Requirements -Then install the package with Pip: +- Windows (for now, see [this](https://brunchstudio.github.io/pytvpaint/limitations/#windows-only)) +- Python v3.9+ +- TVPaint v11.5+ +- TVPaint RPC plugin (install instructions [here](https://brunchstudio.github.io/pytvpaint/installation/#tvpaint-plugin-installation)) + +Install the package with Pip: ```console ❯ pip install pytvpaint @@ -59,7 +65,11 @@ project.close() Pull requests are welcome. For major changes, please [open an issue](https://github.com/brunchstudio/pytvpaint/issues/new/choose) first to discuss what you would like to change. -Please make sure to update tests as appropriate. +Please make sure to [update tests](https://brunchstudio.github.io/pytvpaint/contributing/developer_setup/#unit-tests) as appropriate. + +## Disclaimer + +We are not affiliated with the TVPaint developers therefore we won't fix bugs in the software, PyTVPaint is a separate project created here at BRUNCH Studio to facilitate our developer experience. ## License @@ -67,4 +77,6 @@ Please make sure to update tests as appropriate.


-Made with ❤️ at [BRUNCH Studio](https://brunchstudio.tv/) 🥐🍳 +Made with ❤️ at + +[](https://brunchstudio.tv/) diff --git a/docs/assets/logo_brunch_black.svg b/docs/assets/logo_brunch_black.svg new file mode 100644 index 0000000..511f8ed --- /dev/null +++ b/docs/assets/logo_brunch_black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/logo_brunch_white.svg b/docs/assets/logo_brunch_white.svg new file mode 100644 index 0000000..6d15511 --- /dev/null +++ b/docs/assets/logo_brunch_white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 09207b1..5196caa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,6 +5,7 @@ [![](https://img.shields.io/pypi/v/pytvpaint)](https://pypi.org/project/pytvpaint/) [![Downloads](https://static.pepy.tech/badge/pytvpaint/month)](https://pepy.tech/project/pytvpaint) [![](https://img.shields.io/pypi/pyversions/pytvpaint)](https://pypi.org/project/pytvpaint/) +[![](https://custom-icon-badges.demolab.com/badge/custom-11.5+-blue.svg?logo=butterfly_1f98b&label=TVPaint)](https://www.tvpaint.com/doc/tvp11/) PyTVPaint is a library that allows you to script for [TVPaint](https://www.tvpaint.com/) in Python instead of [George](https://www.tvpaint.com/doc/tvp11/index.php?id=lesson-advanced-functions-george-introduction). @@ -84,3 +85,9 @@ from pytvpaint import Layer for instance in Layer.current_layer().instances: print(instance.start, instance.name) ``` + +## Disclaimer + +!!! note + + We are not affiliated with the TVPaint developers therefore we won't fix bugs in the software, PyTVPaint is a separate project created here at BRUNCH Studio to facilitate our developer experience. diff --git a/docs/overrides/partials/copyright.html b/docs/overrides/partials/copyright.html new file mode 100644 index 0000000..62c9632 --- /dev/null +++ b/docs/overrides/partials/copyright.html @@ -0,0 +1,16 @@ + diff --git a/mkdocs.yml b/mkdocs.yml index 9f0a3ca..aa957eb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,7 @@ copyright: Made with ❤️ by BRUNCH Studio developers theme: name: material + custom_dir: docs/overrides logo: assets/pytvpaint_logo.png favicon: assets/pytvpaint_logo.png icon: From 53010fc72a4ceadc33aa83b4c4f59bed74381ee7 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Tue, 19 Mar 2024 19:07:27 +0100 Subject: [PATCH 18/28] fix logo link --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85b165c..98bcf18 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,11 @@ Please make sure to [update tests](https://brunchstudio.github.io/pytvpaint/cont ## Disclaimer -We are not affiliated with the TVPaint developers therefore we won't fix bugs in the software, PyTVPaint is a separate project created here at BRUNCH Studio to facilitate our developer experience. +PyTVPaint is a project created at BRUNCH Studio to facilitate our developer experience with George. The API is targeted at experienced developers and is by no means a replacement for TVPaint or George but simply builds on it. + +We are not affiliated with the TVPaint development team and therefore can't fix any bugs in the software or the George API, we are very much subject to the same issues faced by other developers. Please direct your issues appropriately; any issues with PyTVPaint should be submitted as an issue in this repository or the C++ plugin's repository, any issues with TVPaint the software should be addressed to the tvp support team. + +For any questions on the limitations of our API, please head to [this page](https://brunchstudio.github.io/pytvpaint/limitations/) in the documentation. ## License @@ -79,4 +83,4 @@ We are not affiliated with the TVPaint developers therefore we won't fix bugs in Made with ❤️ at -[](https://brunchstudio.tv/) +[](https://brunchstudio.tv/) From e230d1bc85caad343ec0e15ae235a32978680495 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 10:39:48 +0100 Subject: [PATCH 19/28] disclaimer text --- README.md | 6 ++++-- docs/index.md | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 98bcf18..2d46075 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,11 @@ Please make sure to [update tests](https://brunchstudio.github.io/pytvpaint/cont PyTVPaint is a project created at BRUNCH Studio to facilitate our developer experience with George. The API is targeted at experienced developers and is by no means a replacement for TVPaint or George but simply builds on it. -We are not affiliated with the TVPaint development team and therefore can't fix any bugs in the software or the George API, we are very much subject to the same issues faced by other developers. Please direct your issues appropriately; any issues with PyTVPaint should be submitted as an issue in this repository or the C++ plugin's repository, any issues with TVPaint the software should be addressed to the tvp support team. +We are not affiliated with the TVPaint development team and therefore can't fix any bugs in the software or the George API. -For any questions on the limitations of our API, please head to [this page](https://brunchstudio.github.io/pytvpaint/limitations/) in the documentation. +Please direct your issues appropriately; any issues with PyTVPaint should be submitted as [an issue in this repository](https://github.com/brunchstudio/pytvpaint/issues) or the [C++ plugin's repository](https://github.com/brunchstudio/tvpaint-rpc), any issues with TVPaint the software should be addressed to the [tvp support team](https://tvpaint.odoo.com/en_US/contactus). + +For any questions on the limitations of our API, please head to [this page](https://brunchstudio.github.io/pytvpaint/limitations/). ## License diff --git a/docs/index.md b/docs/index.md index 5196caa..16392ad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -88,6 +88,10 @@ for instance in Layer.current_layer().instances: ## Disclaimer -!!! note +PyTVPaint is a project created at BRUNCH Studio to facilitate our developer experience with George. The API is targeted at experienced developers and is by no means a replacement for TVPaint or George but simply builds on it. - We are not affiliated with the TVPaint developers therefore we won't fix bugs in the software, PyTVPaint is a separate project created here at BRUNCH Studio to facilitate our developer experience. +We are not affiliated with the TVPaint development team and therefore can't fix any bugs in the software or the George API. + +Please direct your issues appropriately; any issues with PyTVPaint should be submitted as [an issue in this repository](https://github.com/brunchstudio/pytvpaint/issues) or the [C++ plugin's repository](https://github.com/brunchstudio/tvpaint-rpc), any issues with TVPaint the software should be addressed to the [tvp support team](https://tvpaint.odoo.com/en_US/contactus). + +For any questions on the limitations of our API, please head to [this page](limitations.md). From 9fd2a499c9cb4231b42cdb8e463d4c19da6abea5 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 11:33:42 +0100 Subject: [PATCH 20/28] test multiple python versions --- .github/workflows/tests.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 99c1393..68c0096 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,14 +9,33 @@ concurrency: jobs: unit-tests: runs-on: self-hosted + concurrency: group: uses-tvpaint cancel-in-progress: true + + strategy: + fail-fast: true + max-parallel: 1 + matrix: + python-version: [python39, python310, python311, python] + + env: + VENV_PATH: ${GITHUB_WORKSPACE}/env_${{ matrix.python-version }} + steps: - uses: actions/checkout@v4 + - name: Switching Python version + run: scoop reset ${{ matrix.python-version }} + + - name: Setup Poetry in venv + run: | + python -m venv $VENV_PATH + $VENV_PATH/Scripts/pip.exe install poetry + - name: Installing dependencies... - run: poetry install --no-interaction --with test + run: $VENV_PATH/Scripts/poetry install --no-interaction --with test - name: Pytest - run: poetry run pytest --maxfail 5 --cov=pytvpaint + run: $VENV_PATH/Scripts/poetry run pytest --maxfail 5 --cov=pytvpaint From ab03f4f36bd4ca45c9c5ab7e7021a6a7aa9828d0 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 11:39:16 +0100 Subject: [PATCH 21/28] use env syntax --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 68c0096..fb43f49 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,11 +31,11 @@ jobs: - name: Setup Poetry in venv run: | - python -m venv $VENV_PATH - $VENV_PATH/Scripts/pip.exe install poetry + python -m venv ${{ env.VENV_PATH }} + ${{ env.VENV_PATH }}/Scripts/pip.exe install poetry - name: Installing dependencies... - run: $VENV_PATH/Scripts/poetry install --no-interaction --with test + run: ${{ env.VENV_PATH }}/Scripts/poetry.exe install --no-interaction --with test - name: Pytest - run: $VENV_PATH/Scripts/poetry run pytest --maxfail 5 --cov=pytvpaint + run: ${{ env.VENV_PATH }}/Scripts/poetry.exe run pytest --maxfail 5 --cov=pytvpaint From d102da207bc0f7461ae5958eec0ca14969710086 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 11:40:51 +0100 Subject: [PATCH 22/28] fix: use github.workspace --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fb43f49..1b74ba9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: python-version: [python39, python310, python311, python] env: - VENV_PATH: ${GITHUB_WORKSPACE}/env_${{ matrix.python-version }} + VENV_PATH: ${{ github.workspace }}/env_${{ matrix.python-version }} steps: - uses: actions/checkout@v4 From 73edefaacd10aec1136d34e01ba4d3e6b1d46b7c Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 12:10:18 +0100 Subject: [PATCH 23/28] fixes venv --- .github/workflows/tests.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1b74ba9..29407cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,9 @@ jobs: env: VENV_PATH: ${{ github.workspace }}/env_${{ matrix.python-version }} + VENV_SCRIPTS: ${{ env.VENV_PATH }}/Scripts + PIP_EXE: ${{ env.VENV_SCRIPTS }}/pip.exe + POETRY_EXE: ${{ env.VENV_SCRIPTS }}/poetry.exe steps: - uses: actions/checkout@v4 @@ -32,10 +35,13 @@ jobs: - name: Setup Poetry in venv run: | python -m venv ${{ env.VENV_PATH }} - ${{ env.VENV_PATH }}/Scripts/pip.exe install poetry + ${{ env.PIP_EXE }} install --quiet poetry - - name: Installing dependencies... - run: ${{ env.VENV_PATH }}/Scripts/poetry.exe install --no-interaction --with test + - name: Installing dependencies with Poetry... + run: | + rm -r ${{ github.workspace }}/.venv + ${{ env.POETRY_EXE }} config virtualenvs.in-project true + ${{ env.POETRY_EXE }} install --no-interaction --with test - - name: Pytest + - name: Pytest with coverage run: ${{ env.VENV_PATH }}/Scripts/poetry.exe run pytest --maxfail 5 --cov=pytvpaint From 2a5585cf01954b0ac141232ce16f8f0b58301304 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 12:13:17 +0100 Subject: [PATCH 24/28] fix env key --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 29407cb..96e3786 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,9 +22,6 @@ jobs: env: VENV_PATH: ${{ github.workspace }}/env_${{ matrix.python-version }} - VENV_SCRIPTS: ${{ env.VENV_PATH }}/Scripts - PIP_EXE: ${{ env.VENV_SCRIPTS }}/pip.exe - POETRY_EXE: ${{ env.VENV_SCRIPTS }}/poetry.exe steps: - uses: actions/checkout@v4 @@ -33,11 +30,15 @@ jobs: run: scoop reset ${{ matrix.python-version }} - name: Setup Poetry in venv + env: + PIP_EXE: ${{ env.VENV_PATH }}/Scripts/pip.exe run: | python -m venv ${{ env.VENV_PATH }} ${{ env.PIP_EXE }} install --quiet poetry - name: Installing dependencies with Poetry... + env: + POETRY_EXE: ${{ env.VENV_PATH }}/Scripts/poetry.exe run: | rm -r ${{ github.workspace }}/.venv ${{ env.POETRY_EXE }} config virtualenvs.in-project true From 9d41c103adb82381c2a3956b47f266e81f0f9390 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 12:21:58 +0100 Subject: [PATCH 25/28] test if .venv folder exists --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 96e3786..c2d93e7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,8 +39,10 @@ jobs: - name: Installing dependencies with Poetry... env: POETRY_EXE: ${{ env.VENV_PATH }}/Scripts/poetry.exe + POETRY_VENV: ${{ github.workspace }}/.venv + shell: pwsh run: | - rm -r ${{ github.workspace }}/.venv + if (test-path ${{ env.POETRY_VENV }}) { rm -r ${{ env.POETRY_VENV }} } ${{ env.POETRY_EXE }} config virtualenvs.in-project true ${{ env.POETRY_EXE }} install --no-interaction --with test From 340b5e94588c6d52f907feba3ecc60cb061d1cb8 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 12:23:54 +0100 Subject: [PATCH 26/28] powershell --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2d93e7..38521d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,7 +40,7 @@ jobs: env: POETRY_EXE: ${{ env.VENV_PATH }}/Scripts/poetry.exe POETRY_VENV: ${{ github.workspace }}/.venv - shell: pwsh + shell: powershell run: | if (test-path ${{ env.POETRY_VENV }}) { rm -r ${{ env.POETRY_VENV }} } ${{ env.POETRY_EXE }} config virtualenvs.in-project true From d96f4bdfbc2eeb68dd56f6367e218cafac3c710a Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 12:39:23 +0100 Subject: [PATCH 27/28] use python path directly --- .github/workflows/tests.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 38521d7..d1f4e47 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,23 +18,19 @@ jobs: fail-fast: true max-parallel: 1 matrix: - python-version: [python39, python310, python311, python] + python-version: [python39, python310, python311, python312] env: VENV_PATH: ${{ github.workspace }}/env_${{ matrix.python-version }} + PYTHON_EXE: C:/Users/usermuster/scoop/apps/${{ matrix.python-version }}/current/python.exe steps: - uses: actions/checkout@v4 - - name: Switching Python version - run: scoop reset ${{ matrix.python-version }} - - name: Setup Poetry in venv - env: - PIP_EXE: ${{ env.VENV_PATH }}/Scripts/pip.exe run: | - python -m venv ${{ env.VENV_PATH }} - ${{ env.PIP_EXE }} install --quiet poetry + ${{ env.PYTHON_EXE }} -m venv ${{ env.VENV_PATH }} + ${{ env.VENV_PATH }}/Scripts/pip.exe install --quiet poetry - name: Installing dependencies with Poetry... env: From 71ce9c1a60967fe4343bdb5b7bdceaad165f0182 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Wed, 20 Mar 2024 15:16:18 +0100 Subject: [PATCH 28/28] ruff and mypy fixes --- README.md | 2 +- pytvpaint/clip.py | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2d46075..0ffccd0 100644 --- a/README.md +++ b/README.md @@ -85,4 +85,4 @@ For any questions on the limitations of our API, please head to [this page](http Made with ❤️ at -[](https://brunchstudio.tv/) +[](https://brunchstudio.tv/) diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 02efd85..e2ab390 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -410,7 +410,7 @@ def load_media( return new_layer - def _handle_output_range( + def _handle_output_range( # noqa: C901 self, output_path: Path | str | FileSequence, start: int | None = None, @@ -431,12 +431,12 @@ def _handle_output_range( ValueError: if output range (start/end) are inferior to clip start frame Bug: - TVPaint will not render the requested/correct range in some cases. Having no control over this inconsistent - behaviour and to avoid issues we will raise a ValueError if an invalid range is detected. You can however - force an incorrect range using `force_range=True`, we will then log a warning when this happens (to help - with debugging) and the function will render with your range without checking the output at the end. - For more details on the different issues with frame ranges and the timeline in TVPaint, please check the - `Limitations` section of the documentation which explains this in more detail. + TVPaint will not render the requested/correct range in some cases. Having no control over this inconsistent + behaviour and to avoid issues we will raise a ValueError if an invalid range is detected. You can however + force an incorrect range using `force_range=True`, we will then log a warning when this happens (to help + with debugging) and the function will render with your range without checking the output at the end. + For more details on the different issues with frame ranges and the timeline in TVPaint, please check the + `Limitations` section of the documentation which explains this in more detail. Returns: file_sequence: output path as a FileSequence object @@ -496,6 +496,7 @@ def _handle_output_range( err_msg = "" clip_start = self.start clip_end = self.end + if use_camera: if start < clip_start or end > clip_end: err_msg = ( @@ -606,10 +607,14 @@ def render( # raises error if sequence not found found_sequence = FileSequence.findSequenceOnDisk(str(file_sequence)) frame_set = found_sequence.frameSet() + file_sequence_frame_set = file_sequence.frameSet() + + if frame_set is None or file_sequence_frame_set is None: + raise Exception("Frameset should be defined") if not frame_set.issuperset(file_sequence.frameSet()): # not all frames found - missing_frames = file_sequence.frameSet().difference(frame_set) + missing_frames = file_sequence_frame_set.difference(frame_set) raise FileNotFoundError( f"Not all frames found, missing frames ({missing_frames}) " f"in sequence : {output_path}"