From d69f50263fadc5fcf5de2f1870278baaa47771b9 Mon Sep 17 00:00:00 2001 From: Radouane Lahmidi Date: Wed, 12 Mar 2025 19:34:22 +0100 Subject: [PATCH 01/26] UPDATE code for TVPaint-12 --- docs/limitations.md | 16 +- docs/usage/rendering.md | 85 +- pyproject.toml | 1 + pytvpaint/camera.py | 162 ++- pytvpaint/clip.py | 117 +- pytvpaint/george/client/__init__.py | 2 +- pytvpaint/george/client/parse.py | 21 +- pytvpaint/george/grg_base.py | 94 +- pytvpaint/george/grg_camera.py | 28 +- pytvpaint/george/grg_layer.py | 173 ++- pytvpaint/george/grg_project.py | 52 +- pytvpaint/george/grg_scene.py | 32 +- pytvpaint/layer.py | 263 +++- pytvpaint/project.py | 10 +- pytvpaint/scene.py | 55 +- tests/conftest.py | 57 +- tests/george/client/test_init.py | 4 +- tests/george/test_base.py | 282 ---- tests/george/test_grg_base.py | 12 +- tests/george/test_grg_camera.py | 5 +- tests/george/test_grg_clip.py | 1748 ++++++++++++------------ tests/george/test_grg_layer.py | 1911 ++++++++++++++------------- tests/george/test_grg_project.py | 1146 ++++++++-------- tests/george/test_grg_scene.py | 212 +-- tests/george/test_utils.py | 22 +- tests/test_camera.py | 174 +-- tests/test_clip.py | 1048 +++++++-------- tests/test_clip_sound.py | 42 +- tests/test_layer.py | 732 +++++----- tests/test_layer_instance.py | 68 +- tests/test_project.py | 878 ++++++------ tests/test_project_sound.py | 42 +- tests/test_scene.py | 176 +-- tests/test_utils.py | 50 +- 34 files changed, 5203 insertions(+), 4517 deletions(-) delete mode 100644 tests/george/test_base.py diff --git a/docs/limitations.md b/docs/limitations.md index 4ccbed1..3977847 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -83,10 +83,12 @@ PyTVPaint's documentation is usually more accurate. We will try to keep a list of the bugs/inconsistencies we encountered with any George commands, and describe the issues in the table below: -| Method | Description | -|:-------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------| -| [`tv_Ratio`](api/george/project.md#pytvpaint.george.grg_project.tv_ratio) | Always returns an empty string (`""`) | -| [`tv_InstanceName`](api/george/layer.md#pytvpaint.george.grg_layer.tv_instance_name) | Crashes if we provided with an invalid `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` seems to work for the current clip | -| `tv_LayerSelectInfo` | Does not select frames as stated in the documentation and will also return non selected frames if attribute `full` is set to True | +| Method | Description | +|:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`tv_Ratio`](api/george/project.md#pytvpaint.george.grg_project.tv_ratio) | Always returns an empty string (`""`) | +| [`tv_InstanceName`](api/george/layer.md#pytvpaint.george.grg_layer.tv_instance_name) | Crashes if we provided with an invalid `layer_id` | +| `tv_CameraEnumPoints` | Only returns the first point, no matter how many points there are. | +| `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` seems to work for the current clip | +| `tv_LayerSelectInfo` | Does not select frames as stated in the documentation and will also return non selected frames if attribute `full` is set to True | +| `tv_ProjectSaveAudioDependencies` and `tv_ProjectSaveVideoDependencies` | Missing arguments in documentation rendering the function useless, thankfully someone provided the correct details [here](http://tvpaint.net/forum/viewtopic.php?p=136709) | diff --git a/docs/usage/rendering.md b/docs/usage/rendering.md index 2c5223a..22e425f 100644 --- a/docs/usage/rendering.md +++ b/docs/usage/rendering.md @@ -47,7 +47,7 @@ range values tend to change and are seemingly handled differently between the UI PyTVPaint handles frames differently in an effort to have a similar behaviour to the other industry software we're familiar with (Premiere, Maya, etc...). Meaning that the ranges provided to the API will have to be formatted a certain -way, with the API handling all the appropriate range conversion behind the scenes. +way, with the API handling all the appropriate range conversions behind the scenes. To explain how this works we first need to review how TVPaint handles ranges (grab a cup coffee, this might take a while...). @@ -154,7 +154,7 @@ p.start_frame = 0 george.tv_save_sequence('out.#.png', 0, 11) # => renders a sequence of 11 frames with our updated range of (0-11) -george.tv_project_save_sequence('out.#.png', start=53, end=63) +george.tv_project_save_sequence('out.#.png', start=0, end=11) # ERROR : only renders 6 frames ``` @@ -165,8 +165,8 @@ so what's happening ? To understand what's happening we need to understand how TVPaint handles its timeline...and the answer is that it doesn't really handle a timeline. TVPaint's elements are handled more like lists than anything else, so all elements start at `0` -and have a range of `(0 to N)`, however there are really two lists; projects have their timeline/list and clips also have -their own timeline/list that is connected to the project's timeline. So to recap: +and have a range of `(0 to N)`. However, there are really *two* lists; projects have their timeline/list and clips also have +their own individual timeline/list that is connected to the project's timeline. So to recap: - Clip: a clip's timeline always starts at 0 and goes to N, N being the last image in the "longest" layer in the clip. - Project: a project's timeline always starts at 0 and goes to N, N being the last frame in the last clip of the project. @@ -186,11 +186,11 @@ project's start frame. So to correct our second example, we'd need to subtract t we want to render, like so : ```python -p_start = 53 +project_start = 53 start, end = (53, 63) # clean range before render -start = (start - p_start) # 0 -end = (end - p_start) # 10 +start = (start - project_start) # 0 +end = (end - project_start) # 10 george.tv_save_sequence('out.#.png', start, end) # => renders a correct sequence of 11 frames with a range of (53-63) (as seen in the timeline in the Clip's UI) @@ -205,8 +205,9 @@ Except we kind of tried this already when we reverted the project start frame to ### Mark IN/OUT The reason the project has only been rendering 6 frames is because we set a mark IN and OUT on the clip at `(55-60)`. -This means that the project now only sees the frames between the mark IN and OUT _(see figures 8 and 9 below)_. If we had only set the mark -IN but not the mark OUT, the project would see all the frames between the mark IN and the clip's end frame and vice versa. +This means that the project now only sees the frames between the mark IN and OUT _(see figures 8 and 9 below)_. If we +had only set the mark IN but not the mark OUT, the project would see all the frames between the mark IN and the clip's +end frame and vice versa. ![](../assets/range8.png)
figure 8 - No Mark IN/OUT
@@ -215,40 +216,45 @@ IN but not the mark OUT, the project would see all the frames between the mark I ![](../assets/range9.png)
figure 9 - Project Timeline with same clip Mark IN/OUT at 55/60
So this explains why we only had 6 frames rendered, since the duration of the range `(55-60)` is equal to 6 frames. -You probably noticed that there is still an issue. The project now says it's range is `(53-59)` _(figure 9 above)_ while the clip -says it's range _(when taking the Mark IN/OUT into account)_ is at `(55-60)` and it's full range is _(without mark IN/OUT)_ is `(53-63)` _(figure 7 above)_ + +You probably noticed that there is still an issue. The project now says it's range is `(53-59)` _(figure 9 above)_ +while the clip says it's range _(when taking the Mark IN/OUT into account)_ is at `(55-60)` and it's full range is +_(without mark IN/OUT)_ is `(53-63)` _(figure 7 above)_ This is not a bug, when you take into account the fact that the project's timeline is separate from the clip timeline and ignores the frames outside the Mark IN/OUT it starts to make sense. -We know the project start frame is ignored by the George rendering functions (as mentioned above) and all timelines (project and clip) -really start a 0. So our project would only see 6 frames in the clip (the one between Mark IN `55` and the Mark OUT `60`), -the project's timeline is really `(0-5)` + the project start frame `53` and we get `(53-58)`. +We know the project start frame is ignored by the rendering functions (as mentioned above) and all timelines +(project and clip) really start a 0. So our project would only see 6 frames in the clip (the one between Mark IN `55` +and the Mark OUT `60`), the project's timeline is really `(0-5)` + the project start frame `53` and we get `(53-58)`. So to fix our previous example and render our clip from the project we would need to do this ```python -p_start = 53 +project_start = 53 start, end = (53, 63) # clean range before render -start = (start - p_start) # 0 -end = (end - p_start) # 10 +start = (start - project_start) # 0 +end = (end - project_start) # 10 mark_in, mark_out = (55, 60) # clean mark IN/OUT range before render -mark_in = (mark_in - p_start) # 2 -mark_out = (mark_out - p_start) # 7 +mark_in = (mark_in - project_start) # 2 +mark_out = (mark_out - project_start) # 7 george.tv_save_sequence('out.#.png', start, end) -# => renders a correct sequence of 11 frames with a range of (53-63) (as seen in the timeline in the Clip's UI) +# => renders a correct sequence of 11 frames with a range of (53-63) +# (as seen in the timeline in the Clip's UI) george.tv_save_sequence('out.#.png', mark_in, mark_out) -# => renders a correct sequence of 6 frames with a range of (55-50) (as seen in the timeline in the Clip's UI) +# => renders a correct sequence of 6 frames with a range of (55-60) +# (as seen in the timeline in the Clip's UI) -render_duration = (mark_in - mark_out) + 1 # 6 +render_duration = (mark_in - mark_out) + 1 # ==> (55 - 60) + 1 ==> 6 george.tv_project_save_sequence('out.#.png', start=0, end=render_duration) -# => renders a correct sequence of 6 frames with a range of (53-58) (as seen in the timeline in the Project's UI) +# => renders a correct sequence of 6 frames with a range of (55-60 for the project, 53-58 for the clip) +# (as seen in the timeline in the Project's UI) ``` !!! Note @@ -285,26 +291,29 @@ So if we want to render the new clip we would do : # rendering from the clip, using the clip's timeline george.tv_save_sequence('out.#.png', 0, 4) -# => renders a correct sequence of 5 frames with a range of (53-57) (as seen in the timeline in the Clip's UI) +# => renders a correct sequence of 5 frames with a range of (53-57) +# (as seen in the timeline in the Clip's UI) # rendering from the project, using the project's timeline george.tv_project_save_sequence('out.#.png', start=6, end=10) -# => renders a correct sequence of 6 frames with a range of (59-63) (as seen in the timeline in the Project's UI) +# => renders a correct sequence of 6 frames with a range of (59-63) +# (as seen in the timeline in the Project's UI) ``` ### Rendering the camera -!!! tip +!!! Tip Rendering the camera does not affect the timeline, however only `tv_project_save_sequence` has an option to render the camera. So you'll have to use the project and it's range to render any frames with the camera. ### Invalid Ranges -An invalid range can be provided to both functions and instead of raising an error, both will render some frames. -Here we will try to describe the observed behavior when encountering an invalid range. +TVPaint does not raise an error when provided with an invalid range, and such a range can be provided to both rendering +functions and both will render some frames, though often the incorrect ones. +Here we will try to describe what an invalid is and the observed behavior when encountering an invalid range. -!!! info +!!! Info A rendering range is considered invalid if it starts before the Project's or Clip's start frame and ends after the Project's or Clip's end frame @@ -316,6 +325,14 @@ For a project using `tv_project_save_sequence` | range starts before the project's start frame | Renders all images in the project | | range ends after the project's end frame | Renders all images in the project | + +!!! Warning + + You might think that using invalid range is fine with `tv_project_save_sequence` since it stills renders all frames, + but depending on the first frame provided to TVPaint you might end up with the wrong range in your file names, + Ex: a project with a range of (0-20) if rendered with a range (-1, 22) will output 21 frames all named incorrectly + and starting at -1 instead of 0, si it is important to avoid invalid ranges as much as possible + For a clip using `tv_save_sequence` | Range Error | Renders | @@ -328,9 +345,9 @@ For a clip using `tv_save_sequence` ### How PyTVPaint handles a frame range -We it comes to our API, we take a very `what you see is what you get` approach when handling the timeline. Basically if timeline in the UI says -that your clip's range is `(55-60)` (as seen in the examples above) then that is what you should provide to our functions. -PyTVPaint will handle the range conversion behind the scenes. +When it comes to our API, we take a very `what you see is what you get` approach when handling the timeline. +Basically if the timeline in the UI says that your clip's range is `(55-60)` (as seen in the examples above) then that +is what you should provide to our functions. PyTVPaint will then handle the range conversion behind the scenes. ```python from pytvpaint.clip import Clip @@ -387,8 +404,9 @@ print(c2.timeline_end) # => 63 !!! Warning - PyTVPaint uses `tv_project_save_sequence` to render frames for the project and the clips, this means that when + PyTVPaint uses `tv_project_save_sequence` to render both the project and the clips, this means that when rendering a clip, it will consider any range outside the clip's Mark IN/OUT (if they have been set) as invalid. + We do not use `tv_save_sequence` as it doesn't handle camera rending. !!! Warning @@ -401,3 +419,4 @@ print(c2.timeline_end) # => 63 Even tough pytvpaint does a pretty good job of correcting the frame ranges for rendering, we're still encountering some weird edge cases where TVPaint will consider the range invalid for seemingly no reason. + diff --git a/pyproject.toml b/pyproject.toml index c5c7a5f..3010d5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ python = ">=3.9" websocket-client = "^1.7.0" typing-extensions = "^4.9.0" fileseq = "^2.0.0" +packaging = ">=22.0" [tool.poetry.group.dev] optional = true diff --git a/pytvpaint/camera.py b/pytvpaint/camera.py index 61ecdca..3a4625e 100644 --- a/pytvpaint/camera.py +++ b/pytvpaint/camera.py @@ -5,7 +5,7 @@ from collections.abc import Iterator from typing import TYPE_CHECKING -from pytvpaint import george, utils +from pytvpaint import george, log, utils from pytvpaint.utils import ( Refreshable, Removable, @@ -15,6 +15,13 @@ if TYPE_CHECKING: from pytvpaint.clip import Clip + from pytvpaint.layer import Layer + + +# FIXME Camera.anti_aliasing always returns 1, Camera.fps no longer returns/sets fps, now only fps is Project fps +# FIXME most Camera values can still be edited/queried using george.tv_camera_info_set() but they are not reflected in +# UI and so can be unintentionally edited or reset +# FIXME george.tv_camera_info_get() values of pixel aspect ratio and fps have been swapped class Camera(Refreshable): @@ -30,7 +37,7 @@ def __init__(self, clip: Clip, data: george.TVPCamera | None = None) -> None: self._points: list[CameraPoint] = [] def refresh(self) -> None: - """Refreshed the camera data.""" + """Refreshes the camera data.""" if not self.refresh_on_call and self._data: return self._data = george.tv_camera_info_get() @@ -72,12 +79,20 @@ def height(self, value: int) -> None: @refreshed_property @set_as_current + @george.deprecated_warning( + msg="DEPRECATED: Property `Camera.fps` is not recommended for use in TVP 12, use Project.fps instead. " + "For now, in TVP 12, it always returns 1.0 but will be removed in future versions." + ) def fps(self) -> float: """The framerate of the camera.""" return self._data.frame_rate @fps.setter @set_as_current + @george.deprecated_warning( + msg="DEPRECATED: Property `Camera.fps` is not recommended for use in TVP 12, use Project.fps instead. " + "For now, in TVP 12, it always sets 1.0 but will be removed in future versions." + ) def fps(self, value: float) -> None: george.tv_camera_info_set( self.width, @@ -105,8 +120,17 @@ def pixel_aspect_ratio(self, value: float) -> None: @refreshed_property @set_as_current + @george.deprecated_warning( + msg="DEPRECATED: Property `Camera.anti_aliasing` not longer exists in TVP 12, " + "for now, in TVP 12, it always returns 1 but will be removed in future versions." + ) def anti_aliasing(self) -> int: - """The antialiasing value of the camera.""" + """The antialiasing value of the camera. + + Warning: + DEPRECATED: This property has been removed from in TVP 12 and for now always returns 1 but will be removed + in future versions. + """ return self._data.anti_aliasing @refreshed_property @@ -120,6 +144,20 @@ def field_order(self) -> george.FieldOrder: def field_order(self, value: george.FieldOrder) -> None: george.tv_camera_info_set(self.width, self.height, field_order=value) + @property + @george.min_version_compatible(min_version="12") + @set_as_current + def layer(self) -> Layer | None: + """The layer associated to this camera. + + Note: + This function is only available in TVPaint version 12 and above. + + Raises: + NotImplemented: if used in tvpaint version inferior to 12 + """ + return self.clip.camera_layer + @set_as_current def insert_point( self, @@ -135,7 +173,18 @@ def insert_point( @property @set_as_current def points(self) -> Iterator[CameraPoint]: - """Iterator for the `CameraPoint` objects of the camera.""" + """Iterator for the `CameraPoint` objects of the camera. + + Warning: + Property `Camera.points` usually only returns the first camera point and nothing else, + this is a TVPaint limitation, we advise using `Camera.get_point_data_at()` to get more accurate results. + """ + log.warning( + "Property `Camera.points` usually only returns the first camera point and nothing else, " + "this is a TVPaint limitation, we advise using `Camera.get_point_data_at()` to get more accurate " + "results." + ) + points_data = utils.position_generator( lambda pos: george.tv_camera_enum_points(pos) ) @@ -143,10 +192,16 @@ def points(self) -> Iterator[CameraPoint]: yield CameraPoint(index, camera=self, data=point_data) @set_as_current - def get_point_data_at(self, position: float) -> george.TVPCameraPoint: + def get_point_data_at(self, position: float) -> InterpolationCameraPoint: """Get the points data interpolated at that position (between 0 and 1).""" position = max(0.0, min(position, 1.0)) - return george.tv_camera_interpolation(position) + return InterpolationCameraPoint(position, self, george.tv_camera_interpolation(position)) + + @set_as_current + def get_point_data_at_frame(self, frame: int) -> FrameCameraPoint: + """Get the points data interpolated at that position (between 0 and 1).""" + real_frame = (frame - self.clip.project.start_frame) + return FrameCameraPoint(frame, self, george.tv_camera_info_frame(real_frame)) @set_as_current def remove_point(self, index: int) -> None: @@ -176,7 +231,7 @@ def __init__( self._data = data or george.tv_camera_enum_points(self._index) def refresh(self) -> None: - """Refreshed the camera point data.""" + """Refreshes the camera point data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -273,6 +328,16 @@ def scale(self, value: float) -> None: scale=value, ) + @property + def width(self) -> float: + """The scale of the camera at that point.""" + return self.camera.clip.project.width * (self.scale * 0.01) + + @property + def height(self) -> float: + """The scale of the camera at that point.""" + return self.camera.clip.project.height * (self.scale * 0.01) + @classmethod def new( cls, @@ -295,3 +360,86 @@ def remove(self) -> None: """ george.tv_camera_remove_point(self.index) self.mark_removed() + + +class InterpolationCameraPoint(CameraPoint): + """A Read-Only CameraPoint. + + You can use them to animate the camera movement. + """ + + def __init__( + self, + interpolation_point: float, + camera: Camera, + data: george.TVPCameraPoint | None = None, + ) -> None: + super().__init__(-1, camera, data) + self._interpolation_point = interpolation_point + + def __repr__(self) -> str: + """String representation of the camera point.""" + return f"CameraPoint({self.camera.clip.name})" + + @property + def interpolation_point(self) -> float: + """Interpolation point (between 0 and 1) for this CameraPoint.""" + return self._interpolation_point + + def refresh(self) -> None: + """Refreshes the camera point data at the interpolation point.""" + if not self.refresh_on_call and self._data: + return + self._data = self.camera.get_point_data_at(self._interpolation_point) + + def remove(self) -> None: + """Remove the camera point. + + Warning: + the FrameCameraPoint instance is read-only and cannot be removed as it doesn't really exist + """ + log.warning("Read-Only InterpolationCameraPoint cannot be deleted as it doesn't really exist, " + "ignoring request.") + return + + +class FrameCameraPoint(CameraPoint): + """A Read-Only CameraPoint. + + You can use them to animate the camera movement. + """ + + def __init__( + self, + frame: int, + camera: Camera, + data: george.TVPCameraPoint | None = None, + ) -> None: + super().__init__(-1, camera, data) + self._frame = frame + + def __repr__(self) -> str: + """String representation of the camera point.""" + return f"CameraPoint({self.camera.clip.name})" + + @property + def frame(self) -> float: + """Frame for this CameraPoint.""" + return self._frame + + def refresh(self) -> None: + """Refreshes the camera point data at the frame.""" + if not self.refresh_on_call and self._data: + return + real_frame = (self._frame - self.camera.clip.project.start_frame) + self._data = george.tv_camera_info_frame(real_frame) + + def remove(self) -> None: + """Remove the camera point. + + Warning: + the FrameCameraPoint instance is read-only and cannot be removed as it doesn't really exist + """ + log.warning("Read-Only FrameCameraPoint cannot be deleted as it doesn't really exist, " + "ignoring request.") + return diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 7ad148a..474b00a 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -4,13 +4,13 @@ from collections.abc import Iterator from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from fileseq.filesequence import FileSequence from pytvpaint import george, utils from pytvpaint.camera import Camera -from pytvpaint.layer import Layer, LayerColor +from pytvpaint.layer import CameraLayer, CTGLayer, Layer, LayerColor, LayerFolder from pytvpaint.sound import ClipSound from pytvpaint.utils import ( Removable, @@ -116,7 +116,7 @@ def scene(self) -> Scene: """The clip's scene. Raises: - ValueError: if clip cannot be found in the project + ValueError: if no current scene in the project """ for scene in self.project.scenes: for other_clip in scene.clips: @@ -326,19 +326,99 @@ def layer_ids(self) -> Iterator[int]: """Iterator over the layer ids.""" return utils.position_generator(lambda pos: george.tv_layer_get_id(pos)) + def _all_layers(self, filter_types: tuple[type, ...] | None = None, ignore_types: tuple[type, ...] | None = None) -> Iterator[Layer]: + """Iterator over the clip's layers. + + Note: + This function is only available in TVPaint version 12 and above. + """ + for layer_id in self.layer_ids: + layer_data = george.tv_layer_info(layer_id) + + layer_class = Layer + if layer_data.type == george.LayerType.FOLDER: + layer_class = LayerFolder + elif layer_data.type == george.LayerType.CAMERA: + layer_class = CameraLayer + # handle CTG layers like regular layers in TVP versions < 12 + elif not george.is_tvp_version_below_12() and layer_data.type is george.LayerType.SCRIBBLES: + layer_class = CTGLayer + + if filter_types and layer_class not in filter_types: + continue + if ignore_types and layer_class in ignore_types: + continue + + yield layer_class(layer_id, clip=self, data=layer_data) + + @property + @george.min_version_compatible(min_version="12") + def all_layers(self) -> Iterator[Layer]: + """Iterator over the clip's layers regardless of their type. + + Note: + This function is only available in TVPaint version 12 and above. + + Raises: + NotImplemented: if used in tvpaint version inferior to 12 + """ + yield from self._all_layers() + @property def layers(self) -> Iterator[Layer]: - """Iterator over the clip's layers.""" - from pytvpaint.layer import Layer + """Iterator over the clip's layers, ignores all Folder, Camera and CTG layers.""" + yield from self._all_layers(ignore_types=(LayerFolder, CameraLayer, CTGLayer)) - for layer_id in self.layer_ids: - yield Layer(layer_id, clip=self) + @property + @george.min_version_compatible(min_version="12") + def ctg_layers(self) -> Iterator[CTGLayer]: + """Iterator over the clip's CTG layers. + + Note: + This function is only available in TVPaint version 12 and above. + + Raises: + NotImplemented: if used in tvpaint version inferior to 12 + """ + yield from self._all_layers(filter_types=(CTGLayer, )) + + @property + @george.min_version_compatible(min_version="12") + def folders(self) -> Iterator[LayerFolder]: + """Iterator over the clip's Folder layers. + + Note: + This function is only available in TVPaint version 12 and above. + + Raises: + NotImplemented: if used in tvpaint version inferior to 12 + """ + yield from self._all_layers(filter_types=(LayerFolder, )) + + @property + @george.min_version_compatible(min_version="12") + def camera_layer(self) -> CameraLayer | None: + """Iterator over the clip's Folder layers. + + Note: + This function is only available in TVPaint version 12 and above. + + Raises: + NotImplemented: if used in tvpaint version inferior to 12 + """ + camera_layer = next(self._all_layers(filter_types=(CameraLayer, )), None) + if camera_layer: + return cast(CameraLayer, camera_layer) + + # if we're here then the camera layer has probably been deleted, let's recreate it by switching to the camera. + george.tv_set_active_shape(george.TVPShape.CAMERA) + return cast(CameraLayer, next(self._all_layers(filter_types=(CameraLayer, )), None)) @property @set_as_current def layer_names(self) -> Iterator[str]: """Iterator over the clip's layer names.""" - for layer in self.layers: + for layer in self._all_layers(): yield layer.name @property @@ -346,12 +426,12 @@ def current_layer(self) -> Layer: """Get the current layer in the clip. Raises: - ValueError: if clip cannot be found in the project + ValueError: if no current layer in clip """ - for layer in self.layers: + for layer in self._all_layers(): if layer.is_current: return layer - raise Exception("Couldn't find a current layer") + raise ValueError("Couldn't find a current layer") def get_layer( self, @@ -359,13 +439,26 @@ def get_layer( by_name: str | None = None, ) -> Layer | None: """Get a specific layer by id or name.""" - return utils.get_tvp_element(self.layers, by_id, by_name) + return utils.get_tvp_element(self._all_layers(), by_id, by_name) @set_as_current def add_layer(self, layer_name: str) -> Layer: """Add a new layer in the layer stack.""" return Layer.new(name=layer_name, clip=self) + @george.min_version_compatible(min_version="12") + @set_as_current + def add_layer_folder(self, folder_name: str) -> LayerFolder: + """Add a new layer in the layer stack. + + Note: + This function is only available in TVPaint version 12 and above. + + Raises: + NotImplemented: if used in tvpaint version inferior to 12 + """ + return cast(LayerFolder, LayerFolder.new(name=folder_name, clip=self)) + @property def selected_layers(self) -> Iterator[Layer]: """Iterator over the selected layers.""" diff --git a/pytvpaint/george/client/__init__.py b/pytvpaint/george/client/__init__.py index 1e981b6..a18f5ca 100644 --- a/pytvpaint/george/client/__init__.py +++ b/pytvpaint/george/client/__init__.py @@ -144,7 +144,7 @@ def send_cmd( # Test for basic ERROR X values and user provided custom errors res_in_error_values = error_values and result in list(map(str, error_values)) if res_in_error_values or re.match(r"ERROR -?\d+", result, re.IGNORECASE): - msg = f"Received value: '{result}' considered as an error" + msg = f"Received value: `{result}` which is considered as an error" raise GeorgeError(msg, error_value=result) return result diff --git a/pytvpaint/george/client/parse.py b/pytvpaint/george/client/parse.py index 73fa29c..ed8696c 100644 --- a/pytvpaint/george/client/parse.py +++ b/pytvpaint/george/client/parse.py @@ -111,10 +111,11 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: f"Enum index {index} is out of bounds (max {len(enum_members) - 1})" ) - if get_origin(cast_type) is tuple: + if get_origin(cast_type) in (tuple, list): # Split by space and convert each member to the right type - values_types = zip(value.split(" "), get_args(cast_type)) - return cast(T, tuple(tv_cast_to_type(v, t) for v, t in values_types)) + value_type = get_args(cast_type)[0] + values_types = [tv_cast_to_type(v, value_type) for v in value.split()] + return cast(T, get_origin(cast_type)(values_types)) if cast_type == bool: return cast(T, value.lower() in ["1", "on", "true"]) @@ -128,7 +129,7 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: FieldTypes: TypeAlias = list[tuple[str, Any]] -def _get_dataclass_fields( +def get_dataclass_fields( datacls: DataclassInstance | type[DataclassInstance], ) -> FieldTypes: """Get the dataclass key/type pairs and filter those with the "parsed" metadata. @@ -164,7 +165,7 @@ def tv_parse_dict( """ # For dataclasses get the type hints and filter those with metadata if is_dataclass(with_fields): - with_fields = _get_dataclass_fields(with_fields) + with_fields = get_dataclass_fields(with_fields) else: # Explicitly cast because we are sure now with_fields = cast(FieldTypes, with_fields) @@ -253,7 +254,7 @@ def tv_parse_list( # Get type annotations from the dataclass fields if is_dataclass(with_fields): - with_fields = _get_dataclass_fields(with_fields) + with_fields = get_dataclass_fields(with_fields) else: # Explicitly cast because we are sure now with_fields = cast(FieldTypes, with_fields) @@ -291,12 +292,14 @@ def args_dict_to_list(args: dict[str, Any]) -> list[Any]: def validate_args_list(optional_args: Sequence[Value | tuple[Value, ...]]) -> list[Any]: - """Some George functions only accept a list of values and not key:value pairs, so to set the last positional argument for instance, you need to give all the previous ones. + """Validates *args equivalent for tvpaint - This function allows you to give a list of argument or key:value pairs (as tuples) and check that they are not None. + Some George functions only accept a list of values and not key:value pairs. If for instance, you need to set the + last positional argument for a function call, you need to provide all the preceding arguments. + This function, given a list of arguments or key:value pairs (as tuples) checks that they are valid/not None. For example, for `tv_camerainfo [ []]` - you can't give `[500, None, "upper"]` because `` is not defined. + you can't pass `[500, None, "upper"]` because `` is not defined. Args: optional_args: list of values or tuple of values (args block) diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index c6d6c12..ab8f4ba 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -3,12 +3,14 @@ from __future__ import annotations import contextlib +import functools from collections.abc import Generator from dataclasses import dataclass from enum import Enum from pathlib import Path from typing import Any, Callable, TypeVar, cast, overload +from packaging import version from typing_extensions import Literal, TypeAlias from pytvpaint import log @@ -623,15 +625,81 @@ def tv_warn(msg: str) -> None: def tv_version() -> tuple[str, str, str]: - """Returns the software name, version and language.""" + """Returns TVPaint version and language info. + + Returns: + software_name (str): software full name (ex: TVPaint Animation 12.0.0 Pro) + tvp_version (str): version number (ex: 12.0.0) + language (str): language (ex: fr, en, etc...) + """ cmd_fields = [ ("software_name", str), ("version", str), ("language", str), ] res = tv_parse_list(send_cmd("tv_Version"), with_fields=cmd_fields) - software_name, version, language = res.values() - return software_name, version, language + software_name, tvp_version, language = res.values() + return software_name, tvp_version, language + + +def is_tvp_version_below_12() -> bool: + """Helper function to check if the connected tvpaint instance version is below v12.""" + _, tvp_version, _ = tv_version() + return version.parse(tvp_version).major < 12 + + +def min_version_compatible(min_version: str) -> Callable[[T], T]: + """Decorator to apply on object methods. + + Given a minimum version, checks if the current tvpaint version if above the minimum requirement otherwise it + raises a NotImplemented error + + Args: + min_version (str): minimum version required to run this version + + Returns: + the decorated function + """ + + def decorate(func: T) -> T: + @functools.wraps(func) + def applicator(*args: Any, **kwargs: Any) -> Any: + _, tvp_version, _ = tv_version() + current_version = version.parse(tvp_version) + min_version_parse = version.parse(min_version) + + if current_version < min_version_parse: + msg = f"This function is only available in TVPaint version ({min_version}) and above." + raise NotImplementedError(msg) + + return func(*args, **kwargs) + + return cast(T, applicator) + + return decorate + + +def deprecated_warning(msg: str) -> Callable[[T], T]: + """Decorator to apply on object methods. + + Prints a deprecation message when decorated function is called + + Args: + msg (str): deprecation message + + Returns: + the decorated function + """ + + def decorate(func: T) -> T: + @functools.wraps(func) + def applicator(*args: Any, **kwargs: Any) -> Any: + log.warning(msg) + return func(*args, **kwargs) + + return cast(T, applicator) + + return decorate def tv_quit() -> None: @@ -657,13 +725,13 @@ def tv_menu_hide() -> None: def add_some_magic( i_am_a_badass: bool = False, magic_number: int | None = None ) -> None: - """Don't use ! Will change your life forever...""" + """Don't use this function ! It just might change your life forever...""" if not i_am_a_badass: log.warning("Sorry, you're not enough of a badass for this function...") magic_number = magic_number or 14 send_cmd("tv_MagicNumber", magic_number) - log.info("Totally worth it, right ? ^^") + log.info("Totally worth it, right ?! ^^") def tv_menu_show( @@ -1044,8 +1112,16 @@ def tv_set_b_pen_hsl(color: HSLColor) -> HSLColor: return _tv_set_ab_pen("b", color.h, color.s, color.l, color_format="hsl") +@deprecated_warning( + msg="DEPRECATED: Function `tv_pen` is most likely deprecated it is undocumented in the George reference but still " + "works, We advise using `tv_penbrush` instead." +) def tv_pen(size: float) -> float: - """Change current pen tool size. This function is most likely deprecated it is undocumented in the George reference but still works.""" + """Change current pen tool size. + + Warning: + DEPRECATED: Function `tv_pen` is deprecated, We advise using `tv_penbrush` instead. + """ res = tv_parse_dict(send_cmd("tv_Pen", size), with_fields=[("size", float)]) return cast(float, res["size"]) @@ -1056,7 +1132,7 @@ def tv_pen_brush_get(tool_mode: bool = False) -> TVPPenBrush: result = send_cmd("tv_PenBrush", *args) # Remove the first value which is tv_penbrush - result = result[len("tv_penbrush") + 1 :] + result = result[len("tv_penbrush") + 1:] res = tv_parse_dict(result, with_fields=TVPPenBrush) return TVPPenBrush(**res) @@ -1107,8 +1183,8 @@ def tv_line( args = [ *xy1, *xy2, - bool(right_click), - bool(dry), + right_click, + dry, ] send_cmd("tv_Line", *args) diff --git a/pytvpaint/george/grg_camera.py b/pytvpaint/george/grg_camera.py index bde0112..6fdb2e2 100644 --- a/pytvpaint/george/grg_camera.py +++ b/pytvpaint/george/grg_camera.py @@ -3,13 +3,16 @@ from __future__ import annotations from dataclasses import dataclass +from typing import cast from pytvpaint.george.client import send_cmd from pytvpaint.george.client.parse import ( + DataclassInstance, + get_dataclass_fields, tv_parse_list, validate_args_list, ) -from pytvpaint.george.grg_base import FieldOrder, GrgErrorValue +from pytvpaint.george.grg_base import FieldOrder, GrgErrorValue, is_tvp_version_below_12 @dataclass(frozen=True) @@ -21,7 +24,7 @@ class TVPCamera: field_order: FieldOrder frame_rate: float pixel_aspect_ratio: float - anti_aliasing: int + anti_aliasing: int = 1 @dataclass(frozen=True) @@ -36,7 +39,15 @@ class TVPCameraPoint: def tv_camera_info_get() -> TVPCamera: """Get the information of the camera.""" - return TVPCamera(**tv_parse_list(send_cmd("tv_CameraInfo"), with_fields=TVPCamera)) + if is_tvp_version_below_12(): + fields = TVPCamera + else: # values of pixel aspect ratio and fps have been swapped in versions > 12 + fields = get_dataclass_fields(cast(DataclassInstance, TVPCamera)) + fields_keys = list(dict(fields).keys()) + pixel_aspect_index, fps_index = fields_keys.index('pixel_aspect_ratio'), fields_keys.index('frame_rate') + fields[pixel_aspect_index], fields[fps_index] = fields[fps_index], fields[pixel_aspect_index] + + return TVPCamera(**tv_parse_list(send_cmd("tv_CameraInfo"), with_fields=fields)) def tv_camera_info_set( @@ -75,6 +86,17 @@ def tv_camera_interpolation(position: float) -> TVPCameraPoint: return TVPCameraPoint(**res) +def tv_camera_info_frame(frame: int) -> TVPCameraPoint: + """Get the position/angle/scale values at the given frame.""" + errors = ["Given frame out of camera layer's range"] + res = send_cmd("tv_CameraInfoFrame", frame, error_values=errors) + res = tv_parse_list( + res, + with_fields=TVPCameraPoint, + ) + return TVPCameraPoint(**res) + + def tv_camera_insert_point( index: int, x: float, diff --git a/pytvpaint/george/grg_layer.py b/pytvpaint/george/grg_layer.py index 6bc3b82..38bbf25 100644 --- a/pytvpaint/george/grg_layer.py +++ b/pytvpaint/george/grg_layer.py @@ -2,11 +2,13 @@ from __future__ import annotations +import contextlib from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import Any +from pytvpaint import log from pytvpaint.george.client import send_cmd, try_cmd from pytvpaint.george.client.parse import ( args_dict_to_list, @@ -18,6 +20,8 @@ BlendingMode, GrgErrorValue, RGBColor, + is_tvp_version_below_12, + min_version_compatible, ) @@ -97,12 +101,16 @@ class LayerType(Enum): SEQUENCE: XSHEET: SCRIBBLES: + FOLDER: + CAMERA: """ IMAGE = "image" SEQUENCE = "sequence" XSHEET = "xsheet" SCRIBBLES = "scribbles" + FOLDER = "folder" + CAMERA = "camera" class StencilMode(Enum): @@ -241,13 +249,26 @@ def tv_layer_info(layer_id: int) -> TVPLayer: @try_cmd(exception_msg="Couldn't move current layer to position") -def tv_layer_move(position: int) -> None: +def tv_layer_move(position: int, folder_id: int | None = None) -> None: """Move the current layer to a new position in the layer stack. + Args: + position: position to move layer to + folder_id: parent folder id, + Raises: GeorgeError: if layer could not be moved """ - send_cmd("tv_LayerMove", position) + if folder_id is not None and is_tvp_version_below_12(): + log.warning( + "`folder_id` option is only available in TVPaint version 12 and above." + ) + + args = [position] + if not is_tvp_version_below_12(): + args.append(folder_id) + + send_cmd("tv_LayerMove", *args) @try_cmd( @@ -327,9 +348,24 @@ def tv_layer_select_info(full: bool = False) -> tuple[int, int]: return frame, count -def tv_layer_create(name: str) -> int: - """Create a new image layer with the given name.""" - return int(send_cmd("tv_LayerCreate", name, handle_string=False)) +def tv_layer_create(name: str, layer_type: int = 1) -> int: + """Create a new image layer with the given name. + + Args: + name: layer name + layer_type: 1 for normal layer, 0 for Folder layer (only available for TVPaint 12 and above) + + """ + if layer_type != 1 and is_tvp_version_below_12(): + log.warning( + "`layer_type` selection is only available in TVPaint version 12 and above." + ) + + args = [name] + if not is_tvp_version_below_12(): + args.append(str(layer_type)) + + return int(send_cmd("tv_LayerCreate", *args, handle_string=False)) def tv_layer_duplicate(name: str) -> int: @@ -363,6 +399,21 @@ def tv_layer_kill(layer_id: int) -> None: send_cmd("tv_LayerKill", layer_id) +@min_version_compatible(min_version="12") +@try_cmd( + raise_exc=NoObjectWithIdError, + exception_msg="Invalid layer id", +) +def tv_layer_folder_delete(layer_id: int, remove_children: bool) -> None: + """Delete the layer with provided id. + + Raises: + NoObjectWithIdError: if given an invalid layer id + NotImplemented: if used in tvpaint version inferior to 12 + """ + send_cmd("tv_LayerFolderDelete", layer_id, int(not remove_children)) + + def tv_layer_density_get() -> int: """Get the current layer density (opacity).""" return int(send_cmd("tv_LayerDensity")) @@ -519,10 +570,8 @@ def tv_layer_stencil_set(layer_id: int, mode: StencilMode) -> None: Raises: NoObjectWithIdError: if given an invalid layer id """ - if mode == StencilMode.OFF: - args = ["off"] - elif mode == StencilMode.ON: - args = ["on"] + if mode in [StencilMode.ON, StencilMode.OFF]: + args = [mode.value] else: args = ["on", mode.value] @@ -1235,3 +1284,109 @@ def tv_load_image(img_path: Path | str, stretch: bool = False) -> None: def tv_clear(fill_b_pen: bool = False) -> None: """Clear (or fill with BPen) the current image (selection) of the current layer.""" send_cmd("tv_Clear", int(fill_b_pen)) + + +@min_version_compatible(min_version="12") +def tv_layer_is_ctg_source() -> list[int]: + """Returns list of CTG layers (ids) that use the current layer as a source""" + res = send_cmd("tv_LayerIsCTGSource", error_values=[GrgErrorValue.EMPTY]) + with contextlib.suppress(Exception): + layer_ids = [int(layer_id) for layer_id in res.split()] + if any([layer_id < 0 for layer_id in layer_ids]): + return [] + else: + return layer_ids + + return [] + + +@min_version_compatible(min_version="12") +def tv_ctg_layer_create(name: str, sources: list[str] | None = None) -> int: + """Create a new CTG layer with the given name. + + Args: + name: layer name + sources: list of source layer ids + + Returns: + layer_id: new layer id + """ + sources = sources or [] + return int(send_cmd("tv_CTGLayerCreate", name, *sources)) + + +@min_version_compatible(min_version="12") +def tv_ctg_load_structure(ctg_layer_id: int) -> None: + """Create a new CTG layer with the given name. + + Args: + ctg_layer_id: ctg layer id + + Raises: + ValueError: if Invalid layer id + """ + res = send_cmd("tv_CTGLoadStructure ", ctg_layer_id) + with contextlib.suppress(Exception): + if int(res) < 0: + raise ValueError("Invalid layer id") + + +@min_version_compatible(min_version="12") +def tv_ctg_apply_changes(ctg_layer_id: int, apply: bool) -> bool: + """Set Apply Changes value on CTG layer + + Args: + ctg_layer_id: ctg layer id + apply: True to apply changes, False otherwise + + Returns: + bool: previous value + """ + res = send_cmd("tv_CTGApplyChanges", ctg_layer_id, "on" if apply else "off") + return tv_cast_to_type(res, bool) + + +@min_version_compatible(min_version="12") +def tv_ctg_squiggles_visible(ctg_layer_id: int, visible: bool) -> bool: + """Set squiggles visibility on CTG layer + + Args: + ctg_layer_id: ctg layer id + visible: True to make squiggles visible, False otherwise + + Returns: + bool: previous value + """ + res = send_cmd("tv_CTGSquigglesVisible", ctg_layer_id, "on" if visible else "off") + return tv_cast_to_type(res, bool) + + +@min_version_compatible(min_version="12") +def tv_ctg_get_source(ctg_layer_id: int) -> list[int]: + """Get a CTG layer's sources + + Args: + ctg_layer_id: ctg layer id + + Returns: + sources: list of sours layer Ids + + Warning: + this function is documented as `tv_CTGGetSources` but it is in fact `tv_CTGGetSource` without the `s` + """ + res = send_cmd("tv_CTGGetSource", ctg_layer_id) + return tv_cast_to_type(res, list[int]) + + +@min_version_compatible(min_version="12") +def tv_ctg_source_add(ctg_layer_id: int, source_ids: list[int]) -> None: + send_cmd("tv_CTGSource", "add", ctg_layer_id, *source_ids) + + +@min_version_compatible(min_version="12") +def tv_ctg_source_remove(ctg_layer_id: int, source_ids: list[int]) -> None: + send_cmd("tv_CTGSource", "remove", ctg_layer_id, *source_ids) + + + + diff --git a/pytvpaint/george/grg_project.py b/pytvpaint/george/grg_project.py index 35a97ce..a9d1c8c 100644 --- a/pytvpaint/george/grg_project.py +++ b/pytvpaint/george/grg_project.py @@ -5,10 +5,12 @@ from dataclasses import dataclass, field from enum import Enum from pathlib import Path -from typing import Any +from typing import Any, cast from pytvpaint.george.client import send_cmd, try_cmd from pytvpaint.george.client.parse import ( + DataclassInstance, + get_dataclass_fields, tv_cast_to_type, tv_parse_list, ) @@ -19,6 +21,7 @@ ResizeOption, RGBColor, TVPSound, + is_tvp_version_below_12, ) @@ -33,8 +36,8 @@ class TVPProject: height: int pixel_aspect_ratio: float frame_rate: float - field_order: FieldOrder - start_frame: int + field_order: FieldOrder = FieldOrder.NONE + start_frame: int = 0 class BackgroundMode(Enum): @@ -51,6 +54,33 @@ class BackgroundMode(Enum): NONE = "none" +@try_cmd( + raise_exc=NoObjectWithIdError, + exception_msg="Invalid project id", +) +def tv_project_info(project_id: str) -> TVPProject: + """Get info of the given project. + + Raises: + NoObjectWithIdError: if given an invalid project id + """ + result = send_cmd("tv_ProjectInfo", project_id, error_values=[GrgErrorValue.EMPTY]) + + if is_tvp_version_below_12(): + fields = TVPProject + else: + # values of field_order have been removed in versions > 12 so for now we provide it ourselves + fields = get_dataclass_fields(cast(DataclassInstance, TVPProject)) + fields_keys = list(dict(fields).keys()) + field_order_index, start_frame_index = fields_keys.index('field_order'), fields_keys.index('start_frame') + fields[field_order_index], fields[start_frame_index] = fields[start_frame_index], fields[field_order_index] + result = f"{result} {tv_get_field().value}" + + project = tv_parse_list(result, with_fields=fields) + project["id"] = project_id + return TVPProject(**project) + + def tv_background_get() -> ( tuple[BackgroundMode, tuple[RGBColor, RGBColor] | RGBColor | None] ): @@ -183,22 +213,6 @@ def tv_project_current_id() -> str: return send_cmd("tv_ProjectCurrentId") -@try_cmd( - raise_exc=NoObjectWithIdError, - exception_msg="Invalid project id", -) -def tv_project_info(project_id: str) -> TVPProject: - """Get info of the given project. - - Raises: - NoObjectWithIdError: if given an invalid project id - """ - result = send_cmd("tv_ProjectInfo", project_id, error_values=[GrgErrorValue.EMPTY]) - project = tv_parse_list(result, with_fields=TVPProject) - project["id"] = project_id - return TVPProject(**project) - - def tv_get_project_name() -> str: """Returns the save path of the current project.""" return send_cmd("tv_GetProjectName") diff --git a/pytvpaint/george/grg_scene.py b/pytvpaint/george/grg_scene.py index 13f1f45..50bc47f 100644 --- a/pytvpaint/george/grg_scene.py +++ b/pytvpaint/george/grg_scene.py @@ -3,7 +3,8 @@ from __future__ import annotations from pytvpaint.george.client import send_cmd, try_cmd -from pytvpaint.george.grg_base import GrgErrorValue +from pytvpaint.george.client.parse import tv_cast_to_type +from pytvpaint.george.grg_base import GrgErrorValue, min_version_compatible @try_cmd(exception_msg="No scene at provided position") @@ -39,3 +40,32 @@ def tv_scene_duplicate(scene_id: int) -> None: def tv_scene_close(scene_id: int) -> None: """Remove the given scene.""" send_cmd("tv_SceneClose", scene_id) + + +@min_version_compatible(min_version="12") +def tv_scene_create(clips: list[str]) -> int: + """Create a new scene (with a list of new clips provided) after the current scene. + + Args: + clips: list of clip names to create alongside new scene + + Returns: + scene_id: new scene id + + """ + return int(send_cmd("tv_SceneCreate", *clips)) + + +@min_version_compatible(min_version="12") +def tv_scene_split(scene_id: int) -> list[int]: + """Splits each clips in the scene into its own scene. + + Args: + scene_id: scene if + + Returns: + clip_ids: list of clip ids in the split scene + + """ + return tv_cast_to_type(send_cmd("tv_SceneSplit", scene_id), list[int]) + diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index f762cf2..006a7e8 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -5,7 +5,7 @@ from collections.abc import Iterator from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from uuid import uuid4 from fileseq.filesequence import FileSequence @@ -21,11 +21,28 @@ ) if TYPE_CHECKING: + from pytvpaint.camera import Camera from pytvpaint.clip import Clip from pytvpaint.project import Project from pytvpaint.scene import Scene +# FIXME folder layer has no way of knowing which layers are it's children +# FIXME child layers have no way of knowing if they are in a folder layer or which one +# FIXME CameraLayer is not a layer and most layer functions will ignore, prefer use of Camera object instead +# FIXME CTG layer sometimes takes a while to update in the UI, don't know if incident is isolated to me or generalised +# FIXME tv_CTGGetSources doesn't seem to work, maybe some missing/undocumented attributes +# FIXME tv_CTGGetSources is actually misspelled and is actually tv_CTGGetSource without the `s` at the end +# FIXME tv_CTGGetSources actually requires and returns layer Ids not names +# FIXME creating a CTG layer from a folder crashes TVPaint +# FIXME moving a layer that is already in a folder in the same folder crashes TVPaint (check again) +# FIXME tv_layer_move position is relative to root and not folder, this is not ideal +# FIXME not providing a FolderID to tv_LayerMove doesn't move the layer to the root, you just need to move outside the +# folder range for it to work +# FIXME moving a layer inside a folder can be done without providing a FolderID, just by moving the layer in the folder's range +# FIXME not knowing whether a layer is in a folder or not is not great, same for folder not knowing it's children + + @dataclass class LayerInstance: """A layer instance is a frame where there is a drawing. It only has a start frame. @@ -329,15 +346,20 @@ def select_layers(self, select: bool) -> None: class Layer(Removable): - """A Layer is inside a clip and contains drawings.""" + """A Layer is parented to a clip and contains LayerInstances.""" - def __init__(self, layer_id: int, clip: Clip | None = None) -> None: + def __init__( + self, + layer_id: int, + clip: Clip | None = None, + data: george.TVPLayer | None = None, + ) -> None: from pytvpaint.clip import Clip super().__init__() self._id = layer_id self._clip = clip or Clip.current_clip() - self._data = george.tv_layer_info(self.id) + self._data = data or george.tv_layer_info(self.id) def refresh(self) -> None: """Refreshes the layer data.""" @@ -352,13 +374,15 @@ def refresh(self) -> None: def __repr__(self) -> str: """The string representation of the layer.""" - return f"Layer({self.name})" + return f"{self.__class__.__name__}({self.name})" def __eq__(self, other: object) -> bool: """Two layers are equal if their id is the same.""" if not isinstance(other, Layer): return NotImplemented - return self.id == other.id + + is_same_type = self.layer_type == other.layer_type + return is_same_type and self.id == other.id @property def id(self) -> int: @@ -412,6 +436,35 @@ def position(self, value: int) -> None: self.make_current() george.tv_layer_move(value) + @george.min_version_compatible(min_version="12") + def set_folder_position(self, folder: LayerFolder, position: int) -> None: + """Moves the layer to the provided position in the folder. + + Note: + the position is relative to the root of the layer stack and not the folder. + If the position is larger than that of the last layer in the folder, then the layer will be placed last. + If you want to move a layer outside it's folder then just set its position using the layer.position property + and move it outside the range of the folder, meaning before the first or after the last layer in the folder. + """ + value = max(0, position) + # TVPaint will always set the position at (value - 1) if value is superior to 0, so we need to add +1 in + # that case to set the position correctly, I don't know why it works this way, but it honestly makes no sense + if value != 0: + value += 1 + + self.make_current() + george.tv_layer_move(value, folder.id) + + @property + @george.min_version_compatible(min_version="12") + def folder(self) -> None: + raise NotImplementedError("There is currently no way to get the parent folder from a Layer.") + + @folder.setter + @george.min_version_compatible(min_version="12") + def folder(self, folder: LayerFolder): + george.tv_layer_move(self.position, folder.id) + @refreshed_property def name(self) -> str: """The layer name.""" @@ -633,6 +686,37 @@ def is_anim_layer(self) -> bool: """Returns True if the layer is an animation layer.""" return self.layer_type == george.LayerType.SEQUENCE + @property + @george.min_version_compatible(min_version="12") + def is_ctg_layer(self) -> bool: + """Returns True if the layer is a CTG layer.""" + return self.layer_type == george.LayerType.SCRIBBLES + + @property + @george.min_version_compatible(min_version="12") + @set_as_current + def is_ctg_source(self) -> bool: + """Returns True if the layer is a CTG layer source.""" + return bool(george.tv_layer_is_ctg_source()) + + @property + @george.min_version_compatible(min_version="12") + @set_as_current + def sourced_ctg_layers(self) -> list[CTGLayer]: + """Returns a list of CTGLayer instances that use this layer as a source.""" + if not self.is_ctg_source: + return [] + + ctg_layers = [] + for layer_id in george.tv_layer_is_ctg_source(): + ctg_layer = self.clip.get_layer(by_id=layer_id) + if not ctg_layer: + continue + + ctg_layers.append(ctg_layer) + + return cast(list[CTGLayer], ctg_layers) + def load_dependencies(self) -> None: """Load all dependencies of the layer in memory.""" george.tv_layer_load_dependencies(self.id) @@ -701,9 +785,10 @@ def merge_all( """ george.tv_layer_merge_all(keep_color_grp, keep_img_mark, keep_instance_name) - @staticmethod + @classmethod @george.undoable def new( + cls, name: str, clip: Clip | None = None, color: LayerColor | None = None, @@ -728,10 +813,10 @@ def new( clip.make_current() name = utils.get_unique_name(clip.layer_names, name) - layer_id = george.tv_layer_create(name) - - layer = Layer(layer_id=layer_id, clip=clip) + layer_type = 1 if cls == Layer else 0 + layer_id = george.tv_layer_create(name, layer_type=layer_type) + layer = cls(layer_id=layer_id, clip=clip) if color: layer.color = color @@ -1246,3 +1331,161 @@ def rename_instances( process: the instance naming process """ george.tv_instance_name(self.id, mode, prefix, suffix, process) + + +class LayerFolder(Layer): + """A LayerFolder is parented to a clip and contains Layers.""" + + @george.min_version_compatible(min_version="12") + def __init__( + self, + layer_id: int, + clip: Clip | None = None, + data: george.TVPLayer | None = None, + ) -> None: + super().__init__(layer_id, clip, data) + + def remove(self, remove_children: bool = True) -> None: + """Remove the layer from the clip. + + Args: + remove_children: True will remove child layers in the folder, False will remove the folder and move the + layers to the root level. Default value is True. + + Warning: + The current instance won't be usable after this call since it will be mark removed. + """ + self.clip.make_current() + self.is_locked = False + george.tv_layer_folder_delete(self.id, remove_children) + self.mark_removed() + + +class CameraLayer(Layer): + """A CameraLayer is parented to a clip and is linked to the Camera object.""" + + @george.min_version_compatible(min_version="12") + def __init__( + self, + layer_id: int, + clip: Clip | None = None, + data: george.TVPLayer | None = None, + ) -> None: + super().__init__(layer_id, clip, data) + + @property + @george.min_version_compatible(min_version="12") + def camera(self) -> Camera | None: + """Returns the Camera object linked ot this layer, if no camera is linked to teh layer, returns None.""" + if self.layer_type != george.LayerType.CAMERA: + return None + return self.clip.camera + + +class CTGLayer(Layer): + """A CameraLayer is parented to a clip and is linked to the Camera object.""" + + @george.min_version_compatible(min_version="12") + def __init__( + self, + layer_id: int, + clip: Clip | None = None, + data: george.TVPLayer | None = None, + ) -> None: + super().__init__(layer_id, clip, data) + + @classmethod + @george.min_version_compatible(min_version="12") + @george.undoable + def new( + cls, + name: str, + clip: Clip | None = None, + color: LayerColor | None = None, + sources: list[Layer] | None = None, + ) -> CTGLayer: + """Create a new CTG layer. + + Args: + name: the name of the new layer + clip: the parent clip + color: the layer color + sources: the layer sources + + Returns: + CTGLayer: the new layer + + Note: + The layer name is checked against all other layers to have a unique name using `get_unique_name`. + This can take a while if you have a lot of layers. + """ + from pytvpaint.clip import Clip + + clip = clip or Clip.current_clip() + clip.make_current() + + name = utils.get_unique_name(clip.layer_names, name) + layer_id = george.tv_ctg_layer_create(name, sources=[layer.name for layer in sources]) + + layer = cls(layer_id=layer_id, clip=clip) + if color: + layer.color = color + + return layer + + @property + @set_as_current + def squiggles_visible(self) -> bool: + prev_value = george.tv_ctg_squiggles_visible(self.id, True) + # reset value + george.tv_ctg_squiggles_visible(self.id, prev_value) + return prev_value + + @squiggles_visible.setter + @set_as_current + def squiggles_visible(self, value: bool) -> None: + george.tv_ctg_squiggles_visible(self.id, value) + + @property + @set_as_current + def apply_changes(self) -> bool: + prev_value = george.tv_ctg_apply_changes(self.id, True) + # reset value + george.tv_ctg_apply_changes(self.id, prev_value) + return prev_value + + @apply_changes.setter + @set_as_current + def apply_changes(self, value: bool) -> None: + george.tv_ctg_apply_changes(self.id, value) + + @property + def sources(self) -> list[Layer]: + sources = [] + + # TODO commenting this since it was used when tv_CTGGetSources "wasn't" working + # for layer in self.clip.layers: + # if not layer.is_ctg_source: + # continue + # if self.id not in [ctg_layer.id for ctg_layer in layer.sourced_ctg_layers]: + # continue + # sources.append(layer) + + for layer_id in george.tv_ctg_get_source(self.id): + layer = self.clip.get_layer(by_id=layer_id) + if not layer: + continue + sources.append(layer) + return sources + + def add_sources(self, sources: list[Layer]) -> None: + george.tv_ctg_source_add(self.id, [layer.id for layer in sources if self not in layer.sourced_ctg_layers]) + + def remove_sources(self, sources: list[Layer]) -> None: + george.tv_ctg_source_remove(self.id, [layer.id for layer in sources if self in layer.sourced_ctg_layers]) + + @set_as_current + def load_structure(self) -> None: + george.tv_ctg_load_structure(self.id) + + diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 709ae32..7bad2b5 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -23,10 +23,14 @@ from pytvpaint.scene import Scene +# FIXME george.tv_project_info() values of field_order have been removed in versions > 12 so for now we provide it ourselves + + class Project(Refreshable, Renderable): - """A TVPaint project is the highest object that contains everything in the data hierarchy. + """A TVPaint project is the highest/root object that contains everything in the data hierarchy. - It looks like this: Project -> Scene -> Clip -> Layer -> LayerInstance + The Project structure can be split like so : Project -> Scene -> Clip -> Layer -> LayerInstance + Or since TVPaint 12, like so : Project -> Scene -> Clip -> LayerFolder -> Layer -> LayerInstance """ def __init__(self, project_id: str) -> None: @@ -46,7 +50,7 @@ def __eq__(self, other: object) -> bool: return self.id == other.id def refresh(self) -> None: - """Refreshed the project data. + """Refreshes the project data. Raises: ValueError: if project has been closed diff --git a/pytvpaint/scene.py b/pytvpaint/scene.py index 72b924a..f344212 100644 --- a/pytvpaint/scene.py +++ b/pytvpaint/scene.py @@ -12,9 +12,11 @@ set_as_current, ) +# FIXME tv_scene_create doesn't seem to work + class Scene(Removable): - """A Scene is a collection of clips. A Scene is inside a project.""" + """A Scene is a collection of clips. A Scene is parented to a project.""" def __init__(self, scene_id: int, project: Project) -> None: super().__init__() @@ -45,12 +47,42 @@ def current_scene() -> Scene: ) @classmethod - def new(cls, project: Project | None = None) -> Scene: - """Creates a new scene in the provided project.""" + def new(cls, project: Project | None = None, clips: list[str] | None = None) -> Scene: + """Creates a new scene in the provided project. + + Args: + project: parent project + clips: list of clip names to create alongside new scene + + Returns: + new scene instance + """ project = project or Project.current_project() project.make_current() + + # TODO commenting this for now until george.tv_scene_create is fixed by TVP devs + # if not clips or george.is_tvp_version_below_12(): + # george.tv_scene_new() + # new_scene = cls.current_scene() + # + # if clips and george.is_tvp_version_below_12(): + # for clip_name in clips: + # new_scene.add_clip(clip_name) + # + # return new_scene + # + # # if here, then we are using tvp 12 or superior + # scene_id = george.tv_scene_create(clips) + # return project.get_scene(by_id=scene_id) + george.tv_scene_new() - return cls.current_scene() + new_scene = cls.current_scene() + + if clips and george.is_tvp_version_below_12(): + for clip_name in clips: + new_scene.add_clip(clip_name) + + return new_scene def make_current(self) -> None: """Make this scene the current one.""" @@ -133,6 +165,21 @@ def duplicate(self) -> Scene: dup_id = george.tv_scene_enum_id(dup_pos) return Scene(dup_id, self.project) + @george.min_version_compatible(min_version="12") + def split(self) -> list[Scene]: + """Duplicate the scene and return it.""" + self.project.make_current() + clip_ids = george.tv_scene_split(self.id) + + new_scenes = [] + for clip_id in clip_ids: + clip = self.project.get_clip(by_id=clip_id) + if not clip: + continue + + new_scenes.append(clip.scene) + return new_scenes + def remove(self) -> None: """Remove the scene and all the clips inside. diff --git a/tests/conftest.py b/tests/conftest.py index 903112f..4161555 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,6 +54,30 @@ FixtureYield = Generator[T, None, None] +def _fix_tvp_12_selection() -> None: + from packaging import version + + _, tvp_version, _ = george.tv_version() + current_version = version.parse(tvp_version) + + # if tvp_version >= 12 and current_layer is Camera, then select first real layer instead: + if current_version.major >= 12: + current_clip = Project.current_project().current_clip + current_layer = current_clip.current_layer + if current_layer.layer_type == george.LayerType.CAMERA: + # switch to first layer in real/non-camera layers + first_layer = None + for layer in current_clip.layers: + if layer.layer_type == george.LayerType.CAMERA: + continue + + first_layer = layer + break + + if first_layer: + first_layer.make_current() + + @pytest.fixture(scope="function") def pen_brush_reset() -> FixtureYield[None]: """Resets the pen brush after the test""" @@ -68,13 +92,18 @@ def test_project(tmp_path: Path) -> FixtureYield[TVPProject]: Useful when you want to isolate a test """ project_id = tv_project_new(tmp_path / "project.tvpp") + + _fix_tvp_12_selection() + yield tv_project_info(project_id) tv_project_close(project_id) @pytest.fixture def test_project_obj(test_project: TVPProject) -> FixtureYield[Project]: - yield Project(test_project.id) + p = Project(test_project.id) + _fix_tvp_12_selection() + yield p @pytest.fixture @@ -299,7 +328,26 @@ def create_some_layers( layer_id = tv_layer_get_id(i + 1) layers.append(Layer(layer_id, test_clip_obj)) - # Remove the default clip + # Remove the default layer + tv_layer_kill(tv_layer_get_id(0)) + + yield layers + + +@pytest.fixture +def create_some_layer_folders( + test_project_obj: Project, + test_clip_obj: Clip, +) -> FixtureYield[list[Layer]]: + """Create some layers in a test project/scene and yields them""" + layers: list[Layer] = [] + + for i in range(5): + tv_layer_create(f"layer_{i}", layer_type=0) + layer_id = tv_layer_get_id(i + 1) + layers.append(Layer(layer_id, test_clip_obj)) + + # Remove the default layer tv_layer_kill(tv_layer_get_id(0)) yield layers @@ -342,3 +390,8 @@ def with_loaded_sequence( stretch=False, preload=True, ) + + +@pytest.fixture(scope="function", autouse=True) +def fix_tvp_12_selection() -> None: + _fix_tvp_12_selection() diff --git a/tests/george/client/test_init.py b/tests/george/client/test_init.py index fffb1f6..b382c3b 100644 --- a/tests/george/client/test_init.py +++ b/tests/george/client/test_init.py @@ -38,8 +38,8 @@ def test_run_grg_script(tmp_path: Path) -> None: tmp_script = tmp_path / "script.grg" tmp_img = tmp_path / "out.png" - with tmp_script.open("w") as script: - script.write(f'tv_SaveImage "{tmp_img}"') + tmp_script.write_text(f'tv_SaveImage "{tmp_img}"') + assert tmp_script.exists() send_cmd("tv_savemode", "png") run_script(tmp_script) diff --git a/tests/george/test_base.py b/tests/george/test_base.py deleted file mode 100644 index 3670b95..0000000 --- a/tests/george/test_base.py +++ /dev/null @@ -1,282 +0,0 @@ -from __future__ import annotations - -import pytest -from pytest_mock import MockFixture - -from pytvpaint.george import ( - AlphaMode, - AlphaSaveMode, - DrawingMode, - FileMode, - GeorgeError, - HSLColor, - MarkAction, - MarkReference, - MenuElement, - RectButton, - RGBColor, - SaveFormat, - TVPProject, - TVPShape, - tv_alpha_load_mode_get, - tv_alpha_load_mode_set, - tv_alpha_save_mode_get, - tv_alpha_save_mode_set, - tv_get_active_shape, - tv_list_request, - tv_mark_in_get, - tv_mark_in_set, - tv_mark_out_get, - tv_mark_out_set, - tv_menu_show, - tv_pen_brush_get, - tv_pen_brush_set, - tv_rect, - tv_rect_fill, - tv_req_angle, - tv_req_file, - tv_req_float, - tv_req_num, - tv_req_string, - tv_request, - tv_save_mode_get, - tv_save_mode_set, - tv_set_a_pen_hsl, - tv_set_a_pen_rgba, - tv_set_active_shape, - tv_undo, - tv_version, - undoable, -) -from pytvpaint.george.client import send_cmd -from pytvpaint.george.grg_layer import tv_layer_info - - -def test_tv_version() -> None: - from packaging import version as version_utils - - name, version, lang = tv_version() - min_version = version_utils.parse("1.0") - - assert "TVP Animation" in name - assert version_utils.parse(version) >= min_version - assert lang in ["en", "fr", "ja", "zh"] - - -@pytest.mark.skip("Will break the UI") -@pytest.mark.parametrize( - "cmd", - [ - MenuElement.CLIP, - MenuElement.PROJECT, - MenuElement.XSHEET, - MenuElement.NOTES, - ], -) -def test_menu_show_simple(cmd: MenuElement) -> None: - tv_menu_show(cmd) - - -@pytest.mark.skip("Will break the UI") -@pytest.mark.parametrize( - "cmd", - [ - MenuElement.SHOW_UI, - MenuElement.HIDE_UI, - MenuElement.CENTER_DISPLAY, - MenuElement.FIT_DISPLAY, - MenuElement.FRONT, - MenuElement.BACK, - ], -) -@pytest.mark.parametrize("current", [True, False]) -def test_menu_show_current(cmd: MenuElement, current: bool) -> None: - tv_menu_show(cmd, current=current) - - -@pytest.mark.skip("Will break the UI") -@pytest.mark.parametrize("args", [(0, 0, 50, 50), (-10, -50, 20, 20)]) -@pytest.mark.parametrize("current", [True, False]) -def test_menu_show_resize_ui(args: tuple[int], current: bool) -> None: - tv_menu_show(MenuElement.RESIZE_UI, *args, current=current) - - -@pytest.mark.skip("Will break the UI") -@pytest.mark.parametrize("arg", [0, 1]) -@pytest.mark.parametrize("current", [True, False]) -def test_menu_show_aspect_ratio(arg: int, current: bool) -> None: - tv_menu_show(MenuElement.ASPECT_RATIO, arg, current=current) - - -@pytest.mark.skip(reason="Will block TVPaint") -def test_tv_request() -> None: - tv_request("ksehfkjhkj", "ouiii", "NOOOOO") - - -@pytest.mark.skip(reason="Will block TVPaint") -def test_tv_req_num() -> None: - tv_req_num(15, 0, 100, title="Request an int") - - -@pytest.mark.skip(reason="Will block TVPaint") -def test_tv_req_angle(mocker: MockFixture) -> None: - tv_req_angle(15, 0, 100, title="Request an angle") - - -@pytest.mark.skip(reason="Will block TVPaint") -def test_tv_req_float(mocker: MockFixture) -> None: - tv_req_float(15.0, 0.0, 100.0, title="Request a float") - - -@pytest.mark.skip(reason="Will block TVPaint") -def test_tv_req_string() -> None: - tv_req_string("Request a string", "Hello this is text\nleo") - - -@pytest.mark.skip(reason="Needs user action") -def test_tv_list_request() -> None: - tv_list_request(["a/b/c|d"]) - - -@pytest.mark.skip(reason="Needs user action") -def test_tv_req_file() -> None: - tv_req_file(FileMode.LOAD, "Open requester", "C:/Users", "out.py", ".py") - - -@pytest.mark.skip("Doesn't work?") -def test_undo_command(test_project: TVPProject) -> None: - @undoable - def create_layer() -> int: - return int(send_cmd("tv_LayerCreate", "test_layer")) - - layer_id = create_layer() - assert layer_id - - tv_undo() - - # The layer doesn't exist anymore - with pytest.raises(GeorgeError): - tv_layer_info(layer_id) - - -def test_save_mode_get() -> None: - fmt, res = tv_save_mode_get() - assert len(fmt.value) > 2 - assert res - - -def test_tv_save_mode_set() -> None: - tv_save_mode_set(SaveFormat.BMP) - - -def test_tv_alpha_load_mode_get() -> None: - tv_alpha_load_mode_get() - - -@pytest.mark.parametrize("mode", AlphaMode) -def test_tv_alpha_load_mode_set(test_project: TVPProject, mode: AlphaMode) -> None: - tv_alpha_load_mode_set(mode) - assert tv_alpha_load_mode_get() == mode - - -def test_tv_alpha_save_mode_get() -> None: - tv_alpha_save_mode_get() - - -@pytest.mark.skip("It does not work for no reason...") -@pytest.mark.parametrize("mode", list(AlphaSaveMode)) -def test_tv_alpha_save_mode_set(test_project: TVPProject, mode: AlphaSaveMode) -> None: - tv_alpha_save_mode_set(mode) - assert tv_alpha_save_mode_get() == mode - - -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_in_get(ref: MarkReference) -> None: - tv_mark_in_get(ref) - - -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_in_set(test_project: TVPProject, ref: MarkReference) -> None: - tv_mark_in_set(ref, 20, MarkAction.SET) - assert tv_mark_in_get(ref) == (20, MarkAction.SET) - - -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_in_set_clear(test_project: TVPProject, ref: MarkReference) -> None: - tv_mark_in_set(ref, 20, MarkAction.SET) - assert tv_mark_in_get(ref) == (20, MarkAction.SET) - tv_mark_in_set(ref, 20, MarkAction.CLEAR) - assert tv_mark_in_get(ref) == (20, MarkAction.CLEAR) - - -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_out_get(ref: MarkReference) -> None: - tv_mark_out_get(ref) - - -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_out_set(test_project: TVPProject, ref: MarkReference) -> None: - tv_mark_out_set(ref, 20, MarkAction.SET) - assert tv_mark_out_get(ref) == (20, MarkAction.SET) - - -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_out_set_clear(test_project: TVPProject, ref: MarkReference) -> None: - tv_mark_out_set(ref, 20, MarkAction.SET) - assert tv_mark_out_get(ref) == (20, MarkAction.SET) - tv_mark_out_set(ref, 20, MarkAction.CLEAR) - assert tv_mark_out_get(ref) == (20, MarkAction.CLEAR) - - -def test_tv_get_active_shape() -> None: - assert tv_get_active_shape() in TVPShape - - -@pytest.mark.parametrize("shape", TVPShape) -def test_tv_set_active_shape(shape: TVPShape) -> None: - tv_set_active_shape(shape) - - -def test_tv_set_a_pen_rgba() -> None: - c = RGBColor(0, 255, 0) - tv_set_a_pen_rgba(c, alpha=40) - assert tv_set_a_pen_rgba(c, alpha=40) == c - - -def test_tv_set_a_pen_hsl() -> None: - c = HSLColor(50, 51, 100) - tv_set_a_pen_hsl(c) - result = tv_set_a_pen_hsl(c) - assert c.h - 1 <= result.h <= c.h + 1 - assert c.s - 1 <= result.s <= c.s + 1 - assert c.l - 1 <= result.l <= c.l + 1 - - -@pytest.mark.parametrize("tool_mode", [True, False]) -def test_tv_pen_brush_get(tool_mode: bool) -> None: - assert tv_pen_brush_get(tool_mode) - - -@pytest.mark.parametrize("mode", DrawingMode) -@pytest.mark.parametrize("tool_mode", [True, False]) -def test_tv_pen_brush_set( - pen_brush_reset: None, mode: DrawingMode, tool_mode: bool -) -> None: - tv_pen_brush_set(mode, tool_mode=tool_mode) - - -@pytest.mark.parametrize("button", [None, *RectButton]) -def test_tv_rect(test_project: TVPProject, button: RectButton | None) -> None: - width = test_project.width - height = test_project.height - tv_rect(0, 0, width, height) - - -@pytest.mark.parametrize("erase_mode", [True, False]) -@pytest.mark.parametrize("tool_mode", [True, False]) -def test_tv_rect_fill( - test_project: TVPProject, erase_mode: bool, tool_mode: bool -) -> None: - width = test_project.width - height = test_project.height - tv_rect_fill(0, 0, width, height, 0, width, erase_mode, tool_mode) diff --git a/tests/george/test_grg_base.py b/tests/george/test_grg_base.py index 1d6574a..1371544 100644 --- a/tests/george/test_grg_base.py +++ b/tests/george/test_grg_base.py @@ -1,6 +1,7 @@ from __future__ import annotations import pytest +from packaging import version from pytest_mock import MockFixture from pytvpaint.george.client import send_cmd @@ -55,13 +56,12 @@ def test_tv_version() -> None: - from packaging import version as version_utils + name, tvp_version, lang = tv_version() + min_version = version.parse("11.0") + current_version = version.parse(tvp_version) - name, version, lang = tv_version() - min_version = version_utils.parse("1.0") - - assert "TVP Animation" in name - assert version_utils.parse(version) >= min_version + assert "TVP Animation" in name or "TVPaint Animation" in name + assert current_version >= min_version assert lang in ["en", "fr", "ja", "zh"] diff --git a/tests/george/test_grg_camera.py b/tests/george/test_grg_camera.py index 4f42a04..ca0ca68 100644 --- a/tests/george/test_grg_camera.py +++ b/tests/george/test_grg_camera.py @@ -5,7 +5,7 @@ import pytest from pytvpaint.george.exceptions import GeorgeError -from pytvpaint.george.grg_base import FieldOrder +from pytvpaint.george.grg_base import FieldOrder, is_tvp_version_below_12 from pytvpaint.george.grg_camera import ( TVPCameraPoint, tv_camera_enum_points, @@ -23,8 +23,9 @@ def test_tv_camera_info_get(test_project: TVPProject) -> None: camera = tv_camera_info_get() assert camera.width == test_project.width assert camera.height == test_project.height - assert camera.frame_rate == test_project.frame_rate assert camera.pixel_aspect_ratio == test_project.pixel_aspect_ratio + if is_tvp_version_below_12(): + assert camera.frame_rate == test_project.frame_rate @pytest.mark.parametrize( diff --git a/tests/george/test_grg_clip.py b/tests/george/test_grg_clip.py index 3e97966..bb2427c 100644 --- a/tests/george/test_grg_clip.py +++ b/tests/george/test_grg_clip.py @@ -1,874 +1,874 @@ -from __future__ import annotations - -import itertools -import re -from collections.abc import Iterable, Iterator -from pathlib import Path -from typing import Any - -import pytest - -from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError -from pytvpaint.george.grg_base import ( - FieldOrder, - SaveFormat, - SpriteLayout, - tv_save_mode_get, -) -from pytvpaint.george.grg_clip import ( - PSDSaveMode, - TVPClip, - tv_bookmark_clear, - tv_bookmark_next, - tv_bookmark_prev, - tv_bookmark_set, - tv_bookmarks_enum, - tv_clip_action_get, - tv_clip_action_set, - tv_clip_close, - tv_clip_color_get, - tv_clip_color_set, - tv_clip_current_id, - tv_clip_dialog_get, - tv_clip_dialog_set, - tv_clip_duplicate, - tv_clip_enum_id, - tv_clip_hidden_get, - tv_clip_hidden_set, - tv_clip_info, - tv_clip_move, - tv_clip_name_get, - tv_clip_name_set, - tv_clip_new, - tv_clip_note_get, - tv_clip_note_set, - tv_clip_save_structure_csv, - tv_clip_save_structure_flix, - tv_clip_save_structure_json, - tv_clip_save_structure_psd, - tv_clip_save_structure_sprite, - tv_clip_select, - tv_clip_selection_get, - tv_clip_selection_set, - tv_first_image, - tv_last_image, - tv_layer_image, - tv_layer_image_get, - tv_load_sequence, - tv_save_clip, - tv_save_display, - tv_save_sequence, - tv_sound_clip_adjust, - tv_sound_clip_info, - tv_sound_clip_new, - tv_sound_clip_reload, - tv_sound_clip_remove, -) -from pytvpaint.george.grg_layer import ( - LayerType, - TVPLayer, - tv_instance_get_name, - tv_layer_current_id, - tv_layer_display_set, - tv_layer_get_id, - tv_layer_info, - tv_layer_kill, - tv_layer_rename, - tv_layer_set, -) -from pytvpaint.george.grg_project import TVPProject, tv_save_project -from pytvpaint.george.grg_scene import tv_scene_current_id, tv_scene_new -from tests.conftest import FixtureYield, test_scene - - -def test_tv_clip_info(test_clip: TVPClip) -> None: - assert tv_clip_info(test_clip.id) - - -def test_tv_clip_info_wrong_id(test_clip: TVPClip) -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_info(-2) - - -def test_tv_clip_enum_id(test_scene: int) -> None: - clips: list[int] = [] - - for i in range(5): - tv_clip_new(f"clip_{i}") - clips.append(tv_clip_current_id()) - - for i, clip_id in enumerate(clips): - # We offset 1 because of default clip - assert tv_clip_enum_id(test_scene, i + 1) == clip_id - - -def test_tv_clip_enum_id_wrong_scene_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_enum_id(-2, 0) - - -@pytest.mark.parametrize("pos", [-1, 1, 50]) -def test_tv_clip_enum_id_wrong_clip_pos(test_scene: int, pos: int) -> None: - with pytest.raises(GeorgeError): - tv_clip_enum_id(test_scene, pos) - - -def test_tv_clip_current_id() -> None: - assert tv_clip_current_id() - - -@pytest.mark.parametrize("name", ["", "a", "0", "lfseflj0", "clip with spaces"]) -def test_tv_clip_new(test_scene: int, name: str) -> None: - tv_clip_new(name) - assert tv_clip_info(tv_clip_current_id()).name == name - - -def test_tv_clip_duplicate(test_scene: int, test_clip: TVPClip) -> None: - tv_clip_duplicate(test_clip.id) - dup_clip = tv_clip_info(tv_clip_current_id()) - assert dup_clip == test_clip - - -def test_tv_clip_close(test_clip: TVPClip) -> None: - tv_clip_close(test_clip.id) - with pytest.raises(NoObjectWithIdError): - tv_clip_info(test_clip.id) - - -def test_tv_clip_name_get(test_clip: TVPClip) -> None: - assert tv_clip_name_get(test_clip.id) == test_clip.name - - -def test_tv_clip_name_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_name_get(-1) - - -@pytest.mark.parametrize("name", ["_", "a", "0", "lfseflj0"]) -def test_tv_clip_name_set(test_clip: TVPClip, name: str) -> None: - tv_clip_name_set(test_clip.id, name) - assert tv_clip_info(test_clip.id).name == name - - -def test_tv_clip_name_set_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_name_get(-1) - - -# "Copy" the fixture to use it twice in the above test -# See: https://stackoverflow.com/questions/36100624/pytest-use-same-fixture-twice-in-one-function -destination_scene = test_scene - - -@pytest.mark.parametrize("new_pos", range(5)) -def test_tv_clip_move( - test_scene: int, test_clip: TVPClip, destination_scene: int, new_pos: int -) -> None: - for i in range(5): - tv_clip_new(f"other_clip_{i}") - - tv_clip_move(test_clip.id, destination_scene, new_pos) - - # Ensure that it's not in the first scene - with pytest.raises(GeorgeError): - tv_clip_enum_id(test_scene, 1) - - # Ensure that it's at the right position in the other scene - tv_clip_enum_id(destination_scene, new_pos) - - -def test_tv_clip_hidden_get(test_clip: TVPClip) -> None: - assert tv_clip_hidden_get(test_clip.id) == test_clip.is_hidden - - -def test_tv_clip_hidden_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_hidden_get(-1) - - -@pytest.mark.parametrize("hidden", [True, False]) -def test_tv_clip_hidden_set(test_clip: TVPClip, hidden: bool) -> None: - tv_clip_hidden_set(test_clip.id, hidden) - assert tv_clip_info(test_clip.id).is_hidden == hidden - - -@pytest.mark.parametrize("hidden", [True, False]) -def test_tv_clip_hidden_set_wrong_id(hidden: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_hidden_set(-1, hidden) - - -def test_tv_clip_select(test_scene: int, test_clip: TVPClip) -> None: - first_clip = tv_clip_enum_id(test_scene, 0) - tv_clip_select(first_clip) - - assert not tv_clip_info(test_clip.id).is_current - tv_clip_select(test_clip.id) - assert tv_clip_info(test_clip.id).is_current - - -def test_tv_clip_select_also_selects_scene( - test_project: TVPProject, - test_scene: int, - test_clip: TVPClip, -) -> None: - tv_scene_new() - assert tv_scene_current_id() != test_scene - tv_clip_select(test_clip.id) - assert tv_scene_current_id() == test_scene - - -def test_tv_clip_selection_get(test_clip: TVPClip) -> None: - assert tv_clip_selection_get(test_clip.id) == test_clip.is_selected - - -def test_tv_clip_selection_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_selection_get(-1) - - -@pytest.mark.parametrize("select", [True, False]) -def test_tv_clip_selection_set(test_clip: TVPClip, select: bool) -> None: - tv_clip_selection_set(test_clip.id, select) - assert tv_clip_info(test_clip.id).is_selected == select - - -@pytest.mark.parametrize("select", [True, False]) -def test_tv_clip_selection_set_wrong_id(test_clip: TVPClip, select: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_selection_set(-1, select) - - -def test_tv_first_image(test_clip: TVPClip) -> None: - assert tv_first_image() == test_clip.first_frame - - -def test_tv_last_image(test_clip: TVPClip) -> None: - assert tv_last_image() == test_clip.last_frame - - -@pytest.mark.parametrize("offset_count", [None, *itertools.product([0, 1], [0, 1])]) -@pytest.mark.parametrize("field_order", [None, *FieldOrder]) -@pytest.mark.parametrize("stretch", [False, True]) -@pytest.mark.parametrize("time_stretch", [False, True]) -@pytest.mark.parametrize("preload", [False, True]) -def test_tv_load_sequence( - ppm_sequence: list[Path], - test_clip: TVPClip, - offset_count: tuple[int, int] | None, - field_order: FieldOrder | None, - stretch: bool, - time_stretch: bool, - preload: bool, -) -> None: - total_images = len(ppm_sequence) - first_image = ppm_sequence[0] - - images_loaded = tv_load_sequence( - first_image, - offset_count, - field_order, - stretch, - time_stretch, - preload, - ) - - if offset_count: - offset, count = offset_count - should_load = total_images if count <= 0 else min(count, total_images - offset) - else: - should_load = total_images - - assert should_load == images_loaded - - # Find the extension at the end (including file number) - ext_match = re.search(r"\d+\.[a-z0-9]+$", str(first_image)) - assert ext_match - ext_start, _ = ext_match.span() - - layer_name_cut = str(first_image)[:ext_start] - - layer = tv_layer_info(tv_layer_current_id()) - - assert layer.name == layer_name_cut - assert layer.type == LayerType.SEQUENCE - assert layer.first_frame == 0 - assert layer.last_frame == should_load - 1 - - -def test_tv_load_sequence_sequence_does_not_exist(tmp_path: Path) -> None: - with pytest.raises(FileNotFoundError, match="File not found"): - tv_load_sequence(tmp_path / "file.001.png") - - -@pytest.fixture -def bookmarks(test_clip: TVPClip) -> FixtureYield[Iterable[int]]: - n_bookmarks = 5 - for frame in range(n_bookmarks): - tv_bookmark_set(frame) - yield range(n_bookmarks) - - -def test_tv_bookmarks_enum(bookmarks: Iterable[int]) -> None: - for pos, mark in enumerate(bookmarks): - assert tv_bookmarks_enum(pos) == mark - - -@pytest.mark.parametrize("frame", range(5)) -def test_tv_bookmark_set(test_clip: TVPClip, frame: int) -> None: - tv_bookmark_set(frame) - assert tv_bookmarks_enum(0) == frame - - -@pytest.mark.parametrize("frame", range(5)) -def test_tv_bookmark_clear(test_clip: TVPClip, frame: int) -> None: - tv_bookmark_set(frame) - assert tv_bookmarks_enum(0) == frame - - tv_bookmark_clear(frame) - - with pytest.raises(GeorgeError, match="No bookmark"): - tv_bookmarks_enum(0) - - -@pytest.mark.parametrize("frame", range(5)) -def test_tv_bookmark_next(test_clip: TVPClip, frame: int) -> None: - tv_bookmark_set(frame) - tv_bookmark_next() - assert tv_layer_image_get() == frame - - -@pytest.mark.parametrize("frame", range(5)) -def test_tv_bookmark_prev(test_clip: TVPClip, frame: int) -> None: - # Set the current image far away - tv_layer_image(50) - - tv_bookmark_set(frame) - tv_bookmark_prev() - - assert tv_layer_image_get() == frame - - -def test_tv_clip_color_get(test_clip: TVPClip) -> None: - assert tv_clip_color_get(test_clip.id) in range(27) - - -@pytest.mark.parametrize("color_index", range(27)) -def test_tv_clip_color_set(test_clip: TVPClip, color_index: int) -> None: - tv_clip_color_set(test_clip.id, color_index) - assert tv_clip_color_get(test_clip.id) == color_index - - -def test_tv_clip_action_get(test_clip: TVPClip) -> None: - assert tv_clip_action_get(test_clip.id) == "" - - -def test_tv_clip_action_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_action_get(-3) - - -# Note: we removed the \n test because there's a bug with TVPaint that handle control characters -TEST_TEXTS = ["", "l", "0", "ab", "a0l", "ap*"] # "a\nb"] - - -@pytest.mark.parametrize("text", TEST_TEXTS) -def test_tv_clip_action_set(test_clip: TVPClip, text: str) -> None: - tv_clip_action_set(test_clip.id, text) - assert tv_clip_action_get(test_clip.id) == text - - -def test_tv_clip_action_set_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_action_set(-2, "test") - - -def test_tv_clip_dialog_get(test_clip: TVPClip) -> None: - assert tv_clip_dialog_get(test_clip.id) == "" - - -def test_tv_clip_dialog_get_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_dialog_get(-2) - - -@pytest.mark.parametrize("text", TEST_TEXTS) -def test_tv_clip_dialog_set(test_clip: TVPClip, text: str) -> None: - tv_clip_dialog_set(test_clip.id, text) - assert tv_clip_dialog_get(test_clip.id) == text - - -def test_tv_clip_dialog_set_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_dialog_set(-2, "test") - - -def test_tv_clip_note_get(test_clip: TVPClip) -> None: - assert tv_clip_note_get(test_clip.id) == "" - - -def test_tv_clip_note_get_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_dialog_get(-2) - - -@pytest.mark.parametrize("text", TEST_TEXTS) -def test_tv_clip_note_set(test_clip: TVPClip, text: str) -> None: - tv_clip_note_set(test_clip.id, text) - assert tv_clip_note_get(test_clip.id) == text - - -def test_tv_clip_note_set_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_note_set(-2, "test") - - -def test_tv_save_clip(tmp_path: Path) -> None: - clip_tvp = tmp_path / "clip.tvp" - tv_save_clip(clip_tvp) - assert clip_tvp.exists() - - -@pytest.mark.skip("Will block the UI") -def test_tv_save_clip_folder_does_not_exist(tmp_path: Path) -> None: - with pytest.raises(GeorgeError, match="Can't create file"): - tv_save_clip(tmp_path / "folder" / "out.tvpx") - - -def test_tv_save_display(tmp_path: Path) -> None: - save_ext, _ = tv_save_mode_get() - ext = "jpg" if save_ext == SaveFormat.JPG else save_ext.value - out_display = (tmp_path / "out").with_suffix("." + ext) - tv_save_display(out_display) - assert out_display.exists() - - -def current_clip_layers() -> Iterator[TVPLayer]: - """Iterates through the current clip layers""" - pos = 0 - while True: - try: - lid = tv_layer_get_id(pos) - yield tv_layer_info(lid) - except GeorgeError: - break - pos += 1 - - -def get_instance_frames() -> Iterator[tuple[int, str]]: - """Iterates through the instances of the current layer""" - layer = tv_layer_current_id() - frame = 0 - while True: - try: - name = tv_instance_get_name(layer, frame) - yield frame, name - except NoObjectWithIdError: - break - frame += 1 - - -def apply_folder_pattern(initial_pattern: str | None, layer: TVPLayer) -> str: - # This is the default folder pattern - if initial_pattern is None: - return f"[{layer.position:03d}] {layer.name}" - - patterns = { - r"%li": str(layer.position), - r"%ln": layer.name, - r"%fi": r"%fi", # Couldn't make it work - } - - for pattern, rep in patterns.items(): - initial_pattern = initial_pattern.replace(pattern, rep) - - return initial_pattern - - -def apply_file_pattern( - initial_pattern: str | None, - layer: TVPLayer, - image_index: int, - image_name: str, - file_index: int, -) -> str: - # This is the default file pattern - if initial_pattern is None: - return f"[{image_index:03d}] {layer.name}" - - # When the instance name is empty it takes the image index - if image_name == "": - image_name = str(image_index) - - patterns = { - r"%li": str(layer.position), - r"%ln": layer.name, - r"%ii": str(image_index), - r"%in": image_name, - r"%fi": str(file_index), - } - - for pattern, rep in patterns.items(): - initial_pattern = initial_pattern.replace(pattern, rep) - - return initial_pattern - - -def load_sequence_with_name(first_frame: Path, name: str, count: int) -> int: - """Load an image sequence and rename the layer""" - tv_load_sequence(first_frame, offset_count=(0, count)) - layer = tv_layer_current_id() - tv_layer_rename(layer, name) - return layer - - -@pytest.mark.parametrize( - "mark_in, mark_out", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)] -) -def test_tv_save_sequence( - test_project: TVPProject, - tmp_path: Path, - ppm_sequence: list[Path], - mark_in: int | None, - mark_out: int | None, -) -> None: - tv_load_sequence(ppm_sequence[0]) - - save_ext, _ = tv_save_mode_get() - out_sequence = tmp_path / "out" - tv_save_sequence(out_sequence, mark_in, mark_out) - - clip = tv_clip_info(tv_clip_current_id()) - start, end = ( - (mark_in, mark_out) - if mark_in is not None and mark_out is not None - else (clip.first_frame, clip.last_frame) - ) - - for i in range(end - start): - image_name = f"{out_sequence.name}{i:05d}" - image_ext = "." + ("jpg" if save_ext == SaveFormat.JPG else save_ext.value) - image_path = out_sequence.with_name(f"{image_name}{image_ext}") - assert image_path.exists() - - -def test_tv_save_sequence_wrong_path(tmp_path: Path) -> None: - with pytest.raises(NotADirectoryError): - tv_save_sequence(tmp_path / "folder" / "out") - - -@pytest.mark.parametrize( - "file_format", - [ - SaveFormat.PNG, - # SaveFormat.JPG, - # SaveFormat.BMP, - # SaveFormat.TGA, - # SaveFormat.TIFF, - ], -) -@pytest.mark.parametrize("fill_background", [False, True]) -@pytest.mark.parametrize("folder_pattern", [r"folder_%li_%ln_%fi"]) -@pytest.mark.parametrize("file_pattern", [r"file_%li_%ln_%ii_%in_%fi"]) -@pytest.mark.parametrize("visible_layers_only", [False, True]) -@pytest.mark.parametrize("all_images", [False, True]) -def test_tv_clip_save_structure_json( - test_project: TVPProject, - ppm_sequence: list[Path], - tmp_path: Path, - file_format: SaveFormat, - fill_background: bool, - folder_pattern: str, - file_pattern: str, - visible_layers_only: bool, - all_images: bool, -) -> None: - # Import some frames - load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) - load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=3) - - # Hide that layer - seq_3 = load_sequence_with_name(ppm_sequence[0], name="sequence_3", count=3) - tv_layer_display_set(seq_3, False) - - # Remove the first layer (which is in the last position) - last_layer = list(current_clip_layers())[-1] - tv_layer_kill(last_layer.id) - - out_json_path = tmp_path / "clip.json" - tv_clip_save_structure_json( - out_json_path, - file_format, - fill_background, - folder_pattern, - file_pattern, - visible_layers_only, - all_images, - ) - assert out_json_path.exists() - - for layer in current_clip_layers(): - if visible_layers_only and not layer.visibility: - continue - - # Check that the layer folders exist - layer_folder_name = apply_folder_pattern(folder_pattern, layer) - layer_folder = tmp_path / layer_folder_name - assert layer_folder.exists() - - tv_layer_set(layer.id) - for i, instance in enumerate(get_instance_frames()): - frame, name = instance - - image_name = apply_file_pattern( - file_pattern, - layer, - image_index=frame + 1, - image_name=name, - file_index=i, - ) - - # Check that the images exist - ext = "." + file_format.value - image_path = (layer_folder / image_name).with_suffix(ext) - assert image_path.exists() - - -def test_tv_clip_save_structure_json_file_doesnt_exist(tmp_path: Path) -> None: - with pytest.raises(ValueError, match="destination folder doesn't exist"): - tv_clip_save_structure_json(tmp_path / "folder" / "out.json", SaveFormat.PNG) - - -@pytest.mark.parametrize( - "test_case", - [ - (PSDSaveMode.ALL, {}), - (PSDSaveMode.IMAGE, {"image": 0}), - (PSDSaveMode.MARKIN, {"mark_in": 0, "mark_out": 5}), - ], -) -def test_tv_clip_save_structure_psd( - test_project: TVPProject, - ppm_sequence: list[Path], - tmp_path: Path, - test_case: tuple[PSDSaveMode, dict[str, Any]], -) -> None: - out_psd = tmp_path / "out.psd" - - load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) - load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=2) - - mode, args = test_case - tv_clip_save_structure_psd(out_psd, mode, **args) - - if mode == PSDSaveMode.MARKIN: - # It's a sequence of numbered PSD files - for i, _ in enumerate(get_instance_frames()): - out_psd_frame = out_psd.with_stem(f"{out_psd.stem}{i:05d}") - assert out_psd_frame.exists() - else: - # It's a single PSD file - assert out_psd.exists() - - -def test_tv_tv_clip_save_structure_psd_file_does_not_exist(tmp_path: Path) -> None: - with pytest.raises(ValueError, match="destination folder doesn't exist"): - tv_clip_save_structure_psd(tmp_path / "folder" / "out.psd", PSDSaveMode.ALL) - - -@pytest.mark.parametrize("all_images", [None, False, True]) -@pytest.mark.parametrize("exposure_label", [None, "", "expo", "ex po"]) -def test_tv_clip_save_structure_csv( - test_project: TVPProject, - ppm_sequence: list[Path], - tmp_path: Path, - all_images: bool | None, - exposure_label: str | None, -) -> None: - out_csv = tmp_path / "out.csv" - - load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) - load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=2) - - # Remove the first layer (which is in the last position) - last_layer = list(current_clip_layers())[-1] - tv_layer_kill(last_layer.id) - - tv_clip_save_structure_csv(out_csv, all_images, exposure_label) - - assert out_csv.exists() - - out_layers_folder = out_csv.with_suffix(".layers") - assert out_layers_folder.exists() - - for i, layer in enumerate(current_clip_layers()): - layer_index = f"{(i + 1):03d}" - layer_folder_name = f"[{layer_index}] {layer.name}" - layer_folder = out_layers_folder / layer_folder_name - assert layer_folder.exists() - - tv_layer_set(layer.id) - for j, _ in enumerate(get_instance_frames()): - image_name = f"[{layer_index}][{(j + 1):05d}] {layer.name}.png" - assert (layer_folder / image_name).exists() - - -@pytest.mark.parametrize("layout", [None, *SpriteLayout]) -@pytest.mark.parametrize("space", [None, 0, 5, 50]) -def test_tv_clip_save_structure_sprite( - test_project: TVPProject, - ppm_sequence: list[Path], - tmp_path: Path, - layout: SpriteLayout | None, - space: int | None, -) -> None: - load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) - - out_sprite = tmp_path / "out.png" - tv_clip_save_structure_sprite(out_sprite, layout, space) - - assert out_sprite.exists() - - -@pytest.mark.parametrize("mark_in_out", [None, (0, 0), (0, 5), (2, 5)]) -def test_tv_clip_save_structure_flix( - test_project: TVPProject, - ppm_sequence: list[Path], - tmp_path: Path, - mark_in_out: tuple[int, int] | 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) - - # Export to flix needs to save the project first - tv_save_project(test_project.path) - - mark_in: int | None - mark_out: int | None - - if mark_in_out: - mark_in, mark_out = mark_in_out - else: - mark_in = mark_out = None - - out_flix = tmp_path / "out.xml" - - tv_clip_save_structure_flix(out_flix, mark_in, mark_out, send=False) - assert out_flix.exists() - - -def test_tv_sound_clip_info(test_clip: TVPClip, wav_file: Path) -> None: - tv_sound_clip_new(wav_file) - sound = tv_sound_clip_info(test_clip.id, 0) - - assert sound.sound_in == 0 - assert Path(sound.path) == wav_file - - -def test_tv_sound_clip_info_wrong_clip_id() -> None: - with pytest.raises(GeorgeError): - tv_sound_clip_info(-2, 0) - - -def test_tv_sound_clip_info_wrong_sound_track_pos(test_clip: TVPClip) -> None: - with pytest.raises(GeorgeError): - tv_sound_clip_info(test_clip.id, 1) - - -def test_tv_sound_clip_new( - test_project: TVPProject, - test_clip: TVPClip, - wav_file: Path, -) -> None: - for i in range(5): - tv_sound_clip_new(wav_file) - assert tv_sound_clip_info(test_clip.id, i) - - -def test_tv_sound_clip_new_wrong_path(tmp_path: Path) -> None: - with pytest.raises(ValueError): - tv_sound_clip_new(tmp_path / "unknown.wav") - - -def test_tv_sound_clip_remove(test_clip: TVPClip, wav_file: Path) -> None: - tv_sound_clip_new(wav_file) - assert tv_sound_clip_info(test_clip.id, 0) - tv_sound_clip_remove(0) - - with pytest.raises(GeorgeError): - tv_sound_clip_info(test_clip.id, 0) - - -def test_tv_sound_clip_remove_wrong_pos() -> None: - with pytest.raises(GeorgeError): - tv_sound_clip_remove(1) - - -def test_tv_sound_clip_reload(test_clip: TVPClip, wav_file: Path) -> None: - tv_sound_clip_new(wav_file) - tv_sound_clip_reload(0, 0) - - -def test_tv_sound_clip_reload_wrong_sound_clip_id() -> None: - with pytest.raises(GeorgeError): - tv_sound_clip_reload(6, 0) - - -def test_tv_sound_clip_reload_wrong_track_index(test_clip: TVPClip) -> None: - with pytest.raises(GeorgeError): - tv_sound_clip_reload(test_clip.id, 5) - - -def args_optional_combine(args: list[list[Any]]) -> list[list[Any]]: - return [ - list(prod) - for n in range(len(args) + 1) - for prod in itertools.product(*args[:n]) - ] - - -@pytest.mark.skip("Too difficult to test") -@pytest.mark.parametrize( - "args", - args_optional_combine( - [ - [False, True], # mute - [0.0, 1.0, 5.0], # volume - [0, 1.0], # offset - [5.0], # fade_in_start - ] - ), -) -def test_tv_sound_clip_adjust( - test_clip: TVPClip, wav_file: Path, args: list[Any] -) -> None: - tv_sound_clip_new(wav_file) - track_index = 0 - - tv_sound_clip_adjust(track_index, *args) - - sound_clip = tv_sound_clip_info(test_clip.id, track_index) - - real_values = [ - sound_clip.mute, - sound_clip.volume, - sound_clip.offset, - sound_clip.fade_in_start, - sound_clip.fade_in_stop, - sound_clip.fade_out_start, - sound_clip.fade_out_stop, - sound_clip.color_index, - ] - - for real, expected in zip(real_values, args): - assert real == expected - - -def test_tv_layer_image_get(test_project: TVPLayer) -> None: - assert tv_layer_image_get() == 0 - - -@pytest.mark.parametrize("frame", range(10)) -def test_tv_layer_image(frame: int) -> None: - tv_layer_image(frame) - assert tv_layer_image_get() == frame +# from __future__ import annotations +# +# import itertools +# import re +# from collections.abc import Iterable, Iterator +# from pathlib import Path +# from typing import Any +# +# import pytest +# +# from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError +# from pytvpaint.george.grg_base import ( +# FieldOrder, +# SaveFormat, +# SpriteLayout, +# tv_save_mode_get, +# ) +# from pytvpaint.george.grg_clip import ( +# PSDSaveMode, +# TVPClip, +# tv_bookmark_clear, +# tv_bookmark_next, +# tv_bookmark_prev, +# tv_bookmark_set, +# tv_bookmarks_enum, +# tv_clip_action_get, +# tv_clip_action_set, +# tv_clip_close, +# tv_clip_color_get, +# tv_clip_color_set, +# tv_clip_current_id, +# tv_clip_dialog_get, +# tv_clip_dialog_set, +# tv_clip_duplicate, +# tv_clip_enum_id, +# tv_clip_hidden_get, +# tv_clip_hidden_set, +# tv_clip_info, +# tv_clip_move, +# tv_clip_name_get, +# tv_clip_name_set, +# tv_clip_new, +# tv_clip_note_get, +# tv_clip_note_set, +# tv_clip_save_structure_csv, +# tv_clip_save_structure_flix, +# tv_clip_save_structure_json, +# tv_clip_save_structure_psd, +# tv_clip_save_structure_sprite, +# tv_clip_select, +# tv_clip_selection_get, +# tv_clip_selection_set, +# tv_first_image, +# tv_last_image, +# tv_layer_image, +# tv_layer_image_get, +# tv_load_sequence, +# tv_save_clip, +# tv_save_display, +# tv_save_sequence, +# tv_sound_clip_adjust, +# tv_sound_clip_info, +# tv_sound_clip_new, +# tv_sound_clip_reload, +# tv_sound_clip_remove, +# ) +# from pytvpaint.george.grg_layer import ( +# LayerType, +# TVPLayer, +# tv_instance_get_name, +# tv_layer_current_id, +# tv_layer_display_set, +# tv_layer_get_id, +# tv_layer_info, +# tv_layer_kill, +# tv_layer_rename, +# tv_layer_set, +# ) +# from pytvpaint.george.grg_project import TVPProject, tv_save_project +# from pytvpaint.george.grg_scene import tv_scene_current_id, tv_scene_new +# from tests.conftest import FixtureYield, test_scene +# +# +# def test_tv_clip_info(test_clip: TVPClip) -> None: +# assert tv_clip_info(test_clip.id) +# +# +# def test_tv_clip_info_wrong_id(test_clip: TVPClip) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_clip_info(-2) +# +# +# def test_tv_clip_enum_id(test_scene: int) -> None: +# clips: list[int] = [] +# +# for i in range(5): +# tv_clip_new(f"clip_{i}") +# clips.append(tv_clip_current_id()) +# +# for i, clip_id in enumerate(clips): +# # We offset 1 because of default clip +# assert tv_clip_enum_id(test_scene, i + 1) == clip_id +# +# +# def test_tv_clip_enum_id_wrong_scene_id() -> None: +# with pytest.raises(GeorgeError): +# tv_clip_enum_id(-2, 0) +# +# +# @pytest.mark.parametrize("pos", [-1, 1, 50]) +# def test_tv_clip_enum_id_wrong_clip_pos(test_scene: int, pos: int) -> None: +# with pytest.raises(GeorgeError): +# tv_clip_enum_id(test_scene, pos) +# +# +# def test_tv_clip_current_id() -> None: +# assert tv_clip_current_id() +# +# +# @pytest.mark.parametrize("name", ["", "a", "0", "lfseflj0", "clip with spaces"]) +# def test_tv_clip_new(test_scene: int, name: str) -> None: +# tv_clip_new(name) +# assert tv_clip_info(tv_clip_current_id()).name == name +# +# +# def test_tv_clip_duplicate(test_scene: int, test_clip: TVPClip) -> None: +# tv_clip_duplicate(test_clip.id) +# dup_clip = tv_clip_info(tv_clip_current_id()) +# assert dup_clip == test_clip +# +# +# def test_tv_clip_close(test_clip: TVPClip) -> None: +# tv_clip_close(test_clip.id) +# with pytest.raises(NoObjectWithIdError): +# tv_clip_info(test_clip.id) +# +# +# def test_tv_clip_name_get(test_clip: TVPClip) -> None: +# assert tv_clip_name_get(test_clip.id) == test_clip.name +# +# +# def test_tv_clip_name_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_clip_name_get(-1) +# +# +# @pytest.mark.parametrize("name", ["_", "a", "0", "lfseflj0"]) +# def test_tv_clip_name_set(test_clip: TVPClip, name: str) -> None: +# tv_clip_name_set(test_clip.id, name) +# assert tv_clip_info(test_clip.id).name == name +# +# +# def test_tv_clip_name_set_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_clip_name_get(-1) +# +# +# # "Copy" the fixture to use it twice in the above test +# # See: https://stackoverflow.com/questions/36100624/pytest-use-same-fixture-twice-in-one-function +# destination_scene = test_scene +# +# +# @pytest.mark.parametrize("new_pos", range(5)) +# def test_tv_clip_move( +# test_scene: int, test_clip: TVPClip, destination_scene: int, new_pos: int +# ) -> None: +# for i in range(5): +# tv_clip_new(f"other_clip_{i}") +# +# tv_clip_move(test_clip.id, destination_scene, new_pos) +# +# # Ensure that it's not in the first scene +# with pytest.raises(GeorgeError): +# tv_clip_enum_id(test_scene, 1) +# +# # Ensure that it's at the right position in the other scene +# tv_clip_enum_id(destination_scene, new_pos) +# +# +# def test_tv_clip_hidden_get(test_clip: TVPClip) -> None: +# assert tv_clip_hidden_get(test_clip.id) == test_clip.is_hidden +# +# +# def test_tv_clip_hidden_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_clip_hidden_get(-1) +# +# +# @pytest.mark.parametrize("hidden", [True, False]) +# def test_tv_clip_hidden_set(test_clip: TVPClip, hidden: bool) -> None: +# tv_clip_hidden_set(test_clip.id, hidden) +# assert tv_clip_info(test_clip.id).is_hidden == hidden +# +# +# @pytest.mark.parametrize("hidden", [True, False]) +# def test_tv_clip_hidden_set_wrong_id(hidden: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_clip_hidden_set(-1, hidden) +# +# +# def test_tv_clip_select(test_scene: int, test_clip: TVPClip) -> None: +# first_clip = tv_clip_enum_id(test_scene, 0) +# tv_clip_select(first_clip) +# +# assert not tv_clip_info(test_clip.id).is_current +# tv_clip_select(test_clip.id) +# assert tv_clip_info(test_clip.id).is_current +# +# +# def test_tv_clip_select_also_selects_scene( +# test_project: TVPProject, +# test_scene: int, +# test_clip: TVPClip, +# ) -> None: +# tv_scene_new() +# assert tv_scene_current_id() != test_scene +# tv_clip_select(test_clip.id) +# assert tv_scene_current_id() == test_scene +# +# +# def test_tv_clip_selection_get(test_clip: TVPClip) -> None: +# assert tv_clip_selection_get(test_clip.id) == test_clip.is_selected +# +# +# def test_tv_clip_selection_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_clip_selection_get(-1) +# +# +# @pytest.mark.parametrize("select", [True, False]) +# def test_tv_clip_selection_set(test_clip: TVPClip, select: bool) -> None: +# tv_clip_selection_set(test_clip.id, select) +# assert tv_clip_info(test_clip.id).is_selected == select +# +# +# @pytest.mark.parametrize("select", [True, False]) +# def test_tv_clip_selection_set_wrong_id(test_clip: TVPClip, select: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_clip_selection_set(-1, select) +# +# +# def test_tv_first_image(test_clip: TVPClip) -> None: +# assert tv_first_image() == test_clip.first_frame +# +# +# def test_tv_last_image(test_clip: TVPClip) -> None: +# assert tv_last_image() == test_clip.last_frame +# +# +# @pytest.mark.parametrize("offset_count", [None, *itertools.product([0, 1], [0, 1])]) +# @pytest.mark.parametrize("field_order", [None, *FieldOrder]) +# @pytest.mark.parametrize("stretch", [False, True]) +# @pytest.mark.parametrize("time_stretch", [False, True]) +# @pytest.mark.parametrize("preload", [False, True]) +# def test_tv_load_sequence( +# ppm_sequence: list[Path], +# test_clip: TVPClip, +# offset_count: tuple[int, int] | None, +# field_order: FieldOrder | None, +# stretch: bool, +# time_stretch: bool, +# preload: bool, +# ) -> None: +# total_images = len(ppm_sequence) +# first_image = ppm_sequence[0] +# +# images_loaded = tv_load_sequence( +# first_image, +# offset_count, +# field_order, +# stretch, +# time_stretch, +# preload, +# ) +# +# if offset_count: +# offset, count = offset_count +# should_load = total_images if count <= 0 else min(count, total_images - offset) +# else: +# should_load = total_images +# +# assert should_load == images_loaded +# +# # Find the extension at the end (including file number) +# ext_match = re.search(r"\d+\.[a-z0-9]+$", str(first_image)) +# assert ext_match +# ext_start, _ = ext_match.span() +# +# layer_name_cut = str(first_image)[:ext_start] +# +# layer = tv_layer_info(tv_layer_current_id()) +# +# assert layer.name == layer_name_cut +# assert layer.type == LayerType.SEQUENCE +# assert layer.first_frame == 0 +# assert layer.last_frame == should_load - 1 +# +# +# def test_tv_load_sequence_sequence_does_not_exist(tmp_path: Path) -> None: +# with pytest.raises(FileNotFoundError, match="File not found"): +# tv_load_sequence(tmp_path / "file.001.png") +# +# +# @pytest.fixture +# def bookmarks(test_clip: TVPClip) -> FixtureYield[Iterable[int]]: +# n_bookmarks = 5 +# for frame in range(n_bookmarks): +# tv_bookmark_set(frame) +# yield range(n_bookmarks) +# +# +# def test_tv_bookmarks_enum(bookmarks: Iterable[int]) -> None: +# for pos, mark in enumerate(bookmarks): +# assert tv_bookmarks_enum(pos) == mark +# +# +# @pytest.mark.parametrize("frame", range(5)) +# def test_tv_bookmark_set(test_clip: TVPClip, frame: int) -> None: +# tv_bookmark_set(frame) +# assert tv_bookmarks_enum(0) == frame +# +# +# @pytest.mark.parametrize("frame", range(5)) +# def test_tv_bookmark_clear(test_clip: TVPClip, frame: int) -> None: +# tv_bookmark_set(frame) +# assert tv_bookmarks_enum(0) == frame +# +# tv_bookmark_clear(frame) +# +# with pytest.raises(GeorgeError, match="No bookmark"): +# tv_bookmarks_enum(0) +# +# +# @pytest.mark.parametrize("frame", range(5)) +# def test_tv_bookmark_next(test_clip: TVPClip, frame: int) -> None: +# tv_bookmark_set(frame) +# tv_bookmark_next() +# assert tv_layer_image_get() == frame +# +# +# @pytest.mark.parametrize("frame", range(5)) +# def test_tv_bookmark_prev(test_clip: TVPClip, frame: int) -> None: +# # Set the current image far away +# tv_layer_image(50) +# +# tv_bookmark_set(frame) +# tv_bookmark_prev() +# +# assert tv_layer_image_get() == frame +# +# +# def test_tv_clip_color_get(test_clip: TVPClip) -> None: +# assert tv_clip_color_get(test_clip.id) in range(27) +# +# +# @pytest.mark.parametrize("color_index", range(27)) +# def test_tv_clip_color_set(test_clip: TVPClip, color_index: int) -> None: +# tv_clip_color_set(test_clip.id, color_index) +# assert tv_clip_color_get(test_clip.id) == color_index +# +# +# def test_tv_clip_action_get(test_clip: TVPClip) -> None: +# assert tv_clip_action_get(test_clip.id) == "" +# +# +# def test_tv_clip_action_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_clip_action_get(-3) +# +# +# # Note: we removed the \n test because there's a bug with TVPaint that handle control characters +# TEST_TEXTS = ["", "l", "0", "ab", "a0l", "ap*"] # "a\nb"] +# +# +# @pytest.mark.parametrize("text", TEST_TEXTS) +# def test_tv_clip_action_set(test_clip: TVPClip, text: str) -> None: +# tv_clip_action_set(test_clip.id, text) +# assert tv_clip_action_get(test_clip.id) == text +# +# +# def test_tv_clip_action_set_wrong_id() -> None: +# with pytest.raises(GeorgeError): +# tv_clip_action_set(-2, "test") +# +# +# def test_tv_clip_dialog_get(test_clip: TVPClip) -> None: +# assert tv_clip_dialog_get(test_clip.id) == "" +# +# +# def test_tv_clip_dialog_get_wrong_id() -> None: +# with pytest.raises(GeorgeError): +# tv_clip_dialog_get(-2) +# +# +# @pytest.mark.parametrize("text", TEST_TEXTS) +# def test_tv_clip_dialog_set(test_clip: TVPClip, text: str) -> None: +# tv_clip_dialog_set(test_clip.id, text) +# assert tv_clip_dialog_get(test_clip.id) == text +# +# +# def test_tv_clip_dialog_set_wrong_id() -> None: +# with pytest.raises(GeorgeError): +# tv_clip_dialog_set(-2, "test") +# +# +# def test_tv_clip_note_get(test_clip: TVPClip) -> None: +# assert tv_clip_note_get(test_clip.id) == "" +# +# +# def test_tv_clip_note_get_wrong_id() -> None: +# with pytest.raises(GeorgeError): +# tv_clip_dialog_get(-2) +# +# +# @pytest.mark.parametrize("text", TEST_TEXTS) +# def test_tv_clip_note_set(test_clip: TVPClip, text: str) -> None: +# tv_clip_note_set(test_clip.id, text) +# assert tv_clip_note_get(test_clip.id) == text +# +# +# def test_tv_clip_note_set_wrong_id() -> None: +# with pytest.raises(GeorgeError): +# tv_clip_note_set(-2, "test") +# +# +# def test_tv_save_clip(tmp_path: Path) -> None: +# clip_tvp = tmp_path / "clip.tvp" +# tv_save_clip(clip_tvp) +# assert clip_tvp.exists() +# +# +# @pytest.mark.skip("Will block the UI") +# def test_tv_save_clip_folder_does_not_exist(tmp_path: Path) -> None: +# with pytest.raises(GeorgeError, match="Can't create file"): +# tv_save_clip(tmp_path / "folder" / "out.tvpx") +# +# +# def test_tv_save_display(tmp_path: Path) -> None: +# save_ext, _ = tv_save_mode_get() +# ext = "jpg" if save_ext == SaveFormat.JPG else save_ext.value +# out_display = (tmp_path / "out").with_suffix("." + ext) +# tv_save_display(out_display) +# assert out_display.exists() +# +# +# def current_clip_layers() -> Iterator[TVPLayer]: +# """Iterates through the current clip layers""" +# pos = 0 +# while True: +# try: +# lid = tv_layer_get_id(pos) +# yield tv_layer_info(lid) +# except GeorgeError: +# break +# pos += 1 +# +# +# def get_instance_frames() -> Iterator[tuple[int, str]]: +# """Iterates through the instances of the current layer""" +# layer = tv_layer_current_id() +# frame = 0 +# while True: +# try: +# name = tv_instance_get_name(layer, frame) +# yield frame, name +# except NoObjectWithIdError: +# break +# frame += 1 +# +# +# def apply_folder_pattern(initial_pattern: str | None, layer: TVPLayer) -> str: +# # This is the default folder pattern +# if initial_pattern is None: +# return f"[{layer.position:03d}] {layer.name}" +# +# patterns = { +# r"%li": str(layer.position), +# r"%ln": layer.name, +# r"%fi": r"%fi", # Couldn't make it work +# } +# +# for pattern, rep in patterns.items(): +# initial_pattern = initial_pattern.replace(pattern, rep) +# +# return initial_pattern +# +# +# def apply_file_pattern( +# initial_pattern: str | None, +# layer: TVPLayer, +# image_index: int, +# image_name: str, +# file_index: int, +# ) -> str: +# # This is the default file pattern +# if initial_pattern is None: +# return f"[{image_index:03d}] {layer.name}" +# +# # When the instance name is empty it takes the image index +# if image_name == "": +# image_name = str(image_index) +# +# patterns = { +# r"%li": str(layer.position), +# r"%ln": layer.name, +# r"%ii": str(image_index), +# r"%in": image_name, +# r"%fi": str(file_index), +# } +# +# for pattern, rep in patterns.items(): +# initial_pattern = initial_pattern.replace(pattern, rep) +# +# return initial_pattern +# +# +# def load_sequence_with_name(first_frame: Path, name: str, count: int) -> int: +# """Load an image sequence and rename the layer""" +# tv_load_sequence(first_frame, offset_count=(0, count)) +# layer = tv_layer_current_id() +# tv_layer_rename(layer, name) +# return layer +# +# +# @pytest.mark.parametrize( +# "mark_in, mark_out", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)] +# ) +# def test_tv_save_sequence( +# test_project: TVPProject, +# tmp_path: Path, +# ppm_sequence: list[Path], +# mark_in: int | None, +# mark_out: int | None, +# ) -> None: +# tv_load_sequence(ppm_sequence[0]) +# +# save_ext, _ = tv_save_mode_get() +# out_sequence = tmp_path / "out" +# tv_save_sequence(out_sequence, mark_in, mark_out) +# +# clip = tv_clip_info(tv_clip_current_id()) +# start, end = ( +# (mark_in, mark_out) +# if mark_in is not None and mark_out is not None +# else (clip.first_frame, clip.last_frame) +# ) +# +# for i in range(end - start): +# image_name = f"{out_sequence.name}{i:05d}" +# image_ext = "." + ("jpg" if save_ext == SaveFormat.JPG else save_ext.value) +# image_path = out_sequence.with_name(f"{image_name}{image_ext}") +# assert image_path.exists() +# +# +# def test_tv_save_sequence_wrong_path(tmp_path: Path) -> None: +# with pytest.raises(NotADirectoryError): +# tv_save_sequence(tmp_path / "folder" / "out") +# +# +# @pytest.mark.parametrize( +# "file_format", +# [ +# SaveFormat.PNG, +# # SaveFormat.JPG, +# # SaveFormat.BMP, +# # SaveFormat.TGA, +# # SaveFormat.TIFF, +# ], +# ) +# @pytest.mark.parametrize("fill_background", [False, True]) +# @pytest.mark.parametrize("folder_pattern", [r"folder_%li_%ln_%fi"]) +# @pytest.mark.parametrize("file_pattern", [r"file_%li_%ln_%ii_%in_%fi"]) +# @pytest.mark.parametrize("visible_layers_only", [False, True]) +# @pytest.mark.parametrize("all_images", [False, True]) +# def test_tv_clip_save_structure_json( +# test_project: TVPProject, +# ppm_sequence: list[Path], +# tmp_path: Path, +# file_format: SaveFormat, +# fill_background: bool, +# folder_pattern: str, +# file_pattern: str, +# visible_layers_only: bool, +# all_images: bool, +# ) -> None: +# # Import some frames +# load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) +# load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=3) +# +# # Hide that layer +# seq_3 = load_sequence_with_name(ppm_sequence[0], name="sequence_3", count=3) +# tv_layer_display_set(seq_3, False) +# +# # Remove the first layer (which is in the last position) +# last_layer = list(current_clip_layers())[-1] +# tv_layer_kill(last_layer.id) +# +# out_json_path = tmp_path / "clip.json" +# tv_clip_save_structure_json( +# out_json_path, +# file_format, +# fill_background, +# folder_pattern, +# file_pattern, +# visible_layers_only, +# all_images, +# ) +# assert out_json_path.exists() +# +# for layer in current_clip_layers(): +# if visible_layers_only and not layer.visibility: +# continue +# +# # Check that the layer folders exist +# layer_folder_name = apply_folder_pattern(folder_pattern, layer) +# layer_folder = tmp_path / layer_folder_name +# assert layer_folder.exists() +# +# tv_layer_set(layer.id) +# for i, instance in enumerate(get_instance_frames()): +# frame, name = instance +# +# image_name = apply_file_pattern( +# file_pattern, +# layer, +# image_index=frame + 1, +# image_name=name, +# file_index=i, +# ) +# +# # Check that the images exist +# ext = "." + file_format.value +# image_path = (layer_folder / image_name).with_suffix(ext) +# assert image_path.exists() +# +# +# def test_tv_clip_save_structure_json_file_doesnt_exist(tmp_path: Path) -> None: +# with pytest.raises(ValueError, match="destination folder doesn't exist"): +# tv_clip_save_structure_json(tmp_path / "folder" / "out.json", SaveFormat.PNG) +# +# +# @pytest.mark.parametrize( +# "test_case", +# [ +# (PSDSaveMode.ALL, {}), +# (PSDSaveMode.IMAGE, {"image": 0}), +# (PSDSaveMode.MARKIN, {"mark_in": 0, "mark_out": 5}), +# ], +# ) +# def test_tv_clip_save_structure_psd( +# test_project: TVPProject, +# ppm_sequence: list[Path], +# tmp_path: Path, +# test_case: tuple[PSDSaveMode, dict[str, Any]], +# ) -> None: +# out_psd = tmp_path / "out.psd" +# +# load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) +# load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=2) +# +# mode, args = test_case +# tv_clip_save_structure_psd(out_psd, mode, **args) +# +# if mode == PSDSaveMode.MARKIN: +# # It's a sequence of numbered PSD files +# for i, _ in enumerate(get_instance_frames()): +# out_psd_frame = out_psd.with_stem(f"{out_psd.stem}{i:05d}") +# assert out_psd_frame.exists() +# else: +# # It's a single PSD file +# assert out_psd.exists() +# +# +# def test_tv_tv_clip_save_structure_psd_file_does_not_exist(tmp_path: Path) -> None: +# with pytest.raises(ValueError, match="destination folder doesn't exist"): +# tv_clip_save_structure_psd(tmp_path / "folder" / "out.psd", PSDSaveMode.ALL) +# +# +# @pytest.mark.parametrize("all_images", [None, False, True]) +# @pytest.mark.parametrize("exposure_label", [None, "", "expo", "ex po"]) +# def test_tv_clip_save_structure_csv( +# test_project: TVPProject, +# ppm_sequence: list[Path], +# tmp_path: Path, +# all_images: bool | None, +# exposure_label: str | None, +# ) -> None: +# out_csv = tmp_path / "out.csv" +# +# load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) +# load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=2) +# +# # Remove the first layer (which is in the last position) +# last_layer = list(current_clip_layers())[-1] +# tv_layer_kill(last_layer.id) +# +# tv_clip_save_structure_csv(out_csv, all_images, exposure_label) +# +# assert out_csv.exists() +# +# out_layers_folder = out_csv.with_suffix(".layers") +# assert out_layers_folder.exists() +# +# for i, layer in enumerate(current_clip_layers()): +# layer_index = f"{(i + 1):03d}" +# layer_folder_name = f"[{layer_index}] {layer.name}" +# layer_folder = out_layers_folder / layer_folder_name +# assert layer_folder.exists() +# +# tv_layer_set(layer.id) +# for j, _ in enumerate(get_instance_frames()): +# image_name = f"[{layer_index}][{(j + 1):05d}] {layer.name}.png" +# assert (layer_folder / image_name).exists() +# +# +# @pytest.mark.parametrize("layout", [None, *SpriteLayout]) +# @pytest.mark.parametrize("space", [None, 0, 5, 50]) +# def test_tv_clip_save_structure_sprite( +# test_project: TVPProject, +# ppm_sequence: list[Path], +# tmp_path: Path, +# layout: SpriteLayout | None, +# space: int | None, +# ) -> None: +# load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) +# +# out_sprite = tmp_path / "out.png" +# tv_clip_save_structure_sprite(out_sprite, layout, space) +# +# assert out_sprite.exists() +# +# +# @pytest.mark.parametrize("mark_in_out", [None, (0, 0), (0, 5), (2, 5)]) +# def test_tv_clip_save_structure_flix( +# test_project: TVPProject, +# ppm_sequence: list[Path], +# tmp_path: Path, +# mark_in_out: tuple[int, int] | 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) +# +# # Export to flix needs to save the project first +# tv_save_project(test_project.path) +# +# mark_in: int | None +# mark_out: int | None +# +# if mark_in_out: +# mark_in, mark_out = mark_in_out +# else: +# mark_in = mark_out = None +# +# out_flix = tmp_path / "out.xml" +# +# tv_clip_save_structure_flix(out_flix, mark_in, mark_out, send=False) +# assert out_flix.exists() +# +# +# def test_tv_sound_clip_info(test_clip: TVPClip, wav_file: Path) -> None: +# tv_sound_clip_new(wav_file) +# sound = tv_sound_clip_info(test_clip.id, 0) +# +# assert sound.sound_in == 0 +# assert Path(sound.path) == wav_file +# +# +# def test_tv_sound_clip_info_wrong_clip_id() -> None: +# with pytest.raises(GeorgeError): +# tv_sound_clip_info(-2, 0) +# +# +# def test_tv_sound_clip_info_wrong_sound_track_pos(test_clip: TVPClip) -> None: +# with pytest.raises(GeorgeError): +# tv_sound_clip_info(test_clip.id, 1) +# +# +# def test_tv_sound_clip_new( +# test_project: TVPProject, +# test_clip: TVPClip, +# wav_file: Path, +# ) -> None: +# for i in range(5): +# tv_sound_clip_new(wav_file) +# assert tv_sound_clip_info(test_clip.id, i) +# +# +# def test_tv_sound_clip_new_wrong_path(tmp_path: Path) -> None: +# with pytest.raises(ValueError): +# tv_sound_clip_new(tmp_path / "unknown.wav") +# +# +# def test_tv_sound_clip_remove(test_clip: TVPClip, wav_file: Path) -> None: +# tv_sound_clip_new(wav_file) +# assert tv_sound_clip_info(test_clip.id, 0) +# tv_sound_clip_remove(0) +# +# with pytest.raises(GeorgeError): +# tv_sound_clip_info(test_clip.id, 0) +# +# +# def test_tv_sound_clip_remove_wrong_pos() -> None: +# with pytest.raises(GeorgeError): +# tv_sound_clip_remove(1) +# +# +# def test_tv_sound_clip_reload(test_clip: TVPClip, wav_file: Path) -> None: +# tv_sound_clip_new(wav_file) +# tv_sound_clip_reload(0, 0) +# +# +# def test_tv_sound_clip_reload_wrong_sound_clip_id() -> None: +# with pytest.raises(GeorgeError): +# tv_sound_clip_reload(6, 0) +# +# +# def test_tv_sound_clip_reload_wrong_track_index(test_clip: TVPClip) -> None: +# with pytest.raises(GeorgeError): +# tv_sound_clip_reload(test_clip.id, 5) +# +# +# def args_optional_combine(args: list[list[Any]]) -> list[list[Any]]: +# return [ +# list(prod) +# for n in range(len(args) + 1) +# for prod in itertools.product(*args[:n]) +# ] +# +# +# @pytest.mark.skip("Too difficult to test") +# @pytest.mark.parametrize( +# "args", +# args_optional_combine( +# [ +# [False, True], # mute +# [0.0, 1.0, 5.0], # volume +# [0, 1.0], # offset +# [5.0], # fade_in_start +# ] +# ), +# ) +# def test_tv_sound_clip_adjust( +# test_clip: TVPClip, wav_file: Path, args: list[Any] +# ) -> None: +# tv_sound_clip_new(wav_file) +# track_index = 0 +# +# tv_sound_clip_adjust(track_index, *args) +# +# sound_clip = tv_sound_clip_info(test_clip.id, track_index) +# +# real_values = [ +# sound_clip.mute, +# sound_clip.volume, +# sound_clip.offset, +# sound_clip.fade_in_start, +# sound_clip.fade_in_stop, +# sound_clip.fade_out_start, +# sound_clip.fade_out_stop, +# sound_clip.color_index, +# ] +# +# for real, expected in zip(real_values, args): +# assert real == expected +# +# +# def test_tv_layer_image_get(test_project: TVPLayer) -> None: +# assert tv_layer_image_get() == 0 +# +# +# @pytest.mark.parametrize("frame", range(10)) +# def test_tv_layer_image(frame: int) -> None: +# tv_layer_image(frame) +# assert tv_layer_image_get() == frame diff --git a/tests/george/test_grg_layer.py b/tests/george/test_grg_layer.py index 1f249da..dc17c96 100644 --- a/tests/george/test_grg_layer.py +++ b/tests/george/test_grg_layer.py @@ -1,940 +1,971 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError -from pytvpaint.george.grg_base import ( - BlendingMode, - RGBColor, - SaveFormat, - tv_rect, - tv_save_mode_get, -) -from pytvpaint.george.grg_clip import TVPClip, tv_clip_current_id, tv_layer_image -from pytvpaint.george.grg_layer import ( - InsertDirection, - InstanceNamingMode, - InstanceNamingProcess, - LayerBehavior, - LayerColorDisplayOpt, - LayerTransparency, - StencilMode, - TVPLayer, - tv_instance_get_name, - tv_instance_name, - tv_instance_set_name, - tv_layer_anim, - tv_layer_auto_break_instance_get, - tv_layer_auto_break_instance_set, - tv_layer_auto_create_instance_get, - tv_layer_auto_create_instance_set, - tv_layer_blending_mode_get, - tv_layer_blending_mode_set, - tv_layer_collapse_get, - tv_layer_collapse_set, - tv_layer_color_get, - tv_layer_color_get_color, - tv_layer_color_hide, - tv_layer_color_lock, - tv_layer_color_select, - tv_layer_color_set, - tv_layer_color_set_color, - tv_layer_color_show, - tv_layer_color_unlock, - tv_layer_color_unselect, - tv_layer_color_visible, - tv_layer_copy, - tv_layer_create, - tv_layer_current_id, - tv_layer_cut, - tv_layer_density_get, - tv_layer_density_set, - tv_layer_display_get, - tv_layer_display_set, - tv_layer_duplicate, - tv_layer_get_id, - tv_layer_get_pos, - tv_layer_info, - tv_layer_insert_image, - tv_layer_kill, - tv_layer_load_dependencies, - tv_layer_lock_get, - tv_layer_lock_position_get, - tv_layer_lock_position_set, - tv_layer_lock_set, - tv_layer_mark_get, - tv_layer_mark_set, - tv_layer_merge, - tv_layer_merge_all, - tv_layer_move, - tv_layer_paste, - tv_layer_post_behavior_get, - tv_layer_post_behavior_set, - tv_layer_pre_behavior_get, - tv_layer_pre_behavior_set, - tv_layer_rename, - tv_layer_select, - tv_layer_selection_get, - tv_layer_selection_set, - tv_layer_set, - tv_layer_shift, - tv_layer_show_thumbnails_get, - tv_layer_show_thumbnails_set, - tv_layer_stencil_get, - tv_layer_stencil_set, - tv_load_image, - tv_preserve_get, - tv_preserve_set, - tv_save_image, -) -from pytvpaint.george.grg_project import TVPProject -from pytvpaint.layer import Layer -from tests.conftest import FixtureYield - - -def test_tv_layer_current_id(test_project: TVPProject) -> None: - assert tv_layer_current_id() - - -def test_tv_layer_get_id(test_project: TVPProject) -> None: - assert tv_layer_get_id(0) == tv_layer_current_id() - - -def test_tv_layer_get_id_neg_pos_error(test_project: TVPProject) -> None: - with pytest.raises(GeorgeError, match="No layer at provided position"): - tv_layer_get_id(-1) - - -def test_tv_layer_get_pos(test_project: TVPProject) -> None: - assert tv_layer_get_pos(tv_layer_current_id()) == 0 - - -def test_tv_layer_get_pos_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_get_pos(56) - - -def test_tv_layer_info() -> None: - info = tv_layer_info(tv_layer_current_id()) - assert info.id - - -def test_tv_layer_info_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_info(-4) - - -def test_tv_layer_move(test_project: TVPProject) -> None: - current_layer = tv_layer_current_id() - total_layers = 10 - - for i in range(total_layers): - tv_layer_create(f"layer_{i}") - - # Make the current layer the original one - tv_layer_set(current_layer) - - for i in range(total_layers): - new_pos = i + 1 - tv_layer_move(new_pos) - layer_info = tv_layer_info(current_layer) - # Position starts at 1 - assert layer_info.position + 1 == new_pos - - -@pytest.mark.parametrize("pos", [-1, 2, 100, 1000]) -def test_tv_layer_move_wrong_pos(test_project: TVPProject, pos: int) -> None: - with pytest.raises( - GeorgeError, - match="Couldn't move current layer to position", - ): - tv_layer_move(pos) - - -def test_tv_layer_set(test_project: TVPProject) -> None: - layers = [tv_layer_create(f"layer_{i}") for i in range(5)] - - for layer in layers: - tv_layer_set(layer) - assert tv_layer_current_id() == layer - - -def test_tv_layer_set_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_set(-16) - - -def test_tv_layer_selection_get(test_layer: TVPLayer) -> None: - assert not tv_layer_selection_get(test_layer.id) - - -def test_tv_layer_selection_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_selection_get(-1) - - -@pytest.mark.parametrize("selected", [True, False]) -def test_tv_layer_selection_set(test_project: TVPProject, selected: bool) -> None: - layers = [tv_layer_create(f"layer_{i}") for i in range(5)] - - for layer in layers: - tv_layer_selection_set(layer, new_state=selected) - assert tv_layer_info(layer).selected == selected - - -@pytest.mark.parametrize("selected", [True, False]) -def test_tv_layer_selection_set_wrong_id(selected: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_selection_set(-1, selected) - - -def test_tv_layer_selection_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_selection_set(-16, True) - - -def test_tv_layer_select_n(test_project: TVPProject, test_anim_layer: TVPLayer) -> None: - end_frame = 10 - - # Draw something in order to select frames - tv_layer_copy() - tv_layer_image(end_frame) - tv_layer_paste() - tv_rect(0, 0, 200, 200) - - selected_frames = tv_layer_select(0, end_frame) - assert selected_frames == end_frame - - -LAYER_NAMES_TO_TEST = ["new_layer", "0", "new layer", ""] - - -@pytest.mark.parametrize("name", LAYER_NAMES_TO_TEST) -def test_tv_layer_create(test_project: TVPProject, name: str) -> None: - new_layer = tv_layer_create(name) - assert tv_layer_info(new_layer).name == name - - -@pytest.mark.parametrize("new_name", LAYER_NAMES_TO_TEST) -def test_tv_layer_duplicate(test_project: TVPProject, new_name: str) -> None: - dup_layer_id = tv_layer_duplicate(new_name) - assert tv_layer_current_id() == dup_layer_id - assert tv_layer_info(tv_layer_current_id()).name == new_name - - -@pytest.mark.parametrize("new_name", LAYER_NAMES_TO_TEST) -def test_tv_layer_rename(test_layer: TVPLayer, new_name: str) -> None: - tv_layer_rename(tv_layer_current_id(), new_name) - - -def test_tv_layer_rename_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_rename(-1, "test") - - -def test_tv_layer_kill(test_project: TVPProject) -> None: - new_layer = tv_layer_create("destroy") - tv_layer_kill(new_layer) - - # Layer shouldn't exist anymore - with pytest.raises(NoObjectWithIdError): - tv_layer_get_pos(new_layer) - - -def test_tv_layer_kill_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_kill(-5) - - -def test_tv_layer_density_get() -> None: - assert 0 <= tv_layer_density_get() <= 100 - - -@pytest.mark.parametrize("density", [0, 50, 100, 25, 150, -50]) -def test_tv_layer_density_set(density: int) -> None: - tv_layer_density_set(density) - assert tv_layer_density_get() == min(max(0, density), 100) - - -def test_tv_layer_display_get(test_layer: TVPLayer) -> None: - current_layer = tv_layer_info(tv_layer_current_id()) - assert current_layer.visibility == tv_layer_display_get(current_layer.id) - - -def test_tv_layer_display_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_display_get(-1) - - -@pytest.mark.parametrize("new_state", [True, False]) -def test_tv_layer_display_set(test_layer: TVPLayer, new_state: bool) -> None: - current_layer = tv_layer_info(tv_layer_current_id()) - tv_layer_display_set(current_layer.id, new_state) - assert tv_layer_info(current_layer.id).visibility == new_state - - -def test_tv_layer_lock_get(test_layer: TVPLayer) -> None: - tv_layer_lock_get(tv_layer_current_id()) - - -def test_tv_layer_lock_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_lock_get(-1) - - -@pytest.mark.parametrize("lock", [True, False]) -def test_tv_layer_lock_set(test_layer: TVPLayer, lock: bool) -> None: - current_layer = tv_layer_current_id() - tv_layer_lock_set(current_layer, lock) - assert tv_layer_lock_get(current_layer) == lock - - -@pytest.mark.parametrize("lock", [True, False]) -def test_tv_layer_lock_set_wrong_id(lock: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_lock_set(-1, lock) - - -def test_tv_layer_collapse_get() -> None: - tv_layer_collapse_get(tv_layer_current_id()) - - -def test_tv_layer_collapse_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_collapse_get(-1) - - -@pytest.mark.parametrize("collapse", [True, False]) -def test_tv_layer_collapse_set(test_layer: TVPLayer, collapse: bool) -> None: - current_layer = tv_layer_current_id() - tv_layer_collapse_set(current_layer, collapse) - assert tv_layer_collapse_get(current_layer) == collapse - - -@pytest.mark.parametrize("collapse", [True, False]) -def test_tv_layer_collapse_set_wrong_id(collapse: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_collapse_set(-1, collapse) - - -def test_tv_layer_blending_mode_get() -> None: - assert tv_layer_blending_mode_get(tv_layer_current_id()) in list(BlendingMode) - - -def test_tv_layer_blending_mode_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_blending_mode_get(-1) - - -@pytest.mark.parametrize("mode", BlendingMode) -def test_tv_layer_blending_mode_set(test_layer: TVPLayer, mode: BlendingMode) -> None: - current_layer = tv_layer_current_id() - tv_layer_blending_mode_set(current_layer, mode) - assert tv_layer_blending_mode_get(current_layer) == mode - - -def test_tv_layer_blending_mode_set_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_blending_mode_set(-1, BlendingMode.ADD) - - -def test_tv_layer_stencil_get() -> None: - tv_layer_stencil_get(tv_layer_current_id()) - - -def test_tv_layer_stencil_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_stencil_get(-1) - - -@pytest.mark.parametrize("mode", StencilMode) -def test_tv_layer_stencil_set(test_layer: TVPLayer, mode: StencilMode) -> None: - current_layer = tv_layer_current_id() - tv_layer_stencil_set(current_layer, mode) - - current_mode = tv_layer_stencil_get(current_layer) - - if mode == StencilMode.ON: - assert current_mode == StencilMode.NORMAL - else: - assert current_mode == mode - - -@pytest.mark.parametrize("mode", StencilMode) -def test_tv_layer_stencil_set_wrong_id(mode: StencilMode) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_stencil_set(-1, mode) - - -def test_tv_layer_show_thumbnails_get() -> None: - tv_layer_show_thumbnails_get(tv_layer_current_id()) - - -def test_tv_layer_show_thumbnails_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_show_thumbnails_get(-1) - - -@pytest.mark.parametrize("state", [True, False]) -def test_tv_layer_show_thumbnails_set(test_layer: TVPLayer, state: bool) -> None: - current_layer = tv_layer_current_id() - tv_layer_show_thumbnails_set(current_layer, state) - assert tv_layer_show_thumbnails_get(current_layer) == state - - -@pytest.mark.parametrize("state", [True, False]) -def test_tv_layer_show_thumbnails_set_wrong_id(state: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_show_thumbnails_set(-1, state) - - -def test_tv_layer_auto_break_instance_get() -> None: - tv_layer_auto_break_instance_get(tv_layer_current_id()) - - -def test_tv_layer_auto_break_instance_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_auto_break_instance_get(-1) - - -@pytest.mark.parametrize("state", [True, False]) -def test_tv_layer_auto_break_instance_set( - test_project: TVPProject, state: bool -) -> None: - current_layer = tv_layer_current_id() - tv_layer_auto_break_instance_set(current_layer, state) - assert tv_layer_auto_break_instance_get(current_layer) == state - - -@pytest.mark.parametrize("state", [True, False]) -def test_tv_layer_auto_break_instance_set_wrong_id(state: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_auto_break_instance_set(-1, state) - - -def test_tv_layer_auto_create_instance_get() -> None: - tv_layer_auto_create_instance_get(tv_layer_current_id()) - - -def test_tv_layer_auto_create_instance_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_auto_create_instance_get(-1) - - -@pytest.mark.parametrize("state", [True, False]) -def test_tv_layer_auto_create_instance_set(test_layer: TVPLayer, state: bool) -> None: - current_layer = tv_layer_current_id() - tv_layer_auto_create_instance_set(current_layer, state) - assert tv_layer_auto_create_instance_get(current_layer) == state - - -@pytest.mark.parametrize("state", [True, False]) -def test_tv_layer_auto_create_instance_set_wrong_id(state: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_auto_create_instance_set(-1, state) - - -def test_tv_layer_pre_behavior_get() -> None: - tv_layer_pre_behavior_get(tv_layer_current_id()) - - -def test_tv_layer_pre_behavior_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_pre_behavior_get(-1) - - -@pytest.mark.parametrize("behavior", LayerBehavior) -def test_tv_layer_pre_behavior_set( - test_layer: TVPLayer, behavior: LayerBehavior -) -> None: - current_layer = tv_layer_current_id() - tv_layer_pre_behavior_set(current_layer, behavior) - assert tv_layer_pre_behavior_get(current_layer) == behavior - - -@pytest.mark.parametrize("behavior", LayerBehavior) -def test_tv_layer_pre_behavior_set_wrong_id(behavior: LayerBehavior) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_pre_behavior_set(-1, behavior) - - -def test_tv_layer_post_behavior_get() -> None: - tv_layer_post_behavior_get(tv_layer_current_id()) - - -def test_tv_layer_post_behavior_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_post_behavior_get(-1) - - -@pytest.mark.parametrize("behavior", LayerBehavior) -def test_tv_layer_post_behavior_set( - test_layer: TVPLayer, behavior: LayerBehavior -) -> None: - current_layer = tv_layer_current_id() - tv_layer_post_behavior_set(current_layer, behavior) - assert tv_layer_post_behavior_get(current_layer) == behavior - - -@pytest.mark.parametrize("behavior", LayerBehavior) -def test_tv_layer_post_behavior_set_wrong_id(behavior: LayerBehavior) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_post_behavior_set(-1, behavior) - - -def test_tv_layer_lock_position_get() -> None: - tv_layer_lock_position_get(tv_layer_current_id()) - - -def test_tv_layer_lock_position_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_lock_position_get(-1) - - -@pytest.mark.parametrize("state", [True, False]) -def test_tv_layer_lock_position_set(test_layer: TVPLayer, state: bool) -> None: - current_layer = tv_layer_current_id() - tv_layer_lock_position_set(current_layer, state) - assert tv_layer_lock_position_get(current_layer) == state - - -@pytest.mark.parametrize("state", [True, False]) -def test_tv_layer_lock_position_set_wrong_id(state: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_lock_position_set(-1, state) - - -def test_tv_preserve_get() -> None: - tv_preserve_get() - - -@pytest.mark.parametrize("state", LayerTransparency) -def test_tv_preserve_set(test_layer: TVPLayer, state: LayerTransparency) -> None: - tv_preserve_set(state) - new_state = tv_preserve_get() - - transparency_map = { - LayerTransparency.MINUS_1: LayerTransparency.ON, - LayerTransparency.NONE: LayerTransparency.OFF, - } - - assert transparency_map.get(state, state) == new_state - - -def test_tv_layer_mark_get() -> None: - tv_layer_mark_get(tv_layer_current_id(), 0) - - -def test_tv_layer_mark_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_mark_get(-1, 0) - - -@pytest.mark.parametrize("mark", range(27)) -def test_tv_layer_mark_set(test_anim_layer: TVPLayer, mark: int) -> None: - tv_layer_mark_set(test_anim_layer.id, 0, mark) - assert tv_layer_mark_get(test_anim_layer.id, 0) == mark - - -def test_tv_layer_mark_set_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_mark_set(-1, 0, 0) - - -def test_tv_layer_anim(test_layer: TVPLayer) -> None: - tv_layer_anim(test_layer.id) - - assert tv_layer_auto_break_instance_get(test_layer.id) - assert tv_layer_auto_create_instance_get(test_layer.id) - assert not tv_layer_lock_get(test_layer.id) - - -def test_tv_layer_load_dependencies() -> None: - tv_layer_load_dependencies(tv_layer_current_id()) - - -@pytest.mark.parametrize("color_index", range(1, 27)) -def test_tv_layer_color_get_color(color_index: int) -> None: - current_clip = tv_clip_current_id() - color = tv_layer_color_get_color(current_clip, color_index) - assert color.clip_id == current_clip - assert color.color_index == color_index - - -def test_tv_layer_color_get_color_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_color_get_color(-1, 0) - - -# We skip index 0 because it's the "Default" color and can't be changed -@pytest.mark.parametrize("color_index", range(1, 27)) -@pytest.mark.parametrize("name", [None, "test"]) -@pytest.mark.parametrize("rgb", [RGBColor(255, 0, 0), RGBColor(0, 255, 0)]) -def test_tv_layer_color_set_color( - test_clip: TVPClip, color_index: int, name: str | None, rgb: RGBColor -) -> None: - tv_layer_color_set_color(test_clip.id, color_index, rgb, name) - - color = tv_layer_color_get_color(test_clip.id, color_index) - - assert color.color_index == color_index - assert color.color_r == rgb.r - assert color.color_g == rgb.g - assert color.color_b == rgb.b - assert color.clip_id == test_clip.id - - if name is not None: - assert color.name == name - - -def test_tv_layer_color_set_color_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_color_set_color(-1, 1, RGBColor(0, 0, 0)) - - -def test_tv_layer_color_get(test_layer: TVPLayer) -> None: - index = tv_layer_color_get(test_layer.id) - assert 0 <= index <= 26 - - -def test_tv_layer_color_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_layer_color_get(-1) - - -@pytest.mark.parametrize("color_index", range(27)) -def test_tv_layer_color_set_s(test_layer: TVPLayer, color_index: int) -> None: - tv_layer_color_set(test_layer.id, color_index) - assert tv_layer_color_get(test_layer.id) == color_index - - -@pytest.fixture -def layers_with_colors(test_project: TVPProject) -> FixtureYield[tuple[int, list[int]]]: - """ - Fixture that create some layers with a color - """ - color_index = 5 - layers = [tv_layer_create(f"layer_{i}") for i in range(10)] - color_layers = [layer for i, layer in enumerate(layers) if i % 2 == 0] - - # Set the color of all layers - for layer in color_layers: - tv_layer_color_set(layer, color_index) - - yield color_index, color_layers - - -def test_tv_layer_color_lock(layers_with_colors: tuple[int, list[int]]) -> None: - color_index, layers_to_lock = layers_with_colors - assert tv_layer_color_lock(color_index) == len(layers_to_lock) - assert all(tv_layer_lock_get(layer) for layer in layers_to_lock) - - -def test_tv_layer_color_unlock(layers_with_colors: tuple[int, list[int]]) -> None: - color_index, layers_to_lock = layers_with_colors - - for layer in layers_to_lock: - tv_layer_lock_set(layer, True) - - assert tv_layer_color_unlock(color_index) == len(layers_to_lock) - assert all(not tv_layer_lock_get(layer) for layer in layers_to_lock) - - -@pytest.mark.parametrize("display", LayerColorDisplayOpt) -def test_tv_layer_color_show( - layers_with_colors: tuple[int, list[int]], display: LayerColorDisplayOpt -) -> None: - color_index, layers_to_show = layers_with_colors - - is_display = display == LayerColorDisplayOpt.DISPLAY - - for layer in layers_to_show: - if is_display: - tv_layer_display_set(layer, False) - else: - tv_layer_collapse_set(layer, True) - - tv_layer_color_show(display, color_index) - - check_fn = tv_layer_display_get if is_display else tv_layer_collapse_get - - assert all(check_fn(layer) for layer in layers_to_show) - - -@pytest.mark.parametrize("display", LayerColorDisplayOpt) -def test_tv_layer_color_hide( - layers_with_colors: tuple[int, list[int]], display: LayerColorDisplayOpt -) -> None: - color_index, layers_to_hide = layers_with_colors - - tv_layer_color_hide(display, color_index) - - check_fn = ( - tv_layer_display_get - if display == LayerColorDisplayOpt.DISPLAY - else tv_layer_collapse_get - ) - - assert all(not check_fn(layer) for layer in layers_to_hide) - - -@pytest.mark.parametrize("color_index", range(27)) -def test_tv_layer_color_visible(color_index: int) -> None: - # It seems that there's no way to set the visibility of a layer color group - # So we only test that all groups are visible - assert tv_layer_color_visible(color_index) - - -def test_tv_layer_color_select(layers_with_colors: tuple[int, list[int]]) -> None: - color_index, layers_to_select = layers_with_colors - - tv_layer_color_select(color_index) - assert all(tv_layer_selection_get(layer) for layer in layers_to_select) - - -def test_tv_layer_color_unselect(layers_with_colors: tuple[int, list[int]]) -> None: - color_index, layers_to_select = layers_with_colors - - for layer in layers_to_select: - tv_layer_selection_set(layer, True) - - tv_layer_color_unselect(color_index) - assert all(not tv_layer_selection_get(layer) for layer in layers_to_select) - - -def can_be_parsed_as_int(value: str) -> bool: - if " " in value: - return False - - try: - int(value) - except ValueError: - return False - - 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"]) -@pytest.mark.parametrize("process", [None, *InstanceNamingProcess]) -@pytest.mark.parametrize("initial_name", ["", "5", " 7", "fest", "test_3"]) -def test_tv_instance_name( - test_layer: TVPLayer, - mode: InstanceNamingMode, - prefix: str | None, - suffix: str | None, - process: InstanceNamingProcess | None, - initial_name: str, -) -> None: - instance = 0 - - # Assign an initial name to the instance - tv_instance_set_name(test_layer.id, instance, name=initial_name) - - # Rename all instances - tv_instance_name(test_layer.id, mode, prefix, suffix, process) - - if mode == InstanceNamingMode.ALL: - expected_name = " " + str(instance + 1) - if prefix: - expected_name = prefix + expected_name - else: # SMART mode - no_prefix = prefix is None - no_suffix = suffix is None - - cond_text = ( - len(initial_name) - and process == InstanceNamingProcess.TEXT - and (can_be_parsed_as_int(initial_name)) - ) - - cond_empty = ( - len(initial_name) - and process == InstanceNamingProcess.EMPTY - and (no_prefix != no_suffix and not can_be_parsed_as_int(initial_name)) - ) - - cond_number = ( - len(initial_name) - and process == InstanceNamingProcess.NUMBER - and not (no_prefix == no_suffix and not can_be_parsed_as_int(initial_name)) - and not can_be_parsed_as_int(initial_name) - ) - - if cond_text or cond_empty or cond_number: - expected_name = initial_name - else: - expected_name = str(instance + 1) - - if suffix: - expected_name += suffix - - if prefix: - expected_name = prefix + expected_name - - assert expected_name == tv_instance_get_name(test_layer.id, instance) - - -@pytest.mark.skip("Crashed TVPaint") -@pytest.mark.parametrize("mode", InstanceNamingMode) -def test_tv_instance_name_wrong_id(mode: InstanceNamingMode) -> None: - with pytest.raises(NoObjectWithIdError): - tv_instance_name(-1, mode) - - -def test_tv_instance_get_name(test_layer: TVPLayer) -> None: - # By default there's an instance at frame zero - tv_instance_get_name(test_layer.id, 0) - - -def test_tv_instance_get_name_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_instance_get_name(-1, 0) - - -@pytest.mark.parametrize("name", ["", "5", " 7", "fest", "test_3"]) -def test_tv_instance_set_name(test_layer: TVPLayer, name: str) -> None: - tv_instance_set_name(test_layer.id, 0, name) - assert tv_instance_get_name(test_layer.id, 0) == name - - -def test_tv_instance_set_name_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_instance_set_name(-1, 0, "test") - - -def instance_exists(frame: int) -> bool: - try: - tv_instance_get_name(tv_layer_current_id(), frame) - except NoObjectWithIdError: - return False - return True - - -@pytest.mark.parametrize("frame", range(5)) -def test_tv_layer_copy_paste(test_anim_layer: TVPLayer, frame: int) -> None: - tv_layer_image(0) - tv_layer_copy() - tv_layer_image(frame) - tv_layer_paste() - - assert instance_exists(frame) - - -@pytest.mark.parametrize("frame", range(1, 6)) -def test_tv_layer_cut_paste(test_anim_layer: TVPLayer, frame: int) -> None: - cut_frame = 10 - - # Add another instance because if we cut the first it deletes the layer - tv_layer_copy() - tv_layer_image(cut_frame) - tv_layer_paste() - - # Cut that instance - tv_layer_image(cut_frame) - tv_layer_cut() - - # The instance should be removed - assert not instance_exists(cut_frame) - - # Paste at another frame - tv_layer_image(frame) - tv_layer_paste() - - assert instance_exists(frame) - - -def test_tv_layer_insert_image_duplicate(test_anim_layer: TVPLayer) -> None: - initial_frame = 5 - - # Copy the instance in the middle - tv_layer_image(0) - tv_layer_copy() - tv_layer_image(initial_frame) - tv_layer_paste() - - tv_layer_insert_image(duplicate=True) - assert instance_exists(initial_frame + 1) - - -@pytest.mark.parametrize("count", range(1, 8)) -@pytest.mark.parametrize("direction", InsertDirection) -def test_tv_layer_insert_image( - test_anim_layer: TVPLayer, count: int, direction: InsertDirection -) -> None: - initial_frame = 10 - - # Copy the instance in the middle - tv_layer_image(0) - tv_layer_copy() - tv_layer_image(initial_frame) - tv_layer_paste() - - tv_layer_insert_image(count, direction) - - offset = 0 - while offset < count: - if direction == InsertDirection.AFTER: - next_frame = initial_frame + offset - else: - next_frame = initial_frame - offset - - assert instance_exists(next_frame) - offset += 1 - - -@pytest.mark.parametrize("start", [0, 5, 10, 100]) -def test_tv_layer_shift(test_layer: TVPLayer, start: int) -> None: - tv_layer_shift(test_layer.id, start) - - -@pytest.mark.parametrize("blending", BlendingMode) -@pytest.mark.parametrize("stamp", [True, False]) -def test_tv_layer_merge( - create_some_layers: list[Layer], blending: BlendingMode, stamp: bool -) -> None: - tv_layer_merge(create_some_layers[0].id, blending, stamp) - - -@pytest.mark.parametrize("keep_color_grp", [False, True]) -@pytest.mark.parametrize("keep_img_mark", [False, True]) -@pytest.mark.parametrize("keep_instance_name", [False, True]) -def test_tv_layer_merge_all( - create_some_layers: list[Layer], - keep_color_grp: bool, - keep_img_mark: bool, - keep_instance_name: bool, -) -> None: - tv_layer_merge_all(keep_color_grp, keep_img_mark, keep_instance_name) - - -def test_tv_save_image(tmp_path: Path) -> None: - save_ext, _ = tv_save_mode_get() - ext = "jpg" if save_ext == SaveFormat.JPG else save_ext.value - out_img = (tmp_path / "out").with_suffix("." + ext) - tv_save_image(out_img) - assert out_img.exists() - - -@pytest.mark.parametrize("stretch", [False, True]) -def test_tv_load_image( - test_clip: TVPClip, - test_layer: TVPLayer, - ppm_sequence: list[Path], - stretch: bool, -) -> None: - tv_load_image(ppm_sequence[0], stretch) - # Verify that there's an instance frame - assert tv_instance_get_name(test_layer.id, 0) == "" - - -@pytest.mark.skip("Will block the UI") -def test_tv_load_image_file_does_not_exist(tmp_path: Path) -> None: - with pytest.raises(FileNotFoundError): - tv_load_image(tmp_path / "file.png") +# from __future__ import annotations +# +# from pathlib import Path +# +# import pytest +# +# from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError +# from pytvpaint.george.grg_base import ( +# BlendingMode, +# RGBColor, +# SaveFormat, +# tv_version, +# tv_rect, +# tv_save_mode_get, +# ) +# from pytvpaint.george.grg_clip import TVPClip, tv_clip_current_id, tv_layer_image +# from pytvpaint.george.grg_layer import ( +# InsertDirection, +# InstanceNamingMode, +# InstanceNamingProcess, +# LayerBehavior, +# LayerColorDisplayOpt, +# LayerTransparency, +# StencilMode, +# LayerType, +# TVPLayer, +# tv_instance_get_name, +# tv_instance_name, +# tv_instance_set_name, +# tv_layer_anim, +# tv_layer_auto_break_instance_get, +# tv_layer_auto_break_instance_set, +# tv_layer_auto_create_instance_get, +# tv_layer_auto_create_instance_set, +# tv_layer_blending_mode_get, +# tv_layer_blending_mode_set, +# tv_layer_collapse_get, +# tv_layer_collapse_set, +# tv_layer_color_get, +# tv_layer_color_get_color, +# tv_layer_color_hide, +# tv_layer_color_lock, +# tv_layer_color_select, +# tv_layer_color_set, +# tv_layer_color_set_color, +# tv_layer_color_show, +# tv_layer_color_unlock, +# tv_layer_color_unselect, +# tv_layer_color_visible, +# tv_layer_copy, +# tv_layer_create, +# tv_layer_current_id, +# tv_layer_cut, +# tv_layer_density_get, +# tv_layer_density_set, +# tv_layer_display_get, +# tv_layer_display_set, +# tv_layer_duplicate, +# tv_layer_get_id, +# tv_layer_get_pos, +# tv_layer_info, +# tv_layer_insert_image, +# tv_layer_kill, +# tv_layer_folder_delete, +# tv_layer_load_dependencies, +# tv_layer_lock_get, +# tv_layer_lock_position_get, +# tv_layer_lock_position_set, +# tv_layer_lock_set, +# tv_layer_mark_get, +# tv_layer_mark_set, +# tv_layer_merge, +# tv_layer_merge_all, +# tv_layer_move, +# tv_layer_paste, +# tv_layer_post_behavior_get, +# tv_layer_post_behavior_set, +# tv_layer_pre_behavior_get, +# tv_layer_pre_behavior_set, +# tv_layer_rename, +# tv_layer_select, +# tv_layer_selection_get, +# tv_layer_selection_set, +# tv_layer_set, +# tv_layer_shift, +# tv_layer_show_thumbnails_get, +# tv_layer_show_thumbnails_set, +# tv_layer_stencil_get, +# tv_layer_stencil_set, +# tv_load_image, +# tv_preserve_get, +# tv_preserve_set, +# tv_save_image, +# ) +# from pytvpaint.george.grg_project import TVPProject +# from pytvpaint.layer import Layer +# from tests.conftest import FixtureYield +# +# +# IS_NOT_TVP12 = not tv_version()[1].startswith('12') +# +# +# def test_tv_layer_current_id(test_project: TVPProject) -> None: +# assert tv_layer_current_id() +# +# +# def test_tv_layer_get_id(test_project: TVPProject) -> None: +# assert tv_layer_get_id(0) == tv_layer_current_id() +# +# +# def test_tv_layer_get_id_neg_pos_error(test_project: TVPProject) -> None: +# with pytest.raises(GeorgeError, match="No layer at provided position"): +# tv_layer_get_id(-1) +# +# +# def test_tv_layer_get_pos(test_project: TVPProject) -> None: +# assert tv_layer_get_pos(tv_layer_current_id()) == 0 +# +# +# def test_tv_layer_get_pos_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_get_pos(56) +# +# +# def test_tv_layer_info() -> None: +# info = tv_layer_info(tv_layer_current_id()) +# assert info.id +# +# +# def test_tv_layer_info_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_info(-4) +# +# +# def test_tv_layer_move(test_project: TVPProject) -> None: +# current_layer = tv_layer_current_id() +# total_layers = 10 +# +# for i in range(total_layers): +# tv_layer_create(f"layer_{i}") +# +# # Make the current layer the original one +# tv_layer_set(current_layer) +# +# for i in range(total_layers): +# new_pos = i + 1 +# tv_layer_move(new_pos) +# layer_info = tv_layer_info(current_layer) +# # Position starts at 1 +# assert layer_info.position + 1 == new_pos +# +# +# @pytest.mark.parametrize("pos", [-1, 2, 100, 1000]) +# def test_tv_layer_move_wrong_pos(test_project: TVPProject, pos: int) -> None: +# with pytest.raises( +# GeorgeError, +# match="Couldn't move current layer to position", +# ): +# tv_layer_move(pos) +# +# +# def test_tv_layer_set(test_project: TVPProject) -> None: +# layers = [tv_layer_create(f"layer_{i}") for i in range(5)] +# +# for layer in layers: +# tv_layer_set(layer) +# assert tv_layer_current_id() == layer +# +# +# def test_tv_layer_set_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_set(-16) +# +# +# def test_tv_layer_selection_get(test_layer: TVPLayer) -> None: +# assert not tv_layer_selection_get(test_layer.id) +# +# +# def test_tv_layer_selection_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_selection_get(-1) +# +# +# @pytest.mark.parametrize("selected", [True, False]) +# def test_tv_layer_selection_set(test_project: TVPProject, selected: bool) -> None: +# layers = [tv_layer_create(f"layer_{i}") for i in range(5)] +# +# for layer in layers: +# tv_layer_selection_set(layer, new_state=selected) +# assert tv_layer_info(layer).selected == selected +# +# +# @pytest.mark.parametrize("selected", [True, False]) +# def test_tv_layer_selection_set_wrong_id(selected: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_selection_set(-1, selected) +# +# +# def test_tv_layer_selection_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_selection_set(-16, True) +# +# +# def test_tv_layer_select_n(test_project: TVPProject, test_anim_layer: TVPLayer) -> None: +# end_frame = 10 +# +# # Draw something in order to select frames +# tv_layer_copy() +# tv_layer_image(end_frame) +# tv_layer_paste() +# tv_rect(0, 0, 200, 200) +# +# selected_frames = tv_layer_select(0, end_frame) +# assert selected_frames == end_frame +# +# +# LAYER_NAMES_TO_TEST = ["new_layer", "0", "new layer", ""] +# +# +# @pytest.mark.parametrize("name", LAYER_NAMES_TO_TEST) +# def test_tv_layer_create(test_project: TVPProject, name: str) -> None: +# new_layer = tv_layer_create(name) +# assert tv_layer_info(new_layer).name == name +# +# +# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +# @pytest.mark.parametrize("name", LAYER_NAMES_TO_TEST) +# def test_tv_layer_folder_create(test_project: TVPProject, name: str) -> None: +# new_layer = tv_layer_create(name, layer_type=0) +# assert tv_layer_info(new_layer).type == LayerType.FOLDER +# assert tv_layer_info(new_layer).name == name +# +# +# @pytest.mark.parametrize("new_name", LAYER_NAMES_TO_TEST) +# def test_tv_layer_duplicate(test_project: TVPProject, new_name: str) -> None: +# dup_layer_id = tv_layer_duplicate(new_name) +# assert tv_layer_current_id() == dup_layer_id +# assert tv_layer_info(tv_layer_current_id()).name == new_name +# +# +# @pytest.mark.parametrize("new_name", LAYER_NAMES_TO_TEST) +# def test_tv_layer_rename(test_layer: TVPLayer, new_name: str) -> None: +# tv_layer_rename(tv_layer_current_id(), new_name) +# +# +# def test_tv_layer_rename_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_rename(-1, "test") +# +# +# def test_tv_layer_kill(test_project: TVPProject) -> None: +# new_layer = tv_layer_create("destroy") +# tv_layer_kill(new_layer) +# +# # Layer shouldn't exist anymore +# with pytest.raises(NoObjectWithIdError): +# tv_layer_get_pos(new_layer) +# +# +# def test_tv_layer_kill_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_kill(-5) +# +# +# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +# @pytest.mark.parametrize("remove_children", (True, False)) +# def test_tv_layer_folder_delete(test_project: TVPProject, remove_children) -> None: +# new_layer = tv_layer_create("destroy", layer_type=0) +# tv_layer_folder_delete(new_layer, remove_children=remove_children) +# +# # Layer shouldn't exist anymore +# with pytest.raises(NoObjectWithIdError): +# tv_layer_get_pos(new_layer) +# +# +# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +# def test_tv_layer_folder_delete_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_folder_delete(-5, True) +# +# +# def test_tv_layer_density_get() -> None: +# assert 0 <= tv_layer_density_get() <= 100 +# +# +# @pytest.mark.parametrize("density", [0, 50, 100, 25, 150, -50]) +# def test_tv_layer_density_set(density: int) -> None: +# tv_layer_density_set(density) +# assert tv_layer_density_get() == min(max(0, density), 100) +# +# +# def test_tv_layer_display_get(test_layer: TVPLayer) -> None: +# current_layer = tv_layer_info(tv_layer_current_id()) +# assert current_layer.visibility == tv_layer_display_get(current_layer.id) +# +# +# def test_tv_layer_display_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_display_get(-1) +# +# +# @pytest.mark.parametrize("new_state", [True, False]) +# def test_tv_layer_display_set(test_layer: TVPLayer, new_state: bool) -> None: +# current_layer = tv_layer_info(tv_layer_current_id()) +# tv_layer_display_set(current_layer.id, new_state) +# assert tv_layer_info(current_layer.id).visibility == new_state +# +# +# def test_tv_layer_lock_get(test_layer: TVPLayer) -> None: +# tv_layer_lock_get(tv_layer_current_id()) +# +# +# def test_tv_layer_lock_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_lock_get(-1) +# +# +# @pytest.mark.parametrize("lock", [True, False]) +# def test_tv_layer_lock_set(test_layer: TVPLayer, lock: bool) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_lock_set(current_layer, lock) +# assert tv_layer_lock_get(current_layer) == lock +# +# +# @pytest.mark.parametrize("lock", [True, False]) +# def test_tv_layer_lock_set_wrong_id(lock: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_lock_set(-1, lock) +# +# +# def test_tv_layer_collapse_get() -> None: +# tv_layer_collapse_get(tv_layer_current_id()) +# +# +# def test_tv_layer_collapse_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_collapse_get(-1) +# +# +# @pytest.mark.parametrize("collapse", [True, False]) +# def test_tv_layer_collapse_set(test_layer: TVPLayer, collapse: bool) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_collapse_set(current_layer, collapse) +# assert tv_layer_collapse_get(current_layer) == collapse +# +# +# @pytest.mark.parametrize("collapse", [True, False]) +# def test_tv_layer_collapse_set_wrong_id(collapse: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_collapse_set(-1, collapse) +# +# +# def test_tv_layer_blending_mode_get() -> None: +# assert tv_layer_blending_mode_get(tv_layer_current_id()) in list(BlendingMode) +# +# +# def test_tv_layer_blending_mode_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_blending_mode_get(-1) +# +# +# @pytest.mark.parametrize("mode", BlendingMode) +# def test_tv_layer_blending_mode_set(test_layer: TVPLayer, mode: BlendingMode) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_blending_mode_set(current_layer, mode) +# assert tv_layer_blending_mode_get(current_layer) == mode +# +# +# def test_tv_layer_blending_mode_set_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_blending_mode_set(-1, BlendingMode.ADD) +# +# +# def test_tv_layer_stencil_get() -> None: +# tv_layer_stencil_get(tv_layer_current_id()) +# +# +# def test_tv_layer_stencil_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_stencil_get(-1) +# +# +# @pytest.mark.parametrize("mode", StencilMode) +# def test_tv_layer_stencil_set(test_layer: TVPLayer, mode: StencilMode) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_stencil_set(current_layer, mode) +# +# current_mode = tv_layer_stencil_get(current_layer) +# +# if mode == StencilMode.ON: +# assert current_mode == StencilMode.NORMAL +# else: +# assert current_mode == mode +# +# +# @pytest.mark.parametrize("mode", StencilMode) +# def test_tv_layer_stencil_set_wrong_id(mode: StencilMode) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_stencil_set(-1, mode) +# +# +# def test_tv_layer_show_thumbnails_get() -> None: +# tv_layer_show_thumbnails_get(tv_layer_current_id()) +# +# +# def test_tv_layer_show_thumbnails_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_show_thumbnails_get(-1) +# +# +# @pytest.mark.parametrize("state", [True, False]) +# def test_tv_layer_show_thumbnails_set(test_layer: TVPLayer, state: bool) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_show_thumbnails_set(current_layer, state) +# assert tv_layer_show_thumbnails_get(current_layer) == state +# +# +# @pytest.mark.parametrize("state", [True, False]) +# def test_tv_layer_show_thumbnails_set_wrong_id(state: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_show_thumbnails_set(-1, state) +# +# +# def test_tv_layer_auto_break_instance_get() -> None: +# tv_layer_auto_break_instance_get(tv_layer_current_id()) +# +# +# def test_tv_layer_auto_break_instance_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_auto_break_instance_get(-1) +# +# +# @pytest.mark.parametrize("state", [True, False]) +# def test_tv_layer_auto_break_instance_set( +# test_project: TVPProject, state: bool +# ) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_auto_break_instance_set(current_layer, state) +# assert tv_layer_auto_break_instance_get(current_layer) == state +# +# +# @pytest.mark.parametrize("state", [True, False]) +# def test_tv_layer_auto_break_instance_set_wrong_id(state: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_auto_break_instance_set(-1, state) +# +# +# def test_tv_layer_auto_create_instance_get() -> None: +# tv_layer_auto_create_instance_get(tv_layer_current_id()) +# +# +# def test_tv_layer_auto_create_instance_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_auto_create_instance_get(-1) +# +# +# @pytest.mark.parametrize("state", [True, False]) +# def test_tv_layer_auto_create_instance_set(test_layer: TVPLayer, state: bool) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_auto_create_instance_set(current_layer, state) +# assert tv_layer_auto_create_instance_get(current_layer) == state +# +# +# @pytest.mark.parametrize("state", [True, False]) +# def test_tv_layer_auto_create_instance_set_wrong_id(state: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_auto_create_instance_set(-1, state) +# +# +# def test_tv_layer_pre_behavior_get() -> None: +# tv_layer_pre_behavior_get(tv_layer_current_id()) +# +# +# def test_tv_layer_pre_behavior_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_pre_behavior_get(-1) +# +# +# @pytest.mark.parametrize("behavior", LayerBehavior) +# def test_tv_layer_pre_behavior_set( +# test_layer: TVPLayer, behavior: LayerBehavior +# ) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_pre_behavior_set(current_layer, behavior) +# assert tv_layer_pre_behavior_get(current_layer) == behavior +# +# +# @pytest.mark.parametrize("behavior", LayerBehavior) +# def test_tv_layer_pre_behavior_set_wrong_id(behavior: LayerBehavior) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_pre_behavior_set(-1, behavior) +# +# +# def test_tv_layer_post_behavior_get() -> None: +# tv_layer_post_behavior_get(tv_layer_current_id()) +# +# +# def test_tv_layer_post_behavior_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_post_behavior_get(-1) +# +# +# @pytest.mark.parametrize("behavior", LayerBehavior) +# def test_tv_layer_post_behavior_set( +# test_layer: TVPLayer, behavior: LayerBehavior +# ) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_post_behavior_set(current_layer, behavior) +# assert tv_layer_post_behavior_get(current_layer) == behavior +# +# +# @pytest.mark.parametrize("behavior", LayerBehavior) +# def test_tv_layer_post_behavior_set_wrong_id(behavior: LayerBehavior) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_post_behavior_set(-1, behavior) +# +# +# def test_tv_layer_lock_position_get() -> None: +# tv_layer_lock_position_get(tv_layer_current_id()) +# +# +# def test_tv_layer_lock_position_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_lock_position_get(-1) +# +# +# @pytest.mark.parametrize("state", [True, False]) +# def test_tv_layer_lock_position_set(test_layer: TVPLayer, state: bool) -> None: +# current_layer = tv_layer_current_id() +# tv_layer_lock_position_set(current_layer, state) +# assert tv_layer_lock_position_get(current_layer) == state +# +# +# @pytest.mark.parametrize("state", [True, False]) +# def test_tv_layer_lock_position_set_wrong_id(state: bool) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_lock_position_set(-1, state) +# +# +# def test_tv_preserve_get() -> None: +# tv_preserve_get() +# +# +# @pytest.mark.parametrize("state", LayerTransparency) +# def test_tv_preserve_set(test_layer: TVPLayer, state: LayerTransparency) -> None: +# tv_preserve_set(state) +# new_state = tv_preserve_get() +# +# transparency_map = { +# LayerTransparency.MINUS_1: LayerTransparency.ON, +# LayerTransparency.NONE: LayerTransparency.OFF, +# } +# +# assert transparency_map.get(state, state) == new_state +# +# +# def test_tv_layer_mark_get() -> None: +# tv_layer_mark_get(tv_layer_current_id(), 0) +# +# +# def test_tv_layer_mark_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_mark_get(-1, 0) +# +# +# @pytest.mark.parametrize("mark", range(27)) +# def test_tv_layer_mark_set(test_anim_layer: TVPLayer, mark: int) -> None: +# tv_layer_mark_set(test_anim_layer.id, 0, mark) +# assert tv_layer_mark_get(test_anim_layer.id, 0) == mark +# +# +# def test_tv_layer_mark_set_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_mark_set(-1, 0, 0) +# +# +# def test_tv_layer_anim(test_layer: TVPLayer) -> None: +# tv_layer_anim(test_layer.id) +# +# assert tv_layer_auto_break_instance_get(test_layer.id) +# assert tv_layer_auto_create_instance_get(test_layer.id) +# assert not tv_layer_lock_get(test_layer.id) +# +# +# def test_tv_layer_load_dependencies() -> None: +# tv_layer_load_dependencies(tv_layer_current_id()) +# +# +# @pytest.mark.parametrize("color_index", range(1, 27)) +# def test_tv_layer_color_get_color(color_index: int) -> None: +# current_clip = tv_clip_current_id() +# color = tv_layer_color_get_color(current_clip, color_index) +# assert color.clip_id == current_clip +# assert color.color_index == color_index +# +# +# def test_tv_layer_color_get_color_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_color_get_color(-1, 0) +# +# +# # We skip index 0 because it's the "Default" color and can't be changed +# @pytest.mark.parametrize("color_index", range(1, 27)) +# @pytest.mark.parametrize("name", [None, "test"]) +# @pytest.mark.parametrize("rgb", [RGBColor(255, 0, 0), RGBColor(0, 255, 0)]) +# def test_tv_layer_color_set_color( +# test_clip: TVPClip, color_index: int, name: str | None, rgb: RGBColor +# ) -> None: +# tv_layer_color_set_color(test_clip.id, color_index, rgb, name) +# +# color = tv_layer_color_get_color(test_clip.id, color_index) +# +# assert color.color_index == color_index +# assert color.color_r == rgb.r +# assert color.color_g == rgb.g +# assert color.color_b == rgb.b +# assert color.clip_id == test_clip.id +# +# if name is not None: +# assert color.name == name +# +# +# def test_tv_layer_color_set_color_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_color_set_color(-1, 1, RGBColor(0, 0, 0)) +# +# +# def test_tv_layer_color_get(test_layer: TVPLayer) -> None: +# index = tv_layer_color_get(test_layer.id) +# assert 0 <= index <= 26 +# +# +# def test_tv_layer_color_get_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_layer_color_get(-1) +# +# +# @pytest.mark.parametrize("color_index", range(27)) +# def test_tv_layer_color_set_s(test_layer: TVPLayer, color_index: int) -> None: +# tv_layer_color_set(test_layer.id, color_index) +# assert tv_layer_color_get(test_layer.id) == color_index +# +# +# @pytest.fixture +# def layers_with_colors(test_project: TVPProject) -> FixtureYield[tuple[int, list[int]]]: +# """ +# Fixture that create some layers with a color +# """ +# color_index = 5 +# layers = [tv_layer_create(f"layer_{i}") for i in range(10)] +# color_layers = [layer for i, layer in enumerate(layers) if i % 2 == 0] +# +# # Set the color of all layers +# for layer in color_layers: +# tv_layer_color_set(layer, color_index) +# +# yield color_index, color_layers +# +# +# def test_tv_layer_color_lock(layers_with_colors: tuple[int, list[int]]) -> None: +# color_index, layers_to_lock = layers_with_colors +# assert tv_layer_color_lock(color_index) == len(layers_to_lock) +# assert all(tv_layer_lock_get(layer) for layer in layers_to_lock) +# +# +# def test_tv_layer_color_unlock(layers_with_colors: tuple[int, list[int]]) -> None: +# color_index, layers_to_lock = layers_with_colors +# +# for layer in layers_to_lock: +# tv_layer_lock_set(layer, True) +# +# assert tv_layer_color_unlock(color_index) == len(layers_to_lock) +# assert all(not tv_layer_lock_get(layer) for layer in layers_to_lock) +# +# +# @pytest.mark.parametrize("display", LayerColorDisplayOpt) +# def test_tv_layer_color_show( +# layers_with_colors: tuple[int, list[int]], display: LayerColorDisplayOpt +# ) -> None: +# color_index, layers_to_show = layers_with_colors +# +# is_display = display == LayerColorDisplayOpt.DISPLAY +# +# for layer in layers_to_show: +# if is_display: +# tv_layer_display_set(layer, False) +# else: +# tv_layer_collapse_set(layer, True) +# +# tv_layer_color_show(display, color_index) +# +# check_fn = tv_layer_display_get if is_display else tv_layer_collapse_get +# +# assert all(check_fn(layer) for layer in layers_to_show) +# +# +# @pytest.mark.parametrize("display", LayerColorDisplayOpt) +# def test_tv_layer_color_hide( +# layers_with_colors: tuple[int, list[int]], display: LayerColorDisplayOpt +# ) -> None: +# color_index, layers_to_hide = layers_with_colors +# +# tv_layer_color_hide(display, color_index) +# +# check_fn = ( +# tv_layer_display_get +# if display == LayerColorDisplayOpt.DISPLAY +# else tv_layer_collapse_get +# ) +# +# assert all(not check_fn(layer) for layer in layers_to_hide) +# +# +# @pytest.mark.parametrize("color_index", range(27)) +# def test_tv_layer_color_visible(color_index: int) -> None: +# # It seems that there's no way to set the visibility of a layer color group +# # So we only test that all groups are visible +# assert tv_layer_color_visible(color_index) +# +# +# def test_tv_layer_color_select(layers_with_colors: tuple[int, list[int]]) -> None: +# color_index, layers_to_select = layers_with_colors +# +# tv_layer_color_select(color_index) +# assert all(tv_layer_selection_get(layer) for layer in layers_to_select) +# +# +# def test_tv_layer_color_unselect(layers_with_colors: tuple[int, list[int]]) -> None: +# color_index, layers_to_select = layers_with_colors +# +# for layer in layers_to_select: +# tv_layer_selection_set(layer, True) +# +# tv_layer_color_unselect(color_index) +# assert all(not tv_layer_selection_get(layer) for layer in layers_to_select) +# +# +# def can_be_parsed_as_int(value: str) -> bool: +# if " " in value: +# return False +# +# try: +# int(value) +# except ValueError: +# return False +# +# 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"]) +# @pytest.mark.parametrize("process", [None, *InstanceNamingProcess]) +# @pytest.mark.parametrize("initial_name", ["", "5", " 7", "fest", "test_3"]) +# def test_tv_instance_name( +# test_layer: TVPLayer, +# mode: InstanceNamingMode, +# prefix: str | None, +# suffix: str | None, +# process: InstanceNamingProcess | None, +# initial_name: str, +# ) -> None: +# instance = 0 +# +# # Assign an initial name to the instance +# tv_instance_set_name(test_layer.id, instance, name=initial_name) +# +# # Rename all instances +# tv_instance_name(test_layer.id, mode, prefix, suffix, process) +# +# if mode == InstanceNamingMode.ALL: +# expected_name = " " + str(instance + 1) +# if prefix: +# expected_name = prefix + expected_name +# else: # SMART mode +# no_prefix = prefix is None +# no_suffix = suffix is None +# +# cond_text = ( +# len(initial_name) +# and process == InstanceNamingProcess.TEXT +# and (can_be_parsed_as_int(initial_name)) +# ) +# +# cond_empty = ( +# len(initial_name) +# and process == InstanceNamingProcess.EMPTY +# and (no_prefix != no_suffix and not can_be_parsed_as_int(initial_name)) +# ) +# +# cond_number = ( +# len(initial_name) +# and process == InstanceNamingProcess.NUMBER +# and not (no_prefix == no_suffix and not can_be_parsed_as_int(initial_name)) +# and not can_be_parsed_as_int(initial_name) +# ) +# +# if cond_text or cond_empty or cond_number: +# expected_name = initial_name +# else: +# expected_name = str(instance + 1) +# +# if suffix: +# expected_name += suffix +# +# if prefix: +# expected_name = prefix + expected_name +# +# assert expected_name == tv_instance_get_name(test_layer.id, instance) +# +# +# @pytest.mark.skip("Crashed TVPaint") +# @pytest.mark.parametrize("mode", InstanceNamingMode) +# def test_tv_instance_name_wrong_id(mode: InstanceNamingMode) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_instance_name(-1, mode) +# +# +# def test_tv_instance_get_name(test_layer: TVPLayer) -> None: +# # By default there's an instance at frame zero +# tv_instance_get_name(test_layer.id, 0) +# +# +# def test_tv_instance_get_name_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_instance_get_name(-1, 0) +# +# +# @pytest.mark.parametrize("name", ["", "5", " 7", "fest", "test_3"]) +# def test_tv_instance_set_name(test_layer: TVPLayer, name: str) -> None: +# tv_instance_set_name(test_layer.id, 0, name) +# assert tv_instance_get_name(test_layer.id, 0) == name +# +# +# def test_tv_instance_set_name_wrong_id() -> None: +# with pytest.raises(GeorgeError): +# tv_instance_set_name(-1, 0, "test") +# +# +# def instance_exists(frame: int) -> bool: +# try: +# tv_instance_get_name(tv_layer_current_id(), frame) +# except NoObjectWithIdError: +# return False +# return True +# +# +# @pytest.mark.parametrize("frame", range(5)) +# def test_tv_layer_copy_paste(test_anim_layer: TVPLayer, frame: int) -> None: +# tv_layer_image(0) +# tv_layer_copy() +# tv_layer_image(frame) +# tv_layer_paste() +# +# assert instance_exists(frame) +# +# +# @pytest.mark.parametrize("frame", range(1, 6)) +# def test_tv_layer_cut_paste(test_anim_layer: TVPLayer, frame: int) -> None: +# cut_frame = 10 +# +# # Add another instance because if we cut the first it deletes the layer +# tv_layer_copy() +# tv_layer_image(cut_frame) +# tv_layer_paste() +# +# # Cut that instance +# tv_layer_image(cut_frame) +# tv_layer_cut() +# +# # The instance should be removed +# assert not instance_exists(cut_frame) +# +# # Paste at another frame +# tv_layer_image(frame) +# tv_layer_paste() +# +# assert instance_exists(frame) +# +# +# def test_tv_layer_insert_image_duplicate(test_anim_layer: TVPLayer) -> None: +# initial_frame = 5 +# +# # Copy the instance in the middle +# tv_layer_image(0) +# tv_layer_copy() +# tv_layer_image(initial_frame) +# tv_layer_paste() +# +# tv_layer_insert_image(duplicate=True) +# assert instance_exists(initial_frame + 1) +# +# +# @pytest.mark.parametrize("count", range(1, 8)) +# @pytest.mark.parametrize("direction", InsertDirection) +# def test_tv_layer_insert_image( +# test_anim_layer: TVPLayer, count: int, direction: InsertDirection +# ) -> None: +# initial_frame = 10 +# +# # Copy the instance in the middle +# tv_layer_image(0) +# tv_layer_copy() +# tv_layer_image(initial_frame) +# tv_layer_paste() +# +# tv_layer_insert_image(count, direction) +# +# offset = 0 +# while offset < count: +# if direction == InsertDirection.AFTER: +# next_frame = initial_frame + offset +# else: +# next_frame = initial_frame - offset +# +# assert instance_exists(next_frame) +# offset += 1 +# +# +# @pytest.mark.parametrize("start", [0, 5, 10, 100]) +# def test_tv_layer_shift(test_layer: TVPLayer, start: int) -> None: +# tv_layer_shift(test_layer.id, start) +# +# +# @pytest.mark.parametrize("blending", BlendingMode) +# @pytest.mark.parametrize("stamp", [True, False]) +# def test_tv_layer_merge( +# create_some_layers: list[Layer], blending: BlendingMode, stamp: bool +# ) -> None: +# tv_layer_merge(create_some_layers[0].id, blending, stamp) +# +# +# @pytest.mark.parametrize("keep_color_grp", [False, True]) +# @pytest.mark.parametrize("keep_img_mark", [False, True]) +# @pytest.mark.parametrize("keep_instance_name", [False, True]) +# def test_tv_layer_merge_all( +# create_some_layers: list[Layer], +# keep_color_grp: bool, +# keep_img_mark: bool, +# keep_instance_name: bool, +# ) -> None: +# tv_layer_merge_all(keep_color_grp, keep_img_mark, keep_instance_name) +# +# +# def test_tv_save_image(tmp_path: Path) -> None: +# save_ext, _ = tv_save_mode_get() +# ext = "jpg" if save_ext == SaveFormat.JPG else save_ext.value +# out_img = (tmp_path / "out").with_suffix("." + ext) +# tv_save_image(out_img) +# assert out_img.exists() +# +# +# @pytest.mark.parametrize("stretch", [False, True]) +# def test_tv_load_image( +# test_clip: TVPClip, +# test_layer: TVPLayer, +# ppm_sequence: list[Path], +# stretch: bool, +# ) -> None: +# tv_load_image(ppm_sequence[0], stretch) +# # Verify that there's an instance frame +# assert tv_instance_get_name(test_layer.id, 0) == "" +# +# +# @pytest.mark.skip("Will block the UI") +# def test_tv_load_image_file_does_not_exist(tmp_path: Path) -> None: +# with pytest.raises(FileNotFoundError): +# tv_load_image(tmp_path / "file.png") diff --git a/tests/george/test_grg_project.py b/tests/george/test_grg_project.py index b0491c7..9d8b500 100644 --- a/tests/george/test_grg_project.py +++ b/tests/george/test_grg_project.py @@ -1,573 +1,573 @@ -from __future__ import annotations - -import itertools -from pathlib import Path -from typing import Any - -import pytest - -from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError -from pytvpaint.george.grg_base import ( - FieldOrder, - ResizeOption, - RGBColor, - SaveFormat, - tv_save_mode_get, - tv_save_mode_set, -) -from pytvpaint.george.grg_camera import tv_camera_insert_point -from pytvpaint.george.grg_clip import ( - tv_clip_current_id, - tv_clip_info, - tv_load_sequence, -) -from pytvpaint.george.grg_project import ( - BackgroundMode, - TVPProject, - tv_background_get, - tv_background_set, - tv_frame_rate_get, - tv_frame_rate_set, - tv_get_field, - tv_get_height, - tv_get_width, - tv_load_palette, - tv_load_project, - tv_project_close, - tv_project_current_frame_get, - tv_project_current_frame_set, - tv_project_current_id, - tv_project_duplicate, - tv_project_enum_id, - tv_project_header_author_get, - tv_project_header_author_set, - tv_project_header_info_get, - tv_project_header_info_set, - tv_project_header_notes_get, - tv_project_header_notes_set, - tv_project_info, - tv_project_new, - tv_project_render_camera, - tv_project_save_audio_dependencies, - tv_project_save_sequence, - tv_project_save_video_dependencies, - tv_project_select, - tv_ratio, - tv_resize_page, - tv_resize_project, - tv_save_palette, - tv_save_project, - tv_sound_project_adjust, - tv_sound_project_info, - tv_sound_project_new, - tv_sound_project_reload, - tv_sound_project_remove, - tv_start_frame_get, - tv_start_frame_set, -) -from tests.conftest import FixtureYield - -COLORS = [RGBColor(255, 0, 0), RGBColor(0, 255, 0), RGBColor(0, 0, 255)] - - -@pytest.mark.parametrize( - "mode, colors", - [ - *[(BackgroundMode.COLOR, c) for c in COLORS], - *[(BackgroundMode.CHECK, cs) for cs in itertools.combinations(COLORS, 2)], - (BackgroundMode.NONE, None), - ], -) -def test_tv_background( - mode: BackgroundMode, colors: tuple[RGBColor, RGBColor] | RGBColor | None -) -> None: - tv_background_set(mode, colors) - - current_mode, current_colors = tv_background_get() - - assert mode == current_mode - assert current_colors == colors - - # reset color - tv_background_set(BackgroundMode.COLOR, RGBColor(255, 255, 255)) - - -@pytest.mark.parametrize("width", [500, 1920]) -@pytest.mark.parametrize("height", [500, 1080]) -@pytest.mark.parametrize("pixel_aspect_ratio", [1.0, 2.0, 10.0]) -@pytest.mark.parametrize("frame_rate", [24.0, 12.0]) -@pytest.mark.parametrize("field_order", FieldOrder) -@pytest.mark.parametrize("start_frame", [1, 50]) -def test_tv_project_new( - tmp_path: Path, - cleanup_current_project: None, - width: int, - height: int, - pixel_aspect_ratio: float, - frame_rate: float, - field_order: FieldOrder, - start_frame: int, -) -> None: - project_path = tmp_path / "project.tvpp" - tv_project_new( - project_path, - width, - height, - pixel_aspect_ratio, - frame_rate, - field_order, - start_frame, - ) - - project = tv_project_info(tv_project_current_id()) - assert Path(project.path) == project_path - assert project.width == width - assert project.height == height - assert project.field_order == field_order - assert project.start_frame == start_frame - - -@pytest.fixture -def other_saved_project(tmp_path: Path) -> FixtureYield[TVPProject]: - other_project_path = tmp_path / "other.tvpp" - other_project_id = tv_project_new(other_project_path, width=200, height=200) - other_project = tv_project_info(other_project_id) - - tv_save_project(other_project.path) - tv_project_close(other_project.id) - - yield other_project - - -def test_tv_load_project( - other_saved_project: TVPProject, cleanup_current_project: None -) -> None: - # Load the project - pid = tv_load_project(other_saved_project.path) - assert tv_project_info(pid) == other_saved_project - - -def test_tv_load_project_wrong_path(tmp_path: Path) -> None: - with pytest.raises(FileNotFoundError): - tv_load_project(tmp_path / "folder" / "project.tvpp") - - -@pytest.mark.parametrize("ext", [".tvpp", ".abc", ".tvpx"]) -def test_tv_save_project(test_project: TVPProject, tmp_path: Path, ext: str) -> None: - project_path = (tmp_path / "save").with_suffix(ext) - tv_save_project(project_path) - assert project_path.with_suffix(".tvpp").exists() - - -def test_tv_save_project_wrong_path(test_project: TVPProject, tmp_path: Path) -> None: - with pytest.raises(ValueError): - tv_save_project(tmp_path / "folder" / "project.tvpp") - - -def projects_equal(p1: TVPProject, p2: TVPProject) -> bool: - """Compares two project omitting 'id' and 'path' attributes""" - from dataclasses import asdict - - p1_dict = asdict(p1) - p2_dict = asdict(p2) - - 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, -) -> None: - tv_project_duplicate() - dup_project = tv_project_info(tv_project_current_id()) - assert projects_equal(test_project, dup_project) - - -def test_tv_project_enum_id() -> None: - assert tv_project_enum_id(0) == tv_project_current_id() - - -@pytest.mark.parametrize("pos", [-1, 100, 58]) -def test_tv_project_enum_id_wrong_pos(pos: int) -> None: - with pytest.raises(GeorgeError): - tv_project_enum_id(pos) - - -def test_tv_project_current_id(test_project: TVPProject) -> None: - assert tv_project_current_id() == test_project.id - - -def test_tv_project_info(test_project: TVPProject) -> None: - assert tv_project_info(test_project.id) == test_project - - -@pytest.mark.skip("Doesn't return an empty string") -def test_tv_project_info_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_project_info("unknown") - - -def test_tv_project_select(test_project: TVPProject, tmp_path: Path) -> None: - other = tv_project_new(tmp_path / "other.tvpp") - assert tv_project_current_id() == other - - tv_project_select(test_project.id) - assert tv_project_current_id() == test_project.id - tv_project_close(other) - - -def test_tv_project_close(tmp_path: Path) -> None: - pid = tv_project_new(tmp_path / "close.tvpp") - tv_project_close(pid) - - with pytest.raises(NoObjectWithIdError): - tv_project_info(pid) - - -def get_project_pos(pid: str) -> int: - """Return the given project position""" - pos = 0 - while True: - try: - if tv_project_enum_id(pos) == pid: - return pos - except GeorgeError as err: - raise ValueError("Project does not exist") from err - pos += 1 - - -@pytest.mark.parametrize( - "res", - [(200, 200), (1000, 100), (0, 500)], -) -def test_tv_resize_project( - test_project: TVPProject, - res: tuple[int, int], - cleanup_current_project: None, -) -> None: - current_pos = get_project_pos(test_project.id) - - width, height = res - tv_resize_project(width, height) - - resized_project_id = tv_project_enum_id(current_pos) - resized_project = tv_project_info(resized_project_id) - - assert resized_project.width == width - assert resized_project.height == height - - -@pytest.mark.parametrize( - "res", - [(200, 200), (1000, 100), (0, 500)], -) -@pytest.mark.parametrize("resize", ResizeOption) -def test_tv_resize_page( - test_project: TVPProject, - res: tuple[int, int], - resize: ResizeOption, - cleanup_current_project: None, -) -> None: - current_pos = get_project_pos(test_project.id) - - width, height = res - tv_resize_page(width, height, resize) - - resized_id = tv_project_enum_id(current_pos) - resized = tv_project_info(resized_id) - - assert resized.width == width - assert resized.height == height - - -def test_tv_get_width(test_project: TVPProject) -> None: - assert tv_get_width() == test_project.width - - -def test_tv_get_height(test_project: TVPProject) -> None: - assert tv_get_height() == test_project.height - - -@pytest.mark.skip("Does not work, returns empty string") -def test_tv_ratio(test_project: TVPProject) -> None: - assert tv_ratio() == test_project.pixel_aspect_ratio - - -def test_tv_get_field(test_project: TVPProject) -> None: - assert tv_get_field() == test_project.field_order - - -@pytest.mark.parametrize("use_camera", [False, True]) -@pytest.mark.parametrize("start, end", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)]) -def test_tv_project_save_sequence( - test_project: TVPProject, - tmp_path: Path, - ppm_sequence: list[Path], - use_camera: bool, - start: int | None, - end: int | None, -) -> None: - tv_load_sequence(ppm_sequence[0]) - - if use_camera: - tv_camera_insert_point(0, 50, 50, 0, 1) - - out_sequence = tmp_path / "out" - tv_project_save_sequence(out_sequence, use_camera, start, end) - - clip = tv_clip_info(tv_clip_current_id()) - save_ext, _ = tv_save_mode_get() - start_end = ( - (start, end) - if (start is not None and end is not None) - else (clip.first_frame, clip.last_frame) - ) - - tv_save_mode_set(SaveFormat.JPG) - - for i in range(start_end[1] - start_end[0]): - image_name = f"{out_sequence.name}{i:05d}" - image_ext = "." + ("jpg" if save_ext == SaveFormat.JPG else save_ext.value) - image_path = out_sequence.with_name(f"{image_name}{image_ext}") - assert image_path.exists() - - -def test_tv_project_render_camera( - test_project: TVPProject, - ppm_sequence: list[Path], - cleanup_current_project: None, -) -> None: - tv_load_sequence(ppm_sequence[0]) - - tv_camera_insert_point(0, 50, 50, 0, 1) - tv_camera_insert_point(1, 100, 50, 0, 0.5) - - new_project_id = tv_project_render_camera(test_project.id) - assert projects_equal(tv_project_info(new_project_id), test_project) - - -def test_tv_frame_rate_get(test_project: TVPProject) -> None: - project_frame_rate, playback_frame_rate = tv_frame_rate_get() - assert test_project.frame_rate == playback_frame_rate - assert test_project.frame_rate == project_frame_rate - - -@pytest.mark.parametrize("frame_rate", [1, 25, 50, 100]) -def test_tv_frame_rate_set_project(test_project: TVPProject, frame_rate: float) -> None: - tv_frame_rate_set(frame_rate) - project_frame_rate, _ = tv_frame_rate_get() - assert project_frame_rate == frame_rate - - -@pytest.mark.parametrize("frame_rate", [1, 25, 50, 100]) -def test_tv_frame_rate_set_preview(test_project: TVPProject, frame_rate: float) -> None: - tv_frame_rate_set(frame_rate, preview=True) - _, preview_frame_rate = tv_frame_rate_get() - assert preview_frame_rate == frame_rate - - -def test_tv_project_current_frame_get(test_project: TVPProject) -> None: - assert tv_project_current_frame_get() == 0 - - -@pytest.mark.parametrize("frame", [1, 25, 50, 100]) -def test_tv_project_current_frame_set(test_project: TVPProject, frame: int) -> None: - tv_project_current_frame_set(frame) - assert tv_project_current_frame_get() == frame - - -def test_tv_load_palette(tmp_path: Path) -> None: - palette_path = tmp_path / "palette.tvpx" - tv_save_palette(palette_path) - tv_load_palette(palette_path) - - -def test_tv_load_palette_wrong_path(tmp_path: Path) -> None: - with pytest.raises(FileNotFoundError): - tv_load_palette(tmp_path / "palette.tvpx") - - -def test_tv_save_palette_wrong_path(tmp_path: Path) -> None: - with pytest.raises(NotADirectoryError): - tv_save_palette(tmp_path / "out" / "palette.tvpx") - - -def test_tv_project_save_video_dependencies(test_project: TVPProject) -> None: - with pytest.raises(GeorgeError): - tv_project_save_video_dependencies("") - - assert tv_project_save_video_dependencies(test_project.id) == 0 - assert tv_project_save_video_dependencies(test_project.id, True, False) == 1 - assert tv_project_save_video_dependencies(test_project.id, True, True) == 1 - assert tv_project_save_video_dependencies(test_project.id, False, True) == 1 - - -def test_tv_project_save_audio_dependencies(test_project: TVPProject) -> None: - with pytest.raises(GeorgeError): - tv_project_save_audio_dependencies("") - - assert tv_project_save_audio_dependencies(test_project.id) == 0 - assert tv_project_save_audio_dependencies(test_project.id, True) == 1 - assert tv_project_save_audio_dependencies(test_project.id, False) == 1 - - -def test_tv_sound_project_info(test_project: TVPProject, wav_file: Path) -> None: - tv_sound_project_new(wav_file) - tv_sound_project_info(test_project.id, 0) - - -def test_tv_sound_project_info_wrong_project_id() -> None: - with pytest.raises(GeorgeError): - tv_sound_project_info("lo", 0) - - -def test_tv_sound_project_info_wrong_track_index(test_project: TVPProject) -> None: - with pytest.raises(GeorgeError): - tv_sound_project_info(test_project.id, 0) - - -def test_tv_sound_project_new_wrong_path(tmp_path: Path) -> None: - with pytest.raises(ValueError): - tv_sound_project_new(tmp_path / "sound.wav") - - -def test_tv_sound_project_remove(test_project: TVPProject, wav_file: Path) -> None: - tv_sound_project_new(wav_file) - tv_sound_project_info(test_project.id, 0) - tv_sound_project_remove(0) - - with pytest.raises(GeorgeError): - tv_sound_project_info(test_project.id, 0) - - -def test_tv_sound_project_remove_wrong_track_index(test_project: TVPProject) -> None: - with pytest.raises(GeorgeError): - tv_sound_project_remove(0) - - -def test_tv_sound_project_reload(test_project: TVPProject, wav_file: Path) -> None: - tv_sound_project_new(wav_file) - tv_sound_project_reload(test_project.id, 0) - - -def test_tv_sound_project_reload_wrong_project_id() -> None: - with pytest.raises(GeorgeError): - tv_sound_project_reload("lo", 0) - - -def test_tv_sound_project_reload_wrong_track_index(test_project: TVPProject) -> None: - with pytest.raises(GeorgeError): - tv_sound_project_reload(test_project.id, 0) - - -@pytest.mark.parametrize( - "args", - [ - (True, 5), - (False, 10), - (True,), - (True, 2, 5), - (False, 1.5, 0, 1, 4, 4, 4), - ], -) -def test_tv_sound_project_adjust( - test_project: TVPProject, - wav_file: Path, - args: tuple[Any, ...], -) -> None: - tv_sound_project_new(wav_file) - tv_sound_project_adjust(0, *args) - - attrs_check = [ - "mute", - "volume", - "offset", - "fade_in_start", - "fade_in_stop", - "fade_out_start", - "fade_out_stop", - "color_index", - ] - - sound = tv_sound_project_info(test_project.id, 0) - for attr, arg in zip(attrs_check, args): - current = getattr(sound, attr) - err_msg = f"Error checking {attr} (expected: {arg}, current: {current})" - assert current == arg, err_msg - - -def test_tv_project_header_info_get(test_project: TVPProject) -> None: - assert tv_project_header_info_get(test_project.id) == "" - - -def test_tv_project_header_info_get_wrong_id(test_project: TVPProject) -> None: - with pytest.raises(NoObjectWithIdError): - tv_project_header_info_get("ll") - - -@pytest.mark.parametrize("header", ["", "Hello", "THis is a project header"]) -def test_tv_project_header_info_set(test_project: TVPProject, header: str) -> None: - tv_project_header_info_set(test_project.id, header) - assert tv_project_header_info_get(test_project.id) == header - - -def test_tv_project_header_info_set_wrong_id(test_project: TVPProject) -> None: - with pytest.raises(NoObjectWithIdError): - tv_project_header_info_set("ll", "header") - - -def test_tv_project_header_author_get(test_project: TVPProject) -> None: - import getpass - - assert tv_project_header_author_get(test_project.id) == getpass.getuser() - - -def test_tv_project_header_author_get_wrong_id(test_project: TVPProject) -> None: - with pytest.raises(NoObjectWithIdError): - tv_project_header_author_get("ll") - - -@pytest.mark.parametrize("author", ["l", "Hello", "THis is a project author"]) -def test_tv_project_header_author_set(test_project: TVPProject, author: str) -> None: - tv_project_header_author_set(test_project.id, author) - assert tv_project_header_author_get(test_project.id) == author - - -def test_tv_project_header_author_set_wrong_id(test_project: TVPProject) -> None: - with pytest.raises(NoObjectWithIdError): - tv_project_header_author_set("ll", "header") - - -def test_tv_project_header_notes_get(test_project: TVPProject) -> None: - assert tv_project_header_notes_get(test_project.id) == "" - - -def test_tv_project_header_notes_get_wrong_id(test_project: TVPProject) -> None: - with pytest.raises(NoObjectWithIdError): - tv_project_header_notes_get("ll") - - -@pytest.mark.parametrize("notes", ["l", "Hello", "THis is a project notes"]) -def test_tv_project_header_notes_set(test_project: TVPProject, notes: str) -> None: - tv_project_header_notes_set(test_project.id, notes) - assert tv_project_header_notes_get(test_project.id) == notes - - -def test_tv_project_header_notes_set_wrong_id(test_project: TVPProject) -> None: - with pytest.raises(NoObjectWithIdError): - tv_project_header_notes_set("ll", "header") - - -def test_tv_start_frame_get(test_project: TVPProject) -> None: - tv_start_frame_set(12) - assert tv_start_frame_get() == 12 - - -@pytest.mark.parametrize("start", [0, 1, 50, 100]) -def test_tv_start_frame_set(test_project: TVPProject, start: int) -> None: - tv_start_frame_set(start) - assert tv_start_frame_get() == start +# from __future__ import annotations +# +# import itertools +# from pathlib import Path +# from typing import Any +# +# import pytest +# +# from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError +# from pytvpaint.george.grg_base import ( +# FieldOrder, +# ResizeOption, +# RGBColor, +# SaveFormat, +# tv_save_mode_get, +# tv_save_mode_set, +# ) +# from pytvpaint.george.grg_camera import tv_camera_insert_point +# from pytvpaint.george.grg_clip import ( +# tv_clip_current_id, +# tv_clip_info, +# tv_load_sequence, +# ) +# from pytvpaint.george.grg_project import ( +# BackgroundMode, +# TVPProject, +# tv_background_get, +# tv_background_set, +# tv_frame_rate_get, +# tv_frame_rate_set, +# tv_get_field, +# tv_get_height, +# tv_get_width, +# tv_load_palette, +# tv_load_project, +# tv_project_close, +# tv_project_current_frame_get, +# tv_project_current_frame_set, +# tv_project_current_id, +# tv_project_duplicate, +# tv_project_enum_id, +# tv_project_header_author_get, +# tv_project_header_author_set, +# tv_project_header_info_get, +# tv_project_header_info_set, +# tv_project_header_notes_get, +# tv_project_header_notes_set, +# tv_project_info, +# tv_project_new, +# tv_project_render_camera, +# tv_project_save_audio_dependencies, +# tv_project_save_sequence, +# tv_project_save_video_dependencies, +# tv_project_select, +# tv_ratio, +# tv_resize_page, +# tv_resize_project, +# tv_save_palette, +# tv_save_project, +# tv_sound_project_adjust, +# tv_sound_project_info, +# tv_sound_project_new, +# tv_sound_project_reload, +# tv_sound_project_remove, +# tv_start_frame_get, +# tv_start_frame_set, +# ) +# from tests.conftest import FixtureYield +# +# COLORS = [RGBColor(255, 0, 0), RGBColor(0, 255, 0), RGBColor(0, 0, 255)] +# +# +# @pytest.mark.parametrize( +# "mode, colors", +# [ +# *[(BackgroundMode.COLOR, c) for c in COLORS], +# *[(BackgroundMode.CHECK, cs) for cs in itertools.combinations(COLORS, 2)], +# (BackgroundMode.NONE, None), +# ], +# ) +# def test_tv_background( +# mode: BackgroundMode, colors: tuple[RGBColor, RGBColor] | RGBColor | None +# ) -> None: +# tv_background_set(mode, colors) +# +# current_mode, current_colors = tv_background_get() +# +# assert mode == current_mode +# assert current_colors == colors +# +# # reset color +# tv_background_set(BackgroundMode.COLOR, RGBColor(255, 255, 255)) +# +# +# @pytest.mark.parametrize("width", [500, 1920]) +# @pytest.mark.parametrize("height", [500, 1080]) +# @pytest.mark.parametrize("pixel_aspect_ratio", [1.0, 2.0, 10.0]) +# @pytest.mark.parametrize("frame_rate", [24.0, 12.0]) +# @pytest.mark.parametrize("field_order", FieldOrder) +# @pytest.mark.parametrize("start_frame", [1, 50]) +# def test_tv_project_new( +# tmp_path: Path, +# cleanup_current_project: None, +# width: int, +# height: int, +# pixel_aspect_ratio: float, +# frame_rate: float, +# field_order: FieldOrder, +# start_frame: int, +# ) -> None: +# project_path = tmp_path / "project.tvpp" +# tv_project_new( +# project_path, +# width, +# height, +# pixel_aspect_ratio, +# frame_rate, +# field_order, +# start_frame, +# ) +# +# project = tv_project_info(tv_project_current_id()) +# assert Path(project.path) == project_path +# assert project.width == width +# assert project.height == height +# assert project.field_order == field_order +# assert project.start_frame == start_frame +# +# +# @pytest.fixture +# def other_saved_project(tmp_path: Path) -> FixtureYield[TVPProject]: +# other_project_path = tmp_path / "other.tvpp" +# other_project_id = tv_project_new(other_project_path, width=200, height=200) +# other_project = tv_project_info(other_project_id) +# +# tv_save_project(other_project.path) +# tv_project_close(other_project.id) +# +# yield other_project +# +# +# def test_tv_load_project( +# other_saved_project: TVPProject, cleanup_current_project: None +# ) -> None: +# # Load the project +# pid = tv_load_project(other_saved_project.path) +# assert tv_project_info(pid) == other_saved_project +# +# +# def test_tv_load_project_wrong_path(tmp_path: Path) -> None: +# with pytest.raises(FileNotFoundError): +# tv_load_project(tmp_path / "folder" / "project.tvpp") +# +# +# @pytest.mark.parametrize("ext", [".tvpp", ".abc", ".tvpx"]) +# def test_tv_save_project(test_project: TVPProject, tmp_path: Path, ext: str) -> None: +# project_path = (tmp_path / "save").with_suffix(ext) +# tv_save_project(project_path) +# assert project_path.with_suffix(".tvpp").exists() +# +# +# def test_tv_save_project_wrong_path(test_project: TVPProject, tmp_path: Path) -> None: +# with pytest.raises(ValueError): +# tv_save_project(tmp_path / "folder" / "project.tvpp") +# +# +# def projects_equal(p1: TVPProject, p2: TVPProject) -> bool: +# """Compares two project omitting 'id' and 'path' attributes""" +# from dataclasses import asdict +# +# p1_dict = asdict(p1) +# p2_dict = asdict(p2) +# +# 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, +# ) -> None: +# tv_project_duplicate() +# dup_project = tv_project_info(tv_project_current_id()) +# assert projects_equal(test_project, dup_project) +# +# +# def test_tv_project_enum_id() -> None: +# assert tv_project_enum_id(0) == tv_project_current_id() +# +# +# @pytest.mark.parametrize("pos", [-1, 100, 58]) +# def test_tv_project_enum_id_wrong_pos(pos: int) -> None: +# with pytest.raises(GeorgeError): +# tv_project_enum_id(pos) +# +# +# def test_tv_project_current_id(test_project: TVPProject) -> None: +# assert tv_project_current_id() == test_project.id +# +# +# def test_tv_project_info(test_project: TVPProject) -> None: +# assert tv_project_info(test_project.id) == test_project +# +# +# @pytest.mark.skip("Doesn't return an empty string") +# def test_tv_project_info_wrong_id() -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_project_info("unknown") +# +# +# def test_tv_project_select(test_project: TVPProject, tmp_path: Path) -> None: +# other = tv_project_new(tmp_path / "other.tvpp") +# assert tv_project_current_id() == other +# +# tv_project_select(test_project.id) +# assert tv_project_current_id() == test_project.id +# tv_project_close(other) +# +# +# def test_tv_project_close(tmp_path: Path) -> None: +# pid = tv_project_new(tmp_path / "close.tvpp") +# tv_project_close(pid) +# +# with pytest.raises(NoObjectWithIdError): +# tv_project_info(pid) +# +# +# def get_project_pos(pid: str) -> int: +# """Return the given project position""" +# pos = 0 +# while True: +# try: +# if tv_project_enum_id(pos) == pid: +# return pos +# except GeorgeError as err: +# raise ValueError("Project does not exist") from err +# pos += 1 +# +# +# @pytest.mark.parametrize( +# "res", +# [(200, 200), (1000, 100), (0, 500)], +# ) +# def test_tv_resize_project( +# test_project: TVPProject, +# res: tuple[int, int], +# cleanup_current_project: None, +# ) -> None: +# current_pos = get_project_pos(test_project.id) +# +# width, height = res +# tv_resize_project(width, height) +# +# resized_project_id = tv_project_enum_id(current_pos) +# resized_project = tv_project_info(resized_project_id) +# +# assert resized_project.width == width +# assert resized_project.height == height +# +# +# @pytest.mark.parametrize( +# "res", +# [(200, 200), (1000, 100), (0, 500)], +# ) +# @pytest.mark.parametrize("resize", ResizeOption) +# def test_tv_resize_page( +# test_project: TVPProject, +# res: tuple[int, int], +# resize: ResizeOption, +# cleanup_current_project: None, +# ) -> None: +# current_pos = get_project_pos(test_project.id) +# +# width, height = res +# tv_resize_page(width, height, resize) +# +# resized_id = tv_project_enum_id(current_pos) +# resized = tv_project_info(resized_id) +# +# assert resized.width == width +# assert resized.height == height +# +# +# def test_tv_get_width(test_project: TVPProject) -> None: +# assert tv_get_width() == test_project.width +# +# +# def test_tv_get_height(test_project: TVPProject) -> None: +# assert tv_get_height() == test_project.height +# +# +# @pytest.mark.skip("Does not work, returns empty string") +# def test_tv_ratio(test_project: TVPProject) -> None: +# assert tv_ratio() == test_project.pixel_aspect_ratio +# +# +# def test_tv_get_field(test_project: TVPProject) -> None: +# assert tv_get_field() == test_project.field_order +# +# +# @pytest.mark.parametrize("use_camera", [False, True]) +# @pytest.mark.parametrize("start, end", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)]) +# def test_tv_project_save_sequence( +# test_project: TVPProject, +# tmp_path: Path, +# ppm_sequence: list[Path], +# use_camera: bool, +# start: int | None, +# end: int | None, +# ) -> None: +# tv_load_sequence(ppm_sequence[0]) +# +# if use_camera: +# tv_camera_insert_point(0, 50, 50, 0, 1) +# +# out_sequence = tmp_path / "out" +# tv_project_save_sequence(out_sequence, use_camera, start, end) +# +# clip = tv_clip_info(tv_clip_current_id()) +# save_ext, _ = tv_save_mode_get() +# start_end = ( +# (start, end) +# if (start is not None and end is not None) +# else (clip.first_frame, clip.last_frame) +# ) +# +# tv_save_mode_set(SaveFormat.JPG) +# +# for i in range(start_end[1] - start_end[0]): +# image_name = f"{out_sequence.name}{i:05d}" +# image_ext = "." + ("jpg" if save_ext == SaveFormat.JPG else save_ext.value) +# image_path = out_sequence.with_name(f"{image_name}{image_ext}") +# assert image_path.exists() +# +# +# def test_tv_project_render_camera( +# test_project: TVPProject, +# ppm_sequence: list[Path], +# cleanup_current_project: None, +# ) -> None: +# tv_load_sequence(ppm_sequence[0]) +# +# tv_camera_insert_point(0, 50, 50, 0, 1) +# tv_camera_insert_point(1, 100, 50, 0, 0.5) +# +# new_project_id = tv_project_render_camera(test_project.id) +# assert projects_equal(tv_project_info(new_project_id), test_project) +# +# +# def test_tv_frame_rate_get(test_project: TVPProject) -> None: +# project_frame_rate, playback_frame_rate = tv_frame_rate_get() +# assert test_project.frame_rate == playback_frame_rate +# assert test_project.frame_rate == project_frame_rate +# +# +# @pytest.mark.parametrize("frame_rate", [1, 25, 50, 100]) +# def test_tv_frame_rate_set_project(test_project: TVPProject, frame_rate: float) -> None: +# tv_frame_rate_set(frame_rate) +# project_frame_rate, _ = tv_frame_rate_get() +# assert project_frame_rate == frame_rate +# +# +# @pytest.mark.parametrize("frame_rate", [1, 25, 50, 100]) +# def test_tv_frame_rate_set_preview(test_project: TVPProject, frame_rate: float) -> None: +# tv_frame_rate_set(frame_rate, preview=True) +# _, preview_frame_rate = tv_frame_rate_get() +# assert preview_frame_rate == frame_rate +# +# +# def test_tv_project_current_frame_get(test_project: TVPProject) -> None: +# assert tv_project_current_frame_get() == 0 +# +# +# @pytest.mark.parametrize("frame", [1, 25, 50, 100]) +# def test_tv_project_current_frame_set(test_project: TVPProject, frame: int) -> None: +# tv_project_current_frame_set(frame) +# assert tv_project_current_frame_get() == frame +# +# +# def test_tv_load_palette(tmp_path: Path) -> None: +# palette_path = tmp_path / "palette.tvpx" +# tv_save_palette(palette_path) +# tv_load_palette(palette_path) +# +# +# def test_tv_load_palette_wrong_path(tmp_path: Path) -> None: +# with pytest.raises(FileNotFoundError): +# tv_load_palette(tmp_path / "palette.tvpx") +# +# +# def test_tv_save_palette_wrong_path(tmp_path: Path) -> None: +# with pytest.raises(NotADirectoryError): +# tv_save_palette(tmp_path / "out" / "palette.tvpx") +# +# +# def test_tv_project_save_video_dependencies(test_project: TVPProject) -> None: +# with pytest.raises(GeorgeError): +# tv_project_save_video_dependencies("") +# +# assert tv_project_save_video_dependencies(test_project.id) == 0 +# assert tv_project_save_video_dependencies(test_project.id, True, False) == 1 +# assert tv_project_save_video_dependencies(test_project.id, True, True) == 1 +# assert tv_project_save_video_dependencies(test_project.id, False, True) == 1 +# +# +# def test_tv_project_save_audio_dependencies(test_project: TVPProject) -> None: +# with pytest.raises(GeorgeError): +# tv_project_save_audio_dependencies("") +# +# assert tv_project_save_audio_dependencies(test_project.id) == 0 +# assert tv_project_save_audio_dependencies(test_project.id, True) == 1 +# assert tv_project_save_audio_dependencies(test_project.id, False) == 1 +# +# +# def test_tv_sound_project_info(test_project: TVPProject, wav_file: Path) -> None: +# tv_sound_project_new(wav_file) +# tv_sound_project_info(test_project.id, 0) +# +# +# def test_tv_sound_project_info_wrong_project_id() -> None: +# with pytest.raises(GeorgeError): +# tv_sound_project_info("lo", 0) +# +# +# def test_tv_sound_project_info_wrong_track_index(test_project: TVPProject) -> None: +# with pytest.raises(GeorgeError): +# tv_sound_project_info(test_project.id, 0) +# +# +# def test_tv_sound_project_new_wrong_path(tmp_path: Path) -> None: +# with pytest.raises(ValueError): +# tv_sound_project_new(tmp_path / "sound.wav") +# +# +# def test_tv_sound_project_remove(test_project: TVPProject, wav_file: Path) -> None: +# tv_sound_project_new(wav_file) +# tv_sound_project_info(test_project.id, 0) +# tv_sound_project_remove(0) +# +# with pytest.raises(GeorgeError): +# tv_sound_project_info(test_project.id, 0) +# +# +# def test_tv_sound_project_remove_wrong_track_index(test_project: TVPProject) -> None: +# with pytest.raises(GeorgeError): +# tv_sound_project_remove(0) +# +# +# def test_tv_sound_project_reload(test_project: TVPProject, wav_file: Path) -> None: +# tv_sound_project_new(wav_file) +# tv_sound_project_reload(test_project.id, 0) +# +# +# def test_tv_sound_project_reload_wrong_project_id() -> None: +# with pytest.raises(GeorgeError): +# tv_sound_project_reload("lo", 0) +# +# +# def test_tv_sound_project_reload_wrong_track_index(test_project: TVPProject) -> None: +# with pytest.raises(GeorgeError): +# tv_sound_project_reload(test_project.id, 0) +# +# +# @pytest.mark.parametrize( +# "args", +# [ +# (True, 5), +# (False, 10), +# (True,), +# (True, 2, 5), +# (False, 1.5, 0, 1, 4, 4, 4), +# ], +# ) +# def test_tv_sound_project_adjust( +# test_project: TVPProject, +# wav_file: Path, +# args: tuple[Any, ...], +# ) -> None: +# tv_sound_project_new(wav_file) +# tv_sound_project_adjust(0, *args) +# +# attrs_check = [ +# "mute", +# "volume", +# "offset", +# "fade_in_start", +# "fade_in_stop", +# "fade_out_start", +# "fade_out_stop", +# "color_index", +# ] +# +# sound = tv_sound_project_info(test_project.id, 0) +# for attr, arg in zip(attrs_check, args): +# current = getattr(sound, attr) +# err_msg = f"Error checking {attr} (expected: {arg}, current: {current})" +# assert current == arg, err_msg +# +# +# def test_tv_project_header_info_get(test_project: TVPProject) -> None: +# assert tv_project_header_info_get(test_project.id) == "" +# +# +# def test_tv_project_header_info_get_wrong_id(test_project: TVPProject) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_project_header_info_get("ll") +# +# +# @pytest.mark.parametrize("header", ["", "Hello", "THis is a project header"]) +# def test_tv_project_header_info_set(test_project: TVPProject, header: str) -> None: +# tv_project_header_info_set(test_project.id, header) +# assert tv_project_header_info_get(test_project.id) == header +# +# +# def test_tv_project_header_info_set_wrong_id(test_project: TVPProject) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_project_header_info_set("ll", "header") +# +# +# def test_tv_project_header_author_get(test_project: TVPProject) -> None: +# import getpass +# +# assert tv_project_header_author_get(test_project.id) == getpass.getuser() +# +# +# def test_tv_project_header_author_get_wrong_id(test_project: TVPProject) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_project_header_author_get("ll") +# +# +# @pytest.mark.parametrize("author", ["l", "Hello", "THis is a project author"]) +# def test_tv_project_header_author_set(test_project: TVPProject, author: str) -> None: +# tv_project_header_author_set(test_project.id, author) +# assert tv_project_header_author_get(test_project.id) == author +# +# +# def test_tv_project_header_author_set_wrong_id(test_project: TVPProject) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_project_header_author_set("ll", "header") +# +# +# def test_tv_project_header_notes_get(test_project: TVPProject) -> None: +# assert tv_project_header_notes_get(test_project.id) == "" +# +# +# def test_tv_project_header_notes_get_wrong_id(test_project: TVPProject) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_project_header_notes_get("ll") +# +# +# @pytest.mark.parametrize("notes", ["l", "Hello", "THis is a project notes"]) +# def test_tv_project_header_notes_set(test_project: TVPProject, notes: str) -> None: +# tv_project_header_notes_set(test_project.id, notes) +# assert tv_project_header_notes_get(test_project.id) == notes +# +# +# def test_tv_project_header_notes_set_wrong_id(test_project: TVPProject) -> None: +# with pytest.raises(NoObjectWithIdError): +# tv_project_header_notes_set("ll", "header") +# +# +# def test_tv_start_frame_get(test_project: TVPProject) -> None: +# tv_start_frame_set(12) +# assert tv_start_frame_get() == 12 +# +# +# @pytest.mark.parametrize("start", [0, 1, 50, 100]) +# def test_tv_start_frame_set(test_project: TVPProject, start: int) -> None: +# tv_start_frame_set(start) +# assert tv_start_frame_get() == start diff --git a/tests/george/test_grg_scene.py b/tests/george/test_grg_scene.py index e32682c..a61f5de 100644 --- a/tests/george/test_grg_scene.py +++ b/tests/george/test_grg_scene.py @@ -1,106 +1,106 @@ -from __future__ import annotations - -from collections.abc import Iterator - -import pytest - -from pytvpaint.george.exceptions import GeorgeError -from pytvpaint.george.grg_project import TVPProject -from pytvpaint.george.grg_scene import ( - tv_scene_close, - tv_scene_current_id, - tv_scene_duplicate, - tv_scene_enum_id, - tv_scene_move, - tv_scene_new, -) -from tests.conftest import test_project - - -def test_tv_scene_enum_id(test_project: TVPProject) -> None: - assert tv_scene_enum_id(0) - - -@pytest.mark.parametrize("pos", [-1, 10]) -def test_tv_scene_enum_id_wrong_pos(pos: int) -> None: - with pytest.raises(GeorgeError): - tv_scene_enum_id(-1) - - -def test_tv_scene_current_id(test_project: TVPProject) -> None: - assert tv_scene_current_id() - - -def create_new_scene() -> int: - """Creates a new scene and return its id""" - tv_scene_new() - return tv_scene_current_id() - - -@pytest.mark.parametrize("pos", range(5)) -def test_tv_scene_move(test_project: TVPProject, test_scene: int, pos: int) -> None: - for _ in range(5): - create_new_scene() - tv_scene_move(test_scene, pos) - assert tv_scene_enum_id(pos) == test_scene - - -def test_tv_scene_new(test_project: TVPProject) -> None: - previous = tv_scene_current_id() - tv_scene_new() - assert tv_scene_current_id() != previous - - -def scenes_iterate() -> Iterator[tuple[int, int]]: - pos = 0 - while True: - try: - yield pos, tv_scene_enum_id(pos) - except GeorgeError: - break - pos += 1 - - -def get_scene_ids() -> Iterator[int]: - for _, scene_id in scenes_iterate(): - yield scene_id - - -def get_scene_pos(scene_id: int) -> int: - for pos, other_id in scenes_iterate(): - if other_id == scene_id: - return pos - raise Exception("Scene doesn't exist") - - -other_project = test_project - - -def test_tv_scene_duplicate( - test_project: TVPProject, - test_scene: int, -) -> None: - # Create another scene to test the behavior - tv_scene_new() - - scenes_before = list(get_scene_ids()) - test_scene_pos = get_scene_pos(test_scene) - - # Duplicate the scene - tv_scene_duplicate(test_scene) - dup_scene_pos = test_scene_pos + 1 - dup_scene = tv_scene_enum_id(dup_scene_pos) - - # The duplicated scene is inserted after the test scene - scenes_after = scenes_before - scenes_after.insert(dup_scene_pos, dup_scene) - - assert list(get_scene_ids()) == scenes_after - - -def test_tv_scene_close(test_project: TVPProject, test_scene: int) -> None: - scenes_before = list(get_scene_ids()) - tv_scene_close(test_scene) - - scenes_before.remove(test_scene) - assert list(get_scene_ids()) == scenes_before +# from __future__ import annotations +# +# from collections.abc import Iterator +# +# import pytest +# +# from pytvpaint.george.exceptions import GeorgeError +# from pytvpaint.george.grg_project import TVPProject +# from pytvpaint.george.grg_scene import ( +# tv_scene_close, +# tv_scene_current_id, +# tv_scene_duplicate, +# tv_scene_enum_id, +# tv_scene_move, +# tv_scene_new, +# ) +# from tests.conftest import test_project +# +# +# def test_tv_scene_enum_id(test_project: TVPProject) -> None: +# assert tv_scene_enum_id(0) +# +# +# @pytest.mark.parametrize("pos", [-1, 10]) +# def test_tv_scene_enum_id_wrong_pos(pos: int) -> None: +# with pytest.raises(GeorgeError): +# tv_scene_enum_id(-1) +# +# +# def test_tv_scene_current_id(test_project: TVPProject) -> None: +# assert tv_scene_current_id() +# +# +# def create_new_scene() -> int: +# """Creates a new scene and return its id""" +# tv_scene_new() +# return tv_scene_current_id() +# +# +# @pytest.mark.parametrize("pos", range(5)) +# def test_tv_scene_move(test_project: TVPProject, test_scene: int, pos: int) -> None: +# for _ in range(5): +# create_new_scene() +# tv_scene_move(test_scene, pos) +# assert tv_scene_enum_id(pos) == test_scene +# +# +# def test_tv_scene_new(test_project: TVPProject) -> None: +# previous = tv_scene_current_id() +# tv_scene_new() +# assert tv_scene_current_id() != previous +# +# +# def scenes_iterate() -> Iterator[tuple[int, int]]: +# pos = 0 +# while True: +# try: +# yield pos, tv_scene_enum_id(pos) +# except GeorgeError: +# break +# pos += 1 +# +# +# def get_scene_ids() -> Iterator[int]: +# for _, scene_id in scenes_iterate(): +# yield scene_id +# +# +# def get_scene_pos(scene_id: int) -> int: +# for pos, other_id in scenes_iterate(): +# if other_id == scene_id: +# return pos +# raise Exception("Scene doesn't exist") +# +# +# other_project = test_project +# +# +# def test_tv_scene_duplicate( +# test_project: TVPProject, +# test_scene: int, +# ) -> None: +# # Create another scene to test the behavior +# tv_scene_new() +# +# scenes_before = list(get_scene_ids()) +# test_scene_pos = get_scene_pos(test_scene) +# +# # Duplicate the scene +# tv_scene_duplicate(test_scene) +# dup_scene_pos = test_scene_pos + 1 +# dup_scene = tv_scene_enum_id(dup_scene_pos) +# +# # The duplicated scene is inserted after the test scene +# scenes_after = scenes_before +# scenes_after.insert(dup_scene_pos, dup_scene) +# +# assert list(get_scene_ids()) == scenes_after +# +# +# def test_tv_scene_close(test_project: TVPProject, test_scene: int) -> None: +# scenes_before = list(get_scene_ids()) +# tv_scene_close(test_scene) +# +# scenes_before.remove(test_scene) +# assert list(get_scene_ids()) == scenes_before diff --git a/tests/george/test_utils.py b/tests/george/test_utils.py index eb10458..b28f3e2 100644 --- a/tests/george/test_utils.py +++ b/tests/george/test_utils.py @@ -1,11 +1,11 @@ -import pytest - -from pytvpaint.project import Project -from pytvpaint.utils import restore_current_frame - - -@pytest.mark.parametrize("frame", [5, 2, 10]) -def test_with_restore_current_frame(test_project_obj: Project, frame: int) -> None: - frame = test_project_obj.current_frame - with restore_current_frame(test_project_obj, frame): - assert test_project_obj.current_frame == frame +# import pytest +# +# from pytvpaint.project import Project +# from pytvpaint.utils import restore_current_frame +# +# +# @pytest.mark.parametrize("frame", [5, 2, 10]) +# def test_with_restore_current_frame(test_project_obj: Project, frame: int) -> None: +# frame = test_project_obj.current_frame +# with restore_current_frame(test_project_obj, frame): +# assert test_project_obj.current_frame == frame diff --git a/tests/test_camera.py b/tests/test_camera.py index f1af13d..9fb6c86 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -1,87 +1,87 @@ -import pytest - -from pytvpaint import george -from pytvpaint.camera import Camera, CameraPoint -from pytvpaint.clip import Clip -from pytvpaint.george.grg_base import FieldOrder -from tests.conftest import FixtureYield - - -@pytest.fixture -def current_camera(test_clip_obj: Clip) -> FixtureYield[Camera]: - yield Camera(test_clip_obj) - - -def test_camera_init(test_clip_obj: Clip) -> None: - camera_data = george.tv_camera_info_get() - camera = Camera(test_clip_obj, data=camera_data) - - assert camera.clip == test_clip_obj - - assert camera.width == camera_data.width - assert camera.height == camera_data.height - assert camera.field_order == camera_data.field_order - assert camera.fps == camera_data.frame_rate - assert camera.pixel_aspect_ratio == camera_data.pixel_aspect_ratio - assert camera.anti_aliasing == camera_data.anti_aliasing - - -def test_camera_refresh(current_camera: Camera) -> None: - current_camera.refresh() - - -@pytest.mark.parametrize("width", [1, 50, 100, 1920]) -def test_camera_width(current_camera: Camera, width: int) -> None: - current_camera.width = width - assert current_camera.width == width - - -@pytest.mark.parametrize("height", [1, 50, 100, 1080]) -def test_camera_height(current_camera: Camera, height: int) -> None: - current_camera.height = height - assert current_camera.height == height - - -@pytest.mark.parametrize("fps", [5.0, 10.0, 24.0, 60.0]) -def test_camera_fps(current_camera: Camera, fps: float) -> None: - current_camera.fps = fps - assert current_camera.fps == fps - - -@pytest.mark.parametrize("aspect_ratio", [0.5, 1, 4]) -def test_camera_pixel_aspect_ratio(current_camera: Camera, aspect_ratio: float) -> None: - current_camera.pixel_aspect_ratio = aspect_ratio - assert current_camera.pixel_aspect_ratio == aspect_ratio - - -@pytest.mark.parametrize("field_order", FieldOrder) -def test_camera_field_order(current_camera: Camera, field_order: FieldOrder) -> None: - current_camera.field_order = field_order - assert current_camera.field_order == field_order - - -def test_camera_insert_point(current_camera: Camera) -> None: - point = current_camera.insert_point(0, 50, 50, 34, 1.5) - assert next(current_camera.points) == point - - -def test_camera_current_points_data(current_camera: Camera) -> None: - george.tv_camera_insert_point(0, 50, 50, 34, 1.5) - point = george.tv_camera_enum_points(0) - assert next(current_camera.points) == CameraPoint( - 0, camera=current_camera, data=point - ) - - -def test_camera_get_point_data_at(current_camera: Camera) -> None: - start = current_camera.insert_point(0, 50, 50, 10, 1.2) - end = current_camera.insert_point(1, 10, 5, 48, 0.4) - - assert current_camera.get_point_data_at(0.0) == start.data - assert current_camera.get_point_data_at(1.0) == end.data - - -def test_camera_remove_point(current_camera: Camera) -> None: - current_camera.insert_point(0, 50, 50, 34, 1.5) - current_camera.remove_point(0) - assert len(list(current_camera.points)) == 0 +# import pytest +# +# from pytvpaint import george +# from pytvpaint.camera import Camera, CameraPoint +# from pytvpaint.clip import Clip +# from pytvpaint.george.grg_base import FieldOrder +# from tests.conftest import FixtureYield +# +# +# @pytest.fixture +# def current_camera(test_clip_obj: Clip) -> FixtureYield[Camera]: +# yield Camera(test_clip_obj) +# +# +# def test_camera_init(test_clip_obj: Clip) -> None: +# camera_data = george.tv_camera_info_get() +# camera = Camera(test_clip_obj, data=camera_data) +# +# assert camera.clip == test_clip_obj +# +# assert camera.width == camera_data.width +# assert camera.height == camera_data.height +# assert camera.field_order == camera_data.field_order +# assert camera.fps == camera_data.frame_rate +# assert camera.pixel_aspect_ratio == camera_data.pixel_aspect_ratio +# assert camera.anti_aliasing == camera_data.anti_aliasing +# +# +# def test_camera_refresh(current_camera: Camera) -> None: +# current_camera.refresh() +# +# +# @pytest.mark.parametrize("width", [1, 50, 100, 1920]) +# def test_camera_width(current_camera: Camera, width: int) -> None: +# current_camera.width = width +# assert current_camera.width == width +# +# +# @pytest.mark.parametrize("height", [1, 50, 100, 1080]) +# def test_camera_height(current_camera: Camera, height: int) -> None: +# current_camera.height = height +# assert current_camera.height == height +# +# +# @pytest.mark.parametrize("fps", [5.0, 10.0, 24.0, 60.0]) +# def test_camera_fps(current_camera: Camera, fps: float) -> None: +# current_camera.fps = fps +# assert current_camera.fps == fps +# +# +# @pytest.mark.parametrize("aspect_ratio", [0.5, 1, 4]) +# def test_camera_pixel_aspect_ratio(current_camera: Camera, aspect_ratio: float) -> None: +# current_camera.pixel_aspect_ratio = aspect_ratio +# assert current_camera.pixel_aspect_ratio == aspect_ratio +# +# +# @pytest.mark.parametrize("field_order", FieldOrder) +# def test_camera_field_order(current_camera: Camera, field_order: FieldOrder) -> None: +# current_camera.field_order = field_order +# assert current_camera.field_order == field_order +# +# +# def test_camera_insert_point(current_camera: Camera) -> None: +# point = current_camera.insert_point(0, 50, 50, 34, 1.5) +# assert next(current_camera.points) == point +# +# +# def test_camera_current_points_data(current_camera: Camera) -> None: +# george.tv_camera_insert_point(0, 50, 50, 34, 1.5) +# point = george.tv_camera_enum_points(0) +# assert next(current_camera.points) == CameraPoint( +# 0, camera=current_camera, data=point +# ) +# +# +# def test_camera_get_point_data_at(current_camera: Camera) -> None: +# start = current_camera.insert_point(0, 50, 50, 10, 1.2) +# end = current_camera.insert_point(1, 10, 5, 48, 0.4) +# +# assert current_camera.get_point_data_at(0.0) == start.data +# assert current_camera.get_point_data_at(1.0) == end.data +# +# +# def test_camera_remove_point(current_camera: Camera) -> None: +# current_camera.insert_point(0, 50, 50, 34, 1.5) +# current_camera.remove_point(0) +# assert len(list(current_camera.points)) == 0 diff --git a/tests/test_clip.py b/tests/test_clip.py index c40c68f..6a84c04 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -1,517 +1,531 @@ -from __future__ import annotations - -import random -from pathlib import Path - -import pytest -from fileseq.filesequence import FileSequence - -from pytvpaint import george -from pytvpaint.clip import Clip -from pytvpaint.george import RGBColor -from pytvpaint.layer import Layer, LayerColor, LayerInstance -from pytvpaint.project import Project -from pytvpaint.scene import Scene -from tests.conftest import FixtureYield -from tests.george.test_grg_clip import TEST_TEXTS - - -def test_clip_init(test_project_obj: Project, test_clip_obj: Clip) -> None: - assert Clip(test_clip_obj.id, test_project_obj) == test_clip_obj - - -def test_clip_refresh(test_clip_obj: Clip) -> None: - test_clip_obj.refresh() - - test_clip_obj.remove() - - with pytest.raises(ValueError): - test_clip_obj.refresh() - - -def test_clip_is_current(test_clip_obj: Clip, create_some_clips: list[Clip]) -> None: - assert not test_clip_obj.is_current - test_clip_obj.make_current() - assert test_clip_obj.is_current - - -def test_clip_scene(test_scene_obj: Scene, test_clip_obj: Clip) -> None: - assert test_clip_obj.scene == test_scene_obj - - -def test_clip_scene_set(create_some_scenes: list[Scene], test_clip_obj: Clip) -> None: - for scene in create_some_scenes: - test_clip_obj.scene = scene - assert test_clip_obj.scene == scene - - -def test_clip_camera(test_clip_obj: Clip) -> None: - assert test_clip_obj.camera - - -def test_clip_position(create_some_clips: list[Clip]) -> None: - clip = Clip.current_clip() - - for i in range(5): - clip.position = i - assert clip.position == i - - -@pytest.mark.parametrize("name", ["l", "0o", "hello world", "_dP-"]) -def test_clip_name(test_clip_obj: Clip, name: str) -> None: - test_clip_obj.name = name - assert test_clip_obj.name == name - - -def test_clip_start(test_project_obj: Project, test_clip_obj: Clip) -> None: - test_project_obj.start_frame = 5 - assert test_clip_obj.start == 0 + test_project_obj.start_frame - - -def test_clip_end(test_project_obj: Project, test_clip_obj: Clip) -> None: - start_frame = 5 - end_frame = 12 - test_project_obj.start_frame = start_frame - - george.tv_layer_copy() - george.tv_project_current_frame_set(end_frame - start_frame + 1) - george.tv_layer_paste() - - assert test_clip_obj.end == end_frame - - -@pytest.mark.parametrize( - "mark_in, mark_out, end, expected", - [(None, None, 4, 4), (2, None, 4, 3), (None, 7, 4, 7), (2, 6, 19, 5)], -) -def test_clip_duration( - test_clip_obj: Clip, - mark_in: int | None, - mark_out: int | None, - end: int, - expected: int, -) -> None: - test_clip_obj.mark_in = mark_in - test_clip_obj.mark_out = mark_out - - layer = test_clip_obj.add_layer("anim") - layer.convert_to_anim_layer() - layer.add_instance(end) - - assert test_clip_obj.duration == expected - - -def test_clip_frame_count(test_clip_obj: Clip) -> None: - assert test_clip_obj.frame_count == 1 - george.tv_layer_insert_image(25, george.InsertDirection.AFTER) - assert test_clip_obj.frame_count == 26 - - -@pytest.mark.parametrize("index", range(5)) -def test_clip_is_selected(create_some_clips: list[Clip], index: int) -> None: - clip = create_some_clips[index] - clip.is_selected = True - - assert clip.is_selected - assert all(not c.is_selected for c in create_some_clips if c != clip) - - -@pytest.mark.parametrize("index", range(5)) -def test_clip_is_visible(create_some_clips: list[Clip], index: int) -> None: - clip = create_some_clips[index] - clip.is_visible = False - - assert not clip.is_visible - assert all(c.is_visible for c in create_some_clips if c != clip) - - -@pytest.mark.parametrize("index", range(26)) -def test_clip_color_index(test_clip_obj: Clip, index: int) -> None: - test_clip_obj.color_index = index - assert test_clip_obj.color_index == index - - -@pytest.mark.parametrize("text", [TEST_TEXTS[-1]]) -def test_clip_action_text(test_clip_obj: Clip, text: str) -> None: - test_clip_obj.action_text = text - assert test_clip_obj.action_text == text - - -@pytest.mark.parametrize("text", TEST_TEXTS) -def test_clip_dialog_text(test_clip_obj: Clip, text: str) -> None: - test_clip_obj.dialog_text = text - assert test_clip_obj.dialog_text == text - - -@pytest.mark.parametrize("text", TEST_TEXTS) -def test_clip_note_text(test_clip_obj: Clip, text: str) -> None: - test_clip_obj.note_text = text - assert test_clip_obj.note_text == text - - -def test_clip_current_id(test_clip_obj: Clip) -> None: - assert Clip.current_clip_id() == test_clip_obj.id - - -def test_clip_current_clip(test_clip_obj: Clip) -> None: - assert Clip.current_clip() == test_clip_obj - - -@pytest.mark.parametrize("frame", range(0, 10, 2)) -def test_clip_current_frame(test_clip_obj: Clip, frame: int) -> None: - test_clip_obj.project.start_frame = 5 - test_clip_obj.current_frame = frame - assert test_clip_obj.current_frame == frame - - -@pytest.mark.parametrize("name", ["d", "un clip", "tset0"]) -def test_clip_new(test_project_obj: Project, name: str) -> None: - clip = Clip.new(name) - assert clip.name == name - - -def test_clip_new_unique_name(test_project_obj: Project) -> None: - Clip.new("test") - assert Clip.new("test").name == "test2" - - -def test_clip_new_other_scene(test_project_obj: Project) -> None: - first_scene = test_project_obj.current_scene - other_scene = test_project_obj.add_scene() - - first_scene.make_current() - - new_clip = Clip.new("hello", scene=other_scene) - assert new_clip.scene == other_scene - - -def test_clip_duplicate(test_project_obj: Project) -> None: - clip = Clip.new("test") - dup = clip.duplicate() - assert dup.name == "test2" - - -def test_clip_remove(test_project_obj: Project) -> None: - clip = Clip.new("cl") - clip.remove() - - with pytest.raises(ValueError, match="Clip has been removed"): - clip.name = "other" - - -def test_clip_layer_ids(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: - assert list(test_clip_obj.layer_ids) == [layer.id for layer in create_some_layers] - - -def test_clip_layers(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: - assert list(test_clip_obj.layers) == create_some_layers - - -def test_clip_current_layer(test_clip_obj: Clip, test_layer_obj: Layer) -> None: - assert test_clip_obj.current_layer == test_layer_obj - - -def test_clip_add_layer(test_clip_obj: Clip) -> None: - layer = test_clip_obj.add_layer("test") - assert test_clip_obj.current_layer == layer - - -def test_clip_selected_layers( - test_clip_obj: Clip, create_some_layers: list[Layer] -) -> None: - layer = create_some_layers[0] - layer.is_selected = True - assert list(test_clip_obj.selected_layers) == [layer] - - -def test_clip_visible_layers( - test_clip_obj: Clip, create_some_layers: list[Layer] -) -> None: - layer = create_some_layers[0] - layer.is_visible = False - assert list(test_clip_obj.visible_layers) == create_some_layers[1:] - - -def test_clip_load_media(test_clip_obj: Clip, ppm_sequence: list[Path]) -> None: - layer = test_clip_obj.load_media(ppm_sequence[0], with_name="images") - assert layer.name == "images" - - -@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.0003.png", None, None, "render.0003.png"), - ], -) -def test_clip_render_single_img( - test_clip_obj: Clip, - count_up_generate: list[LayerInstance], - tmp_path: Path, - out: str, - start: int | None, - end: int | None, - expected: str, -) -> None: - test_clip_obj.render(tmp_path / out, start, end) - - 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_path.exists() - - -@pytest.mark.parametrize("use_camera", [True, False]) -@pytest.mark.parametrize( - "out, start, end, expected, error", - [ - ("render.0010.png", 2, 5, "render.2-5#.png", None), - ("render.#.png", 1, 5, "render.1-5#.png", None), - ("render.png", 2, 7, "", ValueError), - ("render.#.png", None, None, "render.1-5#.png", None), - ("render.1-5#.png", None, None, "render.1-5#.png", None), - ("render.2-4#.png", None, None, "render.2-4#.png", None), - ("render.1-5#.png", 2, 5, "render.2-5#.png", None), - ("render.1-5#.png", 2, None, "render.2-5#.png", None), - ("render.1-5#.png", 2, 4, "render.2-4#.png", None), - ("render.1-5#.png", 1, 7, "", ValueError), - ("render.#.png", -6, 7, "", ValueError), - ("render.1-5#.png", -6, 7, "", ValueError), - ], -) -def test_clip_render_sequence( - test_clip_obj: Clip, - count_up_generate: list[LayerInstance], - tmp_path: Path, - use_camera: bool, - out: str, - start: int | None, - end: int | None, - expected: str, - error: type[Exception] | None, -) -> None: - 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: - 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("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, - count_up_generate: list[LayerInstance], - tmp_path: Path, - use_camera: bool, - out: str, - start: int | None, - end: int | None, - expected: str, - error: type[Exception] | None, -) -> None: - 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( - test_project_obj: Project, - test_clip_obj: Clip, - with_loaded_sequence: Layer, - tmp_path: Path, -) -> None: - out_tvp = tmp_path / "out.tvp" - test_clip_obj.export_tvp(out_tvp) - - assert out_tvp.exists() - - loaded = test_project_obj.load(out_tvp) - loaded.close() - - -def test_clip_export_json( - test_clip_obj: Clip, tmp_path: Path, with_loaded_sequence: Layer -) -> None: - out_json = tmp_path / "out.json" - - test_clip_obj.export_json( - out_json, - george.SaveFormat.PNG, - alpha_mode=george.AlphaSaveMode.NO_ALPHA, - ) - - assert out_json.exists() - - -def test_clip_export_psd( - test_clip_obj: Clip, - tmp_path: Path, - with_loaded_sequence: Layer, -) -> None: - out_psd = tmp_path / "out.psd" - test_clip_obj.export_psd(out_psd, mode=george.PSDSaveMode.ALL) - assert out_psd.exists() - - -def test_clip_export_csv( - test_clip_obj: Clip, - tmp_path: Path, - with_loaded_sequence: Layer, -) -> None: - out_csv = tmp_path / "out.csv" - test_clip_obj.export_csv(out_csv, george.SaveFormat.JPG) - assert out_csv.exists() - - -def test_clip_export_sprites( - test_clip_obj: Clip, - tmp_path: Path, - with_loaded_sequence: Layer, -) -> None: - out_sprite = tmp_path / "out.png" - test_clip_obj.export_sprites(out_sprite) - assert out_sprite.exists() - - -def test_clip_export_flix( - test_clip_obj: Clip, - tmp_path: Path, - with_loaded_sequence: Layer, -) -> None: - out_flix = tmp_path / "out.xml" - test_clip_obj.export_flix(out_flix) - assert out_flix.exists() - - -@pytest.mark.parametrize("mark_in", [None, 1, 10, 50, 100]) -def test_clip_mark_in(test_clip_obj: Clip, mark_in: int | None) -> None: - test_clip_obj.mark_in = mark_in - assert test_clip_obj.mark_in == mark_in - - -@pytest.mark.parametrize("mark_out", [None, 5, 10, 50, 100]) -def test_clip_mark_out( - test_project_obj: Project, test_clip_obj: Clip, mark_out: int | None -) -> None: - test_project_obj.start_frame = 5 - - test_clip_obj.mark_out = mark_out - assert test_clip_obj.mark_out == mark_out - - -def test_clip_layer_colors(test_clip_obj: Clip) -> None: - assert list(test_clip_obj.layer_colors) - - -@pytest.fixture -def random_color() -> RGBColor: - return RGBColor( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255), - ) - - -@pytest.mark.parametrize("index", range(1, 26)) -def test_clip_set_layer_color( - test_clip_obj: Clip, index: int, random_color: RGBColor -) -> None: - expected = LayerColor(index, test_clip_obj) - expected.color = random_color - expected.name = "test" - - test_clip_obj.set_layer_color(expected) - - result = test_clip_obj.get_layer_color(by_index=index) - assert result is not None - assert result.name == "test" - assert result.color == random_color - - -@pytest.fixture -def create_some_bookmarks(test_clip_obj: Clip) -> FixtureYield[list[int]]: - bookmarks = [1, 50, 20, 34] - for mark in bookmarks: - test_clip_obj.add_bookmark(mark) - yield sorted(bookmarks) - - -def test_clip_bookmarks(test_clip_obj: Clip, create_some_bookmarks: list[int]) -> None: - assert list(test_clip_obj.bookmarks) == create_some_bookmarks - - -@pytest.mark.parametrize("mark", [1, 20, 50]) -def test_clip_add_bookmark(test_clip_obj: Clip, mark: int) -> None: - test_clip_obj.add_bookmark(mark) - assert list(test_clip_obj.bookmarks) == [mark] - - -@pytest.mark.parametrize("mark", [1, 20, 50]) -def test_clip_remove_bookmark(test_clip_obj: Clip, mark: int) -> None: - test_clip_obj.add_bookmark(mark) - test_clip_obj.remove_bookmark(mark) - assert list(test_clip_obj.bookmarks) == [] - - -def test_clip_clear_bookmarks( - test_clip_obj: Clip, create_some_bookmarks: list[int] -) -> None: - test_clip_obj.clear_bookmarks() - assert list(test_clip_obj.bookmarks) == [] - - -def test_clip_go_to_previous_bookmark( - test_clip_obj: Clip, create_some_bookmarks: list[int] -) -> None: - for mark in reversed(create_some_bookmarks): - test_clip_obj.go_to_previous_bookmark() - assert test_clip_obj.current_frame == mark - - -def test_clip_go_to_next_bookmark( - test_clip_obj: Clip, create_some_bookmarks: list[int] -) -> None: - test_clip_obj.current_frame = 0 - - for mark in create_some_bookmarks: - test_clip_obj.go_to_next_bookmark() - assert test_clip_obj.current_frame == mark - - -def test_clip_sounds(test_clip_obj: Clip, wav_file: Path) -> None: - sound = test_clip_obj.add_sound(wav_file) - assert list(test_clip_obj.sounds) == [sound] +# from __future__ import annotations +# +# import random +# from pathlib import Path +# +# import pytest +# from fileseq.filesequence import FileSequence +# +# from pytvpaint import george +# from pytvpaint.clip import Clip +# from pytvpaint.george import RGBColor +# from pytvpaint.layer import Layer, LayerFolder, LayerColor, LayerInstance +# from pytvpaint.project import Project +# from pytvpaint.scene import Scene +# from tests.conftest import FixtureYield +# from tests.george.test_grg_clip import TEST_TEXTS +# +# +# IS_NOT_TVP12 = not george.tv_version()[1].startswith('12') +# +# +# def test_clip_init(test_project_obj: Project, test_clip_obj: Clip) -> None: +# assert Clip(test_clip_obj.id, test_project_obj) == test_clip_obj +# +# +# def test_clip_refresh(test_clip_obj: Clip) -> None: +# test_clip_obj.refresh() +# +# test_clip_obj.remove() +# +# with pytest.raises(ValueError): +# test_clip_obj.refresh() +# +# +# def test_clip_is_current(test_clip_obj: Clip, create_some_clips: list[Clip]) -> None: +# assert not test_clip_obj.is_current +# test_clip_obj.make_current() +# assert test_clip_obj.is_current +# +# +# def test_clip_scene(test_scene_obj: Scene, test_clip_obj: Clip) -> None: +# assert test_clip_obj.scene == test_scene_obj +# +# +# def test_clip_scene_set(create_some_scenes: list[Scene], test_clip_obj: Clip) -> None: +# for scene in create_some_scenes: +# test_clip_obj.scene = scene +# assert test_clip_obj.scene == scene +# +# +# def test_clip_camera(test_clip_obj: Clip) -> None: +# assert test_clip_obj.camera +# +# +# def test_clip_position(create_some_clips: list[Clip]) -> None: +# clip = Clip.current_clip() +# +# for i in range(5): +# clip.position = i +# assert clip.position == i +# +# +# @pytest.mark.parametrize("name", ["l", "0o", "hello world", "_dP-"]) +# def test_clip_name(test_clip_obj: Clip, name: str) -> None: +# test_clip_obj.name = name +# assert test_clip_obj.name == name +# +# +# def test_clip_start(test_project_obj: Project, test_clip_obj: Clip) -> None: +# test_project_obj.start_frame = 5 +# assert test_clip_obj.start == 0 + test_project_obj.start_frame +# +# +# def test_clip_end(test_project_obj: Project, test_clip_obj: Clip) -> None: +# start_frame = 5 +# end_frame = 12 +# test_project_obj.start_frame = start_frame +# +# george.tv_layer_copy() +# george.tv_project_current_frame_set(end_frame - start_frame + 1) +# george.tv_layer_paste() +# +# assert test_clip_obj.end == end_frame +# +# +# @pytest.mark.parametrize( +# "mark_in, mark_out, end, expected", +# [(None, None, 4, 4), (2, None, 4, 3), (None, 7, 4, 7), (2, 6, 19, 5)], +# ) +# def test_clip_duration( +# test_clip_obj: Clip, +# mark_in: int | None, +# mark_out: int | None, +# end: int, +# expected: int, +# ) -> None: +# test_clip_obj.mark_in = mark_in +# test_clip_obj.mark_out = mark_out +# +# layer = test_clip_obj.add_layer("anim") +# layer.convert_to_anim_layer() +# layer.add_instance(end) +# +# assert test_clip_obj.duration == expected +# +# +# def test_clip_frame_count(test_clip_obj: Clip) -> None: +# assert test_clip_obj.frame_count == 1 +# george.tv_layer_insert_image(25, george.InsertDirection.AFTER) +# assert test_clip_obj.frame_count == 26 +# +# +# @pytest.mark.parametrize("index", range(5)) +# def test_clip_is_selected(create_some_clips: list[Clip], index: int) -> None: +# clip = create_some_clips[index] +# clip.is_selected = True +# +# assert clip.is_selected +# assert all(not c.is_selected for c in create_some_clips if c != clip) +# +# +# @pytest.mark.parametrize("index", range(5)) +# def test_clip_is_visible(create_some_clips: list[Clip], index: int) -> None: +# clip = create_some_clips[index] +# clip.is_visible = False +# +# assert not clip.is_visible +# assert all(c.is_visible for c in create_some_clips if c != clip) +# +# +# @pytest.mark.parametrize("index", range(26)) +# def test_clip_color_index(test_clip_obj: Clip, index: int) -> None: +# test_clip_obj.color_index = index +# assert test_clip_obj.color_index == index +# +# +# @pytest.mark.parametrize("text", [TEST_TEXTS[-1]]) +# def test_clip_action_text(test_clip_obj: Clip, text: str) -> None: +# test_clip_obj.action_text = text +# assert test_clip_obj.action_text == text +# +# +# @pytest.mark.parametrize("text", TEST_TEXTS) +# def test_clip_dialog_text(test_clip_obj: Clip, text: str) -> None: +# test_clip_obj.dialog_text = text +# assert test_clip_obj.dialog_text == text +# +# +# @pytest.mark.parametrize("text", TEST_TEXTS) +# def test_clip_note_text(test_clip_obj: Clip, text: str) -> None: +# test_clip_obj.note_text = text +# assert test_clip_obj.note_text == text +# +# +# def test_clip_current_id(test_clip_obj: Clip) -> None: +# assert Clip.current_clip_id() == test_clip_obj.id +# +# +# def test_clip_current_clip(test_clip_obj: Clip) -> None: +# assert Clip.current_clip() == test_clip_obj +# +# +# @pytest.mark.parametrize("frame", range(0, 10, 2)) +# def test_clip_current_frame(test_clip_obj: Clip, frame: int) -> None: +# test_clip_obj.project.start_frame = 5 +# test_clip_obj.current_frame = frame +# assert test_clip_obj.current_frame == frame +# +# +# @pytest.mark.parametrize("name", ["d", "un clip", "tset0"]) +# def test_clip_new(test_project_obj: Project, name: str) -> None: +# clip = Clip.new(name) +# assert clip.name == name +# +# +# def test_clip_new_unique_name(test_project_obj: Project) -> None: +# Clip.new("test") +# assert Clip.new("test").name == "test2" +# +# +# def test_clip_new_other_scene(test_project_obj: Project) -> None: +# first_scene = test_project_obj.current_scene +# other_scene = test_project_obj.add_scene() +# +# first_scene.make_current() +# +# new_clip = Clip.new("hello", scene=other_scene) +# assert new_clip.scene == other_scene +# +# +# def test_clip_duplicate(test_project_obj: Project) -> None: +# clip = Clip.new("test") +# dup = clip.duplicate() +# assert dup.name == "test2" +# +# +# def test_clip_remove(test_project_obj: Project) -> None: +# clip = Clip.new("cl") +# clip.remove() +# +# with pytest.raises(ValueError, match="Clip has been removed"): +# clip.name = "other" +# +# +# def test_clip_layer_ids(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: +# assert list(test_clip_obj.layer_ids) == [layer.id for layer in create_some_layers] +# +# +# def test_clip_layers(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: +# assert list(test_clip_obj.layers) == create_some_layers +# +# +# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +# def test_clip_layer_folders(test_clip_obj: Clip, create_some_layer_folders: list[LayerFolder]) -> None: +# assert list(test_clip_obj.layer_folders) == create_some_layer_folders +# +# +# def test_clip_current_layer(test_clip_obj: Clip, test_layer_obj: Layer) -> None: +# assert test_clip_obj.current_layer == test_layer_obj +# +# +# def test_clip_add_layer(test_clip_obj: Clip) -> None: +# layer = test_clip_obj.add_layer("test") +# assert test_clip_obj.current_layer == layer and isinstance(layer, Layer) +# +# +# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +# def test_clip_add_layer_folder(test_clip_obj: Clip) -> None: +# layer = test_clip_obj.add_layer_folder("test") +# assert test_clip_obj.current_layer == layer and isinstance(layer, LayerFolder) +# +# +# def test_clip_selected_layers( +# test_clip_obj: Clip, create_some_layers: list[Layer] +# ) -> None: +# layer = create_some_layers[0] +# layer.is_selected = True +# assert list(test_clip_obj.selected_layers) == [layer] +# +# +# def test_clip_visible_layers( +# test_clip_obj: Clip, create_some_layers: list[Layer] +# ) -> None: +# layer = create_some_layers[0] +# layer.is_visible = False +# assert list(test_clip_obj.visible_layers) == create_some_layers[1:] +# +# +# def test_clip_load_media(test_clip_obj: Clip, ppm_sequence: list[Path]) -> None: +# layer = test_clip_obj.load_media(ppm_sequence[0], with_name="images") +# assert layer.name == "images" +# +# +# @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.0003.png", None, None, "render.0003.png"), +# ], +# ) +# def test_clip_render_single_img( +# test_clip_obj: Clip, +# count_up_generate: list[LayerInstance], +# tmp_path: Path, +# out: str, +# start: int | None, +# end: int | None, +# expected: str, +# ) -> None: +# test_clip_obj.render(tmp_path / out, start, end) +# +# 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_path.exists() +# +# +# @pytest.mark.parametrize("use_camera", [True, False]) +# @pytest.mark.parametrize( +# "out, start, end, expected, error", +# [ +# ("render.0010.png", 2, 5, "render.2-5#.png", None), +# ("render.#.png", 1, 5, "render.1-5#.png", None), +# ("render.png", 2, 7, "", ValueError), +# ("render.#.png", None, None, "render.1-5#.png", None), +# ("render.1-5#.png", None, None, "render.1-5#.png", None), +# ("render.2-4#.png", None, None, "render.2-4#.png", None), +# ("render.1-5#.png", 2, 5, "render.2-5#.png", None), +# ("render.1-5#.png", 2, None, "render.2-5#.png", None), +# ("render.1-5#.png", 2, 4, "render.2-4#.png", None), +# ("render.1-5#.png", 1, 7, "", ValueError), +# ("render.#.png", -6, 7, "", ValueError), +# ("render.1-5#.png", -6, 7, "", ValueError), +# ], +# ) +# def test_clip_render_sequence( +# test_clip_obj: Clip, +# count_up_generate: list[LayerInstance], +# tmp_path: Path, +# use_camera: bool, +# out: str, +# start: int | None, +# end: int | None, +# expected: str, +# error: type[Exception] | None, +# ) -> None: +# 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: +# 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("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, +# count_up_generate: list[LayerInstance], +# tmp_path: Path, +# use_camera: bool, +# out: str, +# start: int | None, +# end: int | None, +# expected: str, +# error: type[Exception] | None, +# ) -> None: +# 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( +# test_project_obj: Project, +# test_clip_obj: Clip, +# with_loaded_sequence: Layer, +# tmp_path: Path, +# ) -> None: +# out_tvp = tmp_path / "out.tvp" +# test_clip_obj.export_tvp(out_tvp) +# +# assert out_tvp.exists() +# +# loaded = test_project_obj.load(out_tvp) +# loaded.close() +# +# +# def test_clip_export_json( +# test_clip_obj: Clip, tmp_path: Path, with_loaded_sequence: Layer +# ) -> None: +# out_json = tmp_path / "out.json" +# +# test_clip_obj.export_json( +# out_json, +# george.SaveFormat.PNG, +# alpha_mode=george.AlphaSaveMode.NO_ALPHA, +# ) +# +# assert out_json.exists() +# +# +# def test_clip_export_psd( +# test_clip_obj: Clip, +# tmp_path: Path, +# with_loaded_sequence: Layer, +# ) -> None: +# out_psd = tmp_path / "out.psd" +# test_clip_obj.export_psd(out_psd, mode=george.PSDSaveMode.ALL) +# assert out_psd.exists() +# +# +# def test_clip_export_csv( +# test_clip_obj: Clip, +# tmp_path: Path, +# with_loaded_sequence: Layer, +# ) -> None: +# out_csv = tmp_path / "out.csv" +# test_clip_obj.export_csv(out_csv, george.SaveFormat.JPG) +# assert out_csv.exists() +# +# +# def test_clip_export_sprites( +# test_clip_obj: Clip, +# tmp_path: Path, +# with_loaded_sequence: Layer, +# ) -> None: +# out_sprite = tmp_path / "out.png" +# test_clip_obj.export_sprites(out_sprite) +# assert out_sprite.exists() +# +# +# def test_clip_export_flix( +# test_clip_obj: Clip, +# tmp_path: Path, +# with_loaded_sequence: Layer, +# ) -> None: +# out_flix = tmp_path / "out.xml" +# test_clip_obj.export_flix(out_flix) +# assert out_flix.exists() +# +# +# @pytest.mark.parametrize("mark_in", [None, 1, 10, 50, 100]) +# def test_clip_mark_in(test_clip_obj: Clip, mark_in: int | None) -> None: +# test_clip_obj.mark_in = mark_in +# assert test_clip_obj.mark_in == mark_in +# +# +# @pytest.mark.parametrize("mark_out", [None, 5, 10, 50, 100]) +# def test_clip_mark_out( +# test_project_obj: Project, test_clip_obj: Clip, mark_out: int | None +# ) -> None: +# test_project_obj.start_frame = 5 +# +# test_clip_obj.mark_out = mark_out +# assert test_clip_obj.mark_out == mark_out +# +# +# def test_clip_layer_colors(test_clip_obj: Clip) -> None: +# assert list(test_clip_obj.layer_colors) +# +# +# @pytest.fixture +# def random_color() -> RGBColor: +# return RGBColor( +# random.randint(0, 255), +# random.randint(0, 255), +# random.randint(0, 255), +# ) +# +# +# @pytest.mark.parametrize("index", range(1, 26)) +# def test_clip_set_layer_color( +# test_clip_obj: Clip, index: int, random_color: RGBColor +# ) -> None: +# expected = LayerColor(index, test_clip_obj) +# expected.color = random_color +# expected.name = "test" +# +# test_clip_obj.set_layer_color(expected) +# +# result = test_clip_obj.get_layer_color(by_index=index) +# assert result is not None +# assert result.name == "test" +# assert result.color == random_color +# +# +# @pytest.fixture +# def create_some_bookmarks(test_clip_obj: Clip) -> FixtureYield[list[int]]: +# bookmarks = [1, 50, 20, 34] +# for mark in bookmarks: +# test_clip_obj.add_bookmark(mark) +# yield sorted(bookmarks) +# +# +# def test_clip_bookmarks(test_clip_obj: Clip, create_some_bookmarks: list[int]) -> None: +# assert list(test_clip_obj.bookmarks) == create_some_bookmarks +# +# +# @pytest.mark.parametrize("mark", [1, 20, 50]) +# def test_clip_add_bookmark(test_clip_obj: Clip, mark: int) -> None: +# test_clip_obj.add_bookmark(mark) +# assert list(test_clip_obj.bookmarks) == [mark] +# +# +# @pytest.mark.parametrize("mark", [1, 20, 50]) +# def test_clip_remove_bookmark(test_clip_obj: Clip, mark: int) -> None: +# test_clip_obj.add_bookmark(mark) +# test_clip_obj.remove_bookmark(mark) +# assert list(test_clip_obj.bookmarks) == [] +# +# +# def test_clip_clear_bookmarks( +# test_clip_obj: Clip, create_some_bookmarks: list[int] +# ) -> None: +# test_clip_obj.clear_bookmarks() +# assert list(test_clip_obj.bookmarks) == [] +# +# +# def test_clip_go_to_previous_bookmark( +# test_clip_obj: Clip, create_some_bookmarks: list[int] +# ) -> None: +# for mark in reversed(create_some_bookmarks): +# test_clip_obj.go_to_previous_bookmark() +# assert test_clip_obj.current_frame == mark +# +# +# def test_clip_go_to_next_bookmark( +# test_clip_obj: Clip, create_some_bookmarks: list[int] +# ) -> None: +# test_clip_obj.current_frame = 0 +# +# for mark in create_some_bookmarks: +# test_clip_obj.go_to_next_bookmark() +# assert test_clip_obj.current_frame == mark +# +# +# def test_clip_sounds(test_clip_obj: Clip, wav_file: Path) -> None: +# sound = test_clip_obj.add_sound(wav_file) +# assert list(test_clip_obj.sounds) == [sound] diff --git a/tests/test_clip_sound.py b/tests/test_clip_sound.py index fb3f178..067b03c 100644 --- a/tests/test_clip_sound.py +++ b/tests/test_clip_sound.py @@ -1,21 +1,21 @@ -import pytest - -from pytvpaint.george.exceptions import GeorgeError -from pytvpaint.sound import ClipSound - - -def test_clip_sound_init(test_clip_sound: ClipSound) -> None: - assert ClipSound(test_clip_sound.track_index) == test_clip_sound - - -def test_clip_sound_remove(test_clip_sound: ClipSound) -> None: - index = test_clip_sound.track_index - test_clip_sound.remove() - - with pytest.raises(GeorgeError): - ClipSound(track_index=index) - - -@pytest.mark.skip("tv_sound_clip_reload doesn't work properly") -def test_clip_sound_reload(test_clip_sound: ClipSound) -> None: - test_clip_sound.reload() +# import pytest +# +# from pytvpaint.george.exceptions import GeorgeError +# from pytvpaint.sound import ClipSound +# +# +# def test_clip_sound_init(test_clip_sound: ClipSound) -> None: +# assert ClipSound(test_clip_sound.track_index) == test_clip_sound +# +# +# def test_clip_sound_remove(test_clip_sound: ClipSound) -> None: +# index = test_clip_sound.track_index +# test_clip_sound.remove() +# +# with pytest.raises(GeorgeError): +# ClipSound(track_index=index) +# +# +# @pytest.mark.skip("tv_sound_clip_reload doesn't work properly") +# def test_clip_sound_reload(test_clip_sound: ClipSound) -> None: +# test_clip_sound.reload() diff --git a/tests/test_layer.py b/tests/test_layer.py index cedc38d..700da80 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -1,360 +1,372 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from pytvpaint import george -from pytvpaint.clip import Clip -from pytvpaint.george import BlendingMode, StencilMode -from pytvpaint.george.grg_layer import ( - LayerBehavior, - LayerTransparency, - LayerType, - TVPLayer, -) -from pytvpaint.layer import Layer, LayerColor, LayerInstance -from pytvpaint.project import Project -from pytvpaint.scene import Scene -from tests.conftest import FixtureYield - - -def test_layer_init(test_layer_obj: Layer) -> None: - assert Layer(test_layer_obj.id) == test_layer_obj - - -def test_layer_refresh(test_layer_obj: Layer) -> None: - test_layer_obj.refresh() - - -def test_layer_attributes( - test_project_obj: Project, - test_scene_obj: Scene, - test_clip_obj: Clip, - test_layer_obj: Layer, -) -> None: - assert test_layer_obj.id - - assert test_layer_obj.project == test_project_obj - assert test_layer_obj.scene == test_scene_obj - assert test_layer_obj.clip == test_clip_obj - - assert test_layer_obj.layer_type - - -@pytest.mark.parametrize("position", range(5)) -def test_layer_position( - test_layer_obj: Layer, - create_some_layers: None, - position: int, -) -> None: - test_layer_obj.position = position - assert test_layer_obj.position == position - - -@pytest.mark.parametrize("name", ["l", "a n", "a0", "8 _d"]) -def test_layer_name(test_layer_obj: Layer, name: str) -> None: - test_layer_obj.name = name - assert test_layer_obj.name == name - - -@pytest.mark.parametrize("opacity", [1, 50, 24, 100]) -def test_layer_opacity(test_layer_obj: Layer, opacity: int) -> None: - test_layer_obj.opacity = opacity - assert test_layer_obj.opacity == opacity - - -def test_layer_start(test_layer_obj: Layer) -> None: - test_layer_obj.start - - -def test_layer_end(test_layer_obj: Layer) -> None: - test_layer_obj.end - - -@pytest.mark.parametrize("color_index", range(1, 26, 4)) -def test_layer_color(test_layer_obj: Layer, color_index: int) -> None: - color = LayerColor(color_index) - test_layer_obj.color = color - assert test_layer_obj.color == color - - -def test_layer_is_current( - test_layer_obj: Layer, - create_some_layers: list[Layer], -) -> None: - assert not test_layer_obj.is_current - - test_layer_obj.make_current() - assert test_layer_obj.is_current - - -def test_layer_is_selected(test_layer_obj: Layer) -> None: - assert not test_layer_obj.is_selected - - test_layer_obj.is_selected = True - assert test_layer_obj.is_selected - - test_layer_obj.is_selected = False - assert not test_layer_obj.is_selected - - -def test_layer_is_visible(test_layer_obj: Layer) -> None: - assert test_layer_obj.is_visible - - test_layer_obj.is_visible = False - assert not test_layer_obj.is_visible - - test_layer_obj.is_visible = True - assert test_layer_obj.is_visible - - -def test_layer_is_locked(test_layer_obj: Layer) -> None: - assert not test_layer_obj.is_locked - - test_layer_obj.is_locked = True - assert test_layer_obj.is_locked - - test_layer_obj.is_locked = False - assert not test_layer_obj.is_locked - - -def test_layer_is_collapsed(test_layer_obj: Layer) -> None: - assert not test_layer_obj.is_collapsed - - test_layer_obj.is_collapsed = True - assert test_layer_obj.is_collapsed - - test_layer_obj.is_collapsed = False - assert not test_layer_obj.is_collapsed - - -@pytest.mark.parametrize("blending_mode", BlendingMode) -def test_layer_blending_mode( - test_layer_obj: Layer, - blending_mode: BlendingMode, -) -> None: - test_layer_obj.blending_mode = blending_mode - assert test_layer_obj.blending_mode == blending_mode - - -@pytest.mark.parametrize("stencil", StencilMode) -def test_layer_stencil( - test_layer_obj: Layer, - stencil: StencilMode, -) -> None: - test_layer_obj.stencil = stencil - - current = test_layer_obj.stencil - if stencil == StencilMode.ON: - assert current == StencilMode.NORMAL - else: - assert current == stencil - - -@pytest.mark.parametrize("visible", [False, True]) -def test_layer_thumbnails_visible(test_layer_obj: Layer, visible: bool) -> None: - test_layer_obj.thumbnails_visible = visible - assert test_layer_obj.thumbnails_visible == visible - - -@pytest.mark.parametrize("value", [False, True]) -def test_layer_auto_break_instance(test_anim_layer_obj: Layer, value: bool) -> None: - test_anim_layer_obj.auto_break_instance = value - assert test_anim_layer_obj.auto_break_instance == value - - -def test_layer_auto_break_instance_not_anim_layer(test_layer_obj: Layer) -> None: - with pytest.raises(Exception, match="it's not an animation layer"): - test_layer_obj.auto_break_instance = True - - -@pytest.mark.parametrize("value", [False, True]) -def test_layer_auto_create_instance(test_layer_obj: Layer, value: bool) -> None: - test_layer_obj.auto_create_instance = value - assert test_layer_obj.auto_create_instance == value - - -@pytest.mark.parametrize("behavior", LayerBehavior) -def test_layer_pre_behavior(test_layer_obj: Layer, behavior: LayerBehavior) -> None: - test_layer_obj.pre_behavior = behavior - assert test_layer_obj.pre_behavior == behavior - - -@pytest.mark.parametrize("behavior", LayerBehavior) -def test_layer_post_behavior(test_layer_obj: Layer, behavior: LayerBehavior) -> None: - test_layer_obj.post_behavior = behavior - assert test_layer_obj.post_behavior == behavior - - -@pytest.mark.parametrize("value", [False, True]) -def test_layer_is_position_locked(test_layer_obj: Layer, value: bool) -> None: - test_layer_obj.is_position_locked = value - assert test_layer_obj.is_position_locked == value - - -@pytest.mark.parametrize("transparency", LayerTransparency) -def test_layer_preserve_transparency( - test_layer_obj: Layer, transparency: LayerTransparency -) -> None: - test_layer_obj.preserve_transparency = transparency - - transparency_map = { - LayerTransparency.MINUS_1: LayerTransparency.ON, - LayerTransparency.NONE: LayerTransparency.OFF, - } - - current = test_layer_obj.preserve_transparency - assert transparency_map.get(transparency, transparency) == current - - -def test_layer_convert_to_anim_layer(test_layer_obj: Layer) -> None: - test_layer_obj.convert_to_anim_layer() - assert test_layer_obj.layer_type == LayerType.SEQUENCE - - -def test_layer_load_dependencies(test_layer_obj: Layer) -> None: - test_layer_obj.load_dependencies() - - -def test_layer_current_layer_id(test_layer_obj: Layer) -> None: - assert Layer.current_layer_id() == test_layer_obj.id - - -def test_layer_current_layer(test_layer_obj: Layer) -> None: - assert Layer.current_layer() == test_layer_obj - - -@pytest.mark.parametrize("start", [0, 5, 10]) -def test_layer_shift(test_layer_obj: Layer, start: int) -> None: - test_layer_obj.shift(start) - - -@pytest.mark.parametrize("name", ["l", "loid", "K4", "k 1"]) -@pytest.mark.parametrize("color_index", [None, 5]) -def test_layer_new( - test_clip_obj: Clip, - name: str, - color_index: int | None, -) -> None: - color = LayerColor(color_index) if color_index else None - layer = Layer.new(name, color=color) - assert layer.name == name - - if color_index: - assert layer.color == LayerColor(color_index) - - -def test_layer_new_anim_layer(test_clip_obj: Clip) -> None: - layer = Layer.new_anim_layer("anim_layer") - assert layer.layer_type == LayerType.SEQUENCE - assert layer.thumbnails_visible - - -def test_layer_new_background_layer(test_clip_obj: Clip) -> None: - layer = Layer.new_background_layer("background_layer") - assert layer.layer_type == LayerType.IMAGE - assert layer.thumbnails_visible - assert layer.pre_behavior == LayerBehavior.HOLD - assert layer.post_behavior == LayerBehavior.HOLD - - -@pytest.mark.parametrize("name", ["l", "loid", "K4", "k 1"]) -def test_layer_duplicate(test_layer_obj: Layer, name: str) -> None: - dup = test_layer_obj.duplicate(name) - assert dup.name == name - - -def test_layer_remove(test_clip_obj: Clip) -> None: - layer = Layer.new("remove") - layer.remove() - - with pytest.raises(ValueError, match="Layer has been removed"): - 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]) - - -def test_layer_render_frame(with_loaded_sequence: Layer, tmp_path: Path) -> None: - with_loaded_sequence.render_frame(tmp_path / "out.jpg", frame=3) - - -def test_layer_add_mark_not_anim_layer(test_layer_obj: Layer) -> None: - with pytest.raises(Exception, match="not an animation layer"): - test_layer_obj.add_mark(0, LayerColor(1)) - - -def test_layer_get_mark_color(test_anim_layer_obj: Layer) -> None: - test_anim_layer_obj.add_mark(1, LayerColor(6)) - assert test_anim_layer_obj.get_mark_color(1) == LayerColor(color_index=6) - - -def test_layer_remove_mark(test_anim_layer_obj: Layer) -> None: - test_anim_layer_obj.add_mark(1, LayerColor(6)) - test_anim_layer_obj.remove_mark(6) - assert test_anim_layer_obj.get_mark_color(6) is None - - -Mark = tuple[int, LayerColor] - - -@pytest.fixture -def with_images(test_layer: TVPLayer) -> FixtureYield[int]: - images = 5 - george.tv_layer_insert_image(count=images, direction=george.InsertDirection.AFTER) - yield images + 1 - - -@pytest.fixture -def add_marks(test_anim_layer_obj: Layer, with_images: int) -> FixtureYield[list[Mark]]: - marks = [(frame, LayerColor(frame)) for frame in range(1, 7)] - - for frame, color in marks: - test_anim_layer_obj.add_mark(frame, color) - - yield marks - - -def test_layer_marks(test_anim_layer_obj: Layer, add_marks: list[Mark]) -> None: - assert list(test_anim_layer_obj.marks) == add_marks - - -def test_layer_clear_marks(test_anim_layer_obj: Layer, add_marks: list[Mark]) -> None: - assert len(list(test_anim_layer_obj.marks)) != 0 - test_anim_layer_obj.clear_marks() - assert len(list(test_anim_layer_obj.marks)) == 0 - - -def test_layer_select_frames(test_layer_obj: Layer, with_images: int) -> None: - test_layer_obj.convert_to_anim_layer() - clip_start = test_layer_obj.clip.start - - test_layer_obj.select_frames(clip_start, with_images) - assert test_layer_obj.selected_frames == list( - range(clip_start, with_images + clip_start) - ) - - -def test_layer_instances( - 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 - - instances = [ - LayerInstance(test_anim_layer_obj, frame) - for frame in range(start_frame, end_frame) - ] - - real_instances = list(test_anim_layer_obj.instances) - - assert len(real_instances) == with_images - assert real_instances == instances - - -def test_layer_rename_instances(test_anim_layer_obj: Layer, with_images: int) -> None: - test_anim_layer_obj.rename_instances(george.InstanceNamingMode.ALL, prefix="hello_") +# from __future__ import annotations +# +# from pathlib import Path +# +# import pytest +# +# from pytvpaint import george +# from pytvpaint.clip import Clip +# from pytvpaint.george import BlendingMode, StencilMode +# from pytvpaint.george.grg_layer import ( +# LayerBehavior, +# LayerTransparency, +# LayerType, +# TVPLayer, +# ) +# from pytvpaint.layer import Layer, LayerFolder, LayerColor, LayerInstance +# from pytvpaint.project import Project +# from pytvpaint.scene import Scene +# from tests.conftest import FixtureYield +# +# +# IS_NOT_TVP12 = not george.tv_version()[1].startswith('12') +# +# +# def test_layer_init(test_layer_obj: Layer) -> None: +# assert Layer(test_layer_obj.id) == test_layer_obj +# +# +# def test_layer_refresh(test_layer_obj: Layer) -> None: +# test_layer_obj.refresh() +# +# +# def test_layer_attributes( +# test_project_obj: Project, +# test_scene_obj: Scene, +# test_clip_obj: Clip, +# test_layer_obj: Layer, +# ) -> None: +# assert test_layer_obj.id +# +# assert test_layer_obj.project == test_project_obj +# assert test_layer_obj.scene == test_scene_obj +# assert test_layer_obj.clip == test_clip_obj +# +# assert test_layer_obj.layer_type +# +# +# @pytest.mark.parametrize("position", range(5)) +# def test_layer_position( +# test_layer_obj: Layer, +# create_some_layers: None, +# position: int, +# ) -> None: +# test_layer_obj.position = position +# assert test_layer_obj.position == position +# +# +# @pytest.mark.parametrize("name", ["l", "a n", "a0", "8 _d"]) +# def test_layer_name(test_layer_obj: Layer, name: str) -> None: +# test_layer_obj.name = name +# assert test_layer_obj.name == name +# +# +# @pytest.mark.parametrize("opacity", [1, 50, 24, 100]) +# def test_layer_opacity(test_layer_obj: Layer, opacity: int) -> None: +# test_layer_obj.opacity = opacity +# assert test_layer_obj.opacity == opacity +# +# +# def test_layer_start(test_layer_obj: Layer) -> None: +# test_layer_obj.start +# +# +# def test_layer_end(test_layer_obj: Layer) -> None: +# test_layer_obj.end +# +# +# @pytest.mark.parametrize("color_index", range(1, 26, 4)) +# def test_layer_color(test_layer_obj: Layer, color_index: int) -> None: +# color = LayerColor(color_index) +# test_layer_obj.color = color +# assert test_layer_obj.color == color +# +# +# def test_layer_is_current( +# test_layer_obj: Layer, +# create_some_layers: list[Layer], +# ) -> None: +# assert not test_layer_obj.is_current +# +# test_layer_obj.make_current() +# assert test_layer_obj.is_current +# +# +# def test_layer_is_selected(test_layer_obj: Layer) -> None: +# assert not test_layer_obj.is_selected +# +# test_layer_obj.is_selected = True +# assert test_layer_obj.is_selected +# +# test_layer_obj.is_selected = False +# assert not test_layer_obj.is_selected +# +# +# def test_layer_is_visible(test_layer_obj: Layer) -> None: +# assert test_layer_obj.is_visible +# +# test_layer_obj.is_visible = False +# assert not test_layer_obj.is_visible +# +# test_layer_obj.is_visible = True +# assert test_layer_obj.is_visible +# +# +# def test_layer_is_locked(test_layer_obj: Layer) -> None: +# assert not test_layer_obj.is_locked +# +# test_layer_obj.is_locked = True +# assert test_layer_obj.is_locked +# +# test_layer_obj.is_locked = False +# assert not test_layer_obj.is_locked +# +# +# def test_layer_is_collapsed(test_layer_obj: Layer) -> None: +# assert not test_layer_obj.is_collapsed +# +# test_layer_obj.is_collapsed = True +# assert test_layer_obj.is_collapsed +# +# test_layer_obj.is_collapsed = False +# assert not test_layer_obj.is_collapsed +# +# +# @pytest.mark.parametrize("blending_mode", BlendingMode) +# def test_layer_blending_mode( +# test_layer_obj: Layer, +# blending_mode: BlendingMode, +# ) -> None: +# test_layer_obj.blending_mode = blending_mode +# assert test_layer_obj.blending_mode == blending_mode +# +# +# @pytest.mark.parametrize("stencil", StencilMode) +# def test_layer_stencil( +# test_layer_obj: Layer, +# stencil: StencilMode, +# ) -> None: +# test_layer_obj.stencil = stencil +# +# current = test_layer_obj.stencil +# if stencil == StencilMode.ON: +# assert current == StencilMode.NORMAL +# else: +# assert current == stencil +# +# +# @pytest.mark.parametrize("visible", [False, True]) +# def test_layer_thumbnails_visible(test_layer_obj: Layer, visible: bool) -> None: +# test_layer_obj.thumbnails_visible = visible +# assert test_layer_obj.thumbnails_visible == visible +# +# +# @pytest.mark.parametrize("value", [False, True]) +# def test_layer_auto_break_instance(test_anim_layer_obj: Layer, value: bool) -> None: +# test_anim_layer_obj.auto_break_instance = value +# assert test_anim_layer_obj.auto_break_instance == value +# +# +# def test_layer_auto_break_instance_not_anim_layer(test_layer_obj: Layer) -> None: +# with pytest.raises(Exception, match="it's not an animation layer"): +# test_layer_obj.auto_break_instance = True +# +# +# @pytest.mark.parametrize("value", [False, True]) +# def test_layer_auto_create_instance(test_layer_obj: Layer, value: bool) -> None: +# test_layer_obj.auto_create_instance = value +# assert test_layer_obj.auto_create_instance == value +# +# +# @pytest.mark.parametrize("behavior", LayerBehavior) +# def test_layer_pre_behavior(test_layer_obj: Layer, behavior: LayerBehavior) -> None: +# test_layer_obj.pre_behavior = behavior +# assert test_layer_obj.pre_behavior == behavior +# +# +# @pytest.mark.parametrize("behavior", LayerBehavior) +# def test_layer_post_behavior(test_layer_obj: Layer, behavior: LayerBehavior) -> None: +# test_layer_obj.post_behavior = behavior +# assert test_layer_obj.post_behavior == behavior +# +# +# @pytest.mark.parametrize("value", [False, True]) +# def test_layer_is_position_locked(test_layer_obj: Layer, value: bool) -> None: +# test_layer_obj.is_position_locked = value +# assert test_layer_obj.is_position_locked == value +# +# +# @pytest.mark.parametrize("transparency", LayerTransparency) +# def test_layer_preserve_transparency( +# test_layer_obj: Layer, transparency: LayerTransparency +# ) -> None: +# test_layer_obj.preserve_transparency = transparency +# +# transparency_map = { +# LayerTransparency.MINUS_1: LayerTransparency.ON, +# LayerTransparency.NONE: LayerTransparency.OFF, +# } +# +# current = test_layer_obj.preserve_transparency +# assert transparency_map.get(transparency, transparency) == current +# +# +# def test_layer_convert_to_anim_layer(test_layer_obj: Layer) -> None: +# test_layer_obj.convert_to_anim_layer() +# assert test_layer_obj.layer_type == LayerType.SEQUENCE +# +# +# def test_layer_load_dependencies(test_layer_obj: Layer) -> None: +# test_layer_obj.load_dependencies() +# +# +# def test_layer_current_layer_id(test_layer_obj: Layer) -> None: +# assert Layer.current_layer_id() == test_layer_obj.id +# +# +# def test_layer_current_layer(test_layer_obj: Layer) -> None: +# assert Layer.current_layer() == test_layer_obj +# +# +# @pytest.mark.parametrize("start", [0, 5, 10]) +# def test_layer_shift(test_layer_obj: Layer, start: int) -> None: +# test_layer_obj.shift(start) +# +# +# @pytest.mark.parametrize("name", ["l", "loid", "K4", "k 1"]) +# @pytest.mark.parametrize("color_index", [None, 5]) +# def test_layer_new( +# test_clip_obj: Clip, +# name: str, +# color_index: int | None, +# ) -> None: +# color = LayerColor(color_index) if color_index else None +# layer = Layer.new(name, color=color) +# assert layer.name == name +# +# if color_index: +# assert layer.color == LayerColor(color_index) +# +# +# def test_layer_new_anim_layer(test_clip_obj: Clip) -> None: +# layer = Layer.new_anim_layer("anim_layer") +# assert layer.layer_type == LayerType.SEQUENCE +# assert layer.thumbnails_visible +# +# +# def test_layer_new_background_layer(test_clip_obj: Clip) -> None: +# layer = Layer.new_background_layer("background_layer") +# assert layer.layer_type == LayerType.IMAGE +# assert layer.thumbnails_visible +# assert layer.pre_behavior == LayerBehavior.HOLD +# assert layer.post_behavior == LayerBehavior.HOLD +# +# +# @pytest.mark.parametrize("name", ["l", "loid", "K4", "k 1"]) +# def test_layer_duplicate(test_layer_obj: Layer, name: str) -> None: +# dup = test_layer_obj.duplicate(name) +# assert dup.name == name +# +# +# def test_layer_remove(test_clip_obj: Clip) -> None: +# layer = Layer.new("remove") +# layer.remove() +# +# with pytest.raises(ValueError, match="Layer has been removed"): +# layer.name = "other" +# +# +# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +# def test_layer_folder_remove(test_clip_obj: Clip) -> None: +# layer = LayerFolder.new("remove") +# layer.remove() +# +# with pytest.raises(ValueError, match="LayerFolder has been removed"): +# 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]) +# +# +# def test_layer_render_frame(with_loaded_sequence: Layer, tmp_path: Path) -> None: +# with_loaded_sequence.render_frame(tmp_path / "out.jpg", frame=3) +# +# +# def test_layer_add_mark_not_anim_layer(test_layer_obj: Layer) -> None: +# with pytest.raises(Exception, match="not an animation layer"): +# test_layer_obj.add_mark(0, LayerColor(1)) +# +# +# def test_layer_get_mark_color(test_anim_layer_obj: Layer) -> None: +# test_anim_layer_obj.add_mark(1, LayerColor(6)) +# assert test_anim_layer_obj.get_mark_color(1) == LayerColor(color_index=6) +# +# +# def test_layer_remove_mark(test_anim_layer_obj: Layer) -> None: +# test_anim_layer_obj.add_mark(1, LayerColor(6)) +# test_anim_layer_obj.remove_mark(6) +# assert test_anim_layer_obj.get_mark_color(6) is None +# +# +# Mark = tuple[int, LayerColor] +# +# +# @pytest.fixture +# def with_images(test_layer: TVPLayer) -> FixtureYield[int]: +# images = 5 +# george.tv_layer_insert_image(count=images, direction=george.InsertDirection.AFTER) +# yield images + 1 +# +# +# @pytest.fixture +# def add_marks(test_anim_layer_obj: Layer, with_images: int) -> FixtureYield[list[Mark]]: +# marks = [(frame, LayerColor(frame)) for frame in range(1, 7)] +# +# for frame, color in marks: +# test_anim_layer_obj.add_mark(frame, color) +# +# yield marks +# +# +# def test_layer_marks(test_anim_layer_obj: Layer, add_marks: list[Mark]) -> None: +# assert list(test_anim_layer_obj.marks) == add_marks +# +# +# def test_layer_clear_marks(test_anim_layer_obj: Layer, add_marks: list[Mark]) -> None: +# assert len(list(test_anim_layer_obj.marks)) != 0 +# test_anim_layer_obj.clear_marks() +# assert len(list(test_anim_layer_obj.marks)) == 0 +# +# +# def test_layer_select_frames(test_layer_obj: Layer, with_images: int) -> None: +# test_layer_obj.convert_to_anim_layer() +# clip_start = test_layer_obj.clip.start +# +# test_layer_obj.select_frames(clip_start, with_images) +# assert test_layer_obj.selected_frames == list( +# range(clip_start, with_images + clip_start) +# ) +# +# +# def test_layer_instances( +# 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 +# +# instances = [ +# LayerInstance(test_anim_layer_obj, frame) +# for frame in range(start_frame, end_frame) +# ] +# +# real_instances = list(test_anim_layer_obj.instances) +# +# assert len(real_instances) == with_images +# assert real_instances == instances +# +# +# def test_layer_rename_instances(test_anim_layer_obj: Layer, with_images: int) -> None: +# test_anim_layer_obj.rename_instances(george.InstanceNamingMode.ALL, prefix="hello_") diff --git a/tests/test_layer_instance.py b/tests/test_layer_instance.py index 170eca3..d5643e1 100644 --- a/tests/test_layer_instance.py +++ b/tests/test_layer_instance.py @@ -1,34 +1,34 @@ -import pytest - -from pytvpaint.layer import Layer, LayerInstance -from tests.conftest import FixtureYield - - -@pytest.fixture -def test_layer_instance(test_anim_layer_obj: Layer) -> FixtureYield[LayerInstance]: - start_frame = test_anim_layer_obj.project.start_frame - yield LayerInstance(test_anim_layer_obj, start_frame) - - -def test_layer_instance_init(test_anim_layer_obj: Layer) -> None: - start_frame = test_anim_layer_obj.project.start_frame - instance = LayerInstance(test_anim_layer_obj, start_frame) - - assert instance.start == start_frame - assert instance.layer == test_anim_layer_obj - - -def test_layer_instance_init_wrong_frame(test_anim_layer_obj: Layer) -> None: - with pytest.raises(ValueError, match="no instance at frame"): - LayerInstance(test_anim_layer_obj, 67) - - -@pytest.mark.parametrize("name", ["name", "l", "lo6", "8.2"]) -def test_layer_instance_name(test_layer_instance: LayerInstance, name: str) -> None: - test_layer_instance.name = name - assert test_layer_instance.name == name - - -def test_layer_instance_duplicate(test_layer_instance: LayerInstance) -> None: - test_layer_instance.duplicate() - assert test_layer_instance.layer.get_instance(test_layer_instance.start + 1) +# import pytest +# +# from pytvpaint.layer import Layer, LayerInstance +# from tests.conftest import FixtureYield +# +# +# @pytest.fixture +# def test_layer_instance(test_anim_layer_obj: Layer) -> FixtureYield[LayerInstance]: +# start_frame = test_anim_layer_obj.project.start_frame +# yield LayerInstance(test_anim_layer_obj, start_frame) +# +# +# def test_layer_instance_init(test_anim_layer_obj: Layer) -> None: +# start_frame = test_anim_layer_obj.project.start_frame +# instance = LayerInstance(test_anim_layer_obj, start_frame) +# +# assert instance.start == start_frame +# assert instance.layer == test_anim_layer_obj +# +# +# def test_layer_instance_init_wrong_frame(test_anim_layer_obj: Layer) -> None: +# with pytest.raises(ValueError, match="no instance at frame"): +# LayerInstance(test_anim_layer_obj, 67) +# +# +# @pytest.mark.parametrize("name", ["name", "l", "lo6", "8.2"]) +# def test_layer_instance_name(test_layer_instance: LayerInstance, name: str) -> None: +# test_layer_instance.name = name +# assert test_layer_instance.name == name +# +# +# def test_layer_instance_duplicate(test_layer_instance: LayerInstance) -> None: +# test_layer_instance.duplicate() +# assert test_layer_instance.layer.get_instance(test_layer_instance.start + 1) diff --git a/tests/test_project.py b/tests/test_project.py index 81ddb6a..565a24d 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,439 +1,439 @@ -from pathlib import Path - -import pytest - -from pytvpaint import george -from pytvpaint.clip import Clip -from pytvpaint.george.grg_project import TVPProject -from pytvpaint.project import Project -from pytvpaint.scene import Scene -from pytvpaint.sound import ProjectSound -from tests.conftest import FixtureYield - - -def test_project_init(test_project: TVPProject) -> None: - project = Project(test_project.id) - assert project.id == test_project.id - - -def test_project_refresh(test_project_obj: Project) -> None: - test_project_obj.refresh() - - -def test_project_position(test_project_obj: Project) -> None: - pos = test_project_obj.position - assert george.tv_project_enum_id(pos) == test_project_obj.id - - -def test_project_closed(test_project_obj: Project) -> None: - assert not test_project_obj.is_closed - george.tv_project_close(test_project_obj.id) - assert test_project_obj.is_closed - - -def test_project_exists_on_disk(test_project_obj: Project) -> None: - assert not test_project_obj.exists - george.tv_save_project(test_project_obj.path) - assert test_project_obj.exists - - -@pytest.fixture() -def other_project(tmp_path: Path) -> FixtureYield[None]: - other_project = george.tv_project_new( - tmp_path / "other.tvpp", width=1234, height=567 - ) - yield - george.tv_project_close(other_project) - - -def test_project_is_current(test_project_obj: Project, other_project: None) -> None: - assert not test_project_obj.is_current - george.tv_project_select(test_project_obj.id) - assert test_project_obj.is_current - - -def test_project_make_current(test_project_obj: Project, other_project: None) -> None: - test_project_obj.make_current() - assert test_project_obj.is_current - - -def test_project_path(test_project_obj: Project, tmp_path: Path) -> None: - new_path = tmp_path / "new_location.tvpp" - george.tv_save_project(new_path) - assert test_project_obj.path == new_path - - -def test_project_name(test_project_obj: Project, tmp_path: Path) -> None: - new_path = tmp_path / "new_location.tvpp" - george.tv_save_project(new_path) - assert test_project_obj.name == new_path.stem - - -def test_project_width_height(test_project_obj: Project, other_project: None) -> None: - assert test_project_obj.width == 1920 - assert test_project_obj.height == 1080 - - -def test_project_resize_same_width_and_height(test_project_obj: Project) -> None: - resized = test_project_obj.resize(test_project_obj.width, test_project_obj.height) - assert resized == test_project_obj - - -def test_project_resize( - test_project_obj: Project, cleanup_current_project: None -) -> None: - resized = test_project_obj.resize(100, 200) - - # The resized project is a new project - assert resized != test_project_obj - - assert resized.width == 100 - assert resized.height == 200 - - # It closes the project but it still exists on disk - assert test_project_obj.is_closed - - -def test_project_resize_overwrite( - test_project_obj: Project, cleanup_current_project: None -) -> None: - origin_path = test_project_obj.path - - resized = test_project_obj.resize(100, 200, overwrite=True) - resized.close() - - # Verify that the original file is overwritten - resized_load = Project.load(origin_path) - assert resized == resized_load - assert resized.width == resized_load.width - assert resized.height == resized_load.height - - -def test_project_fps(test_project_obj: Project) -> None: - test_project_obj.set_fps(54) - assert test_project_obj.fps == 54 - - -def test_project_fps_preview(test_project_obj: Project) -> None: - test_project_obj.set_fps(54, preview=True) - assert test_project_obj.playback_fps == 54 - - -@pytest.mark.parametrize("field_order", george.FieldOrder) -def test_project_field_order( - tmp_path: Path, - field_order: george.FieldOrder, - cleanup_current_project: None, -) -> None: - proj = Project.new(tmp_path, field_order=field_order) - assert proj.field_order == field_order - - -@pytest.mark.parametrize("aspect_ratio", [0.5, 1, 4]) -def test_project_pixel_aspect_ratio( - tmp_path: Path, - aspect_ratio: float, - cleanup_current_project: None, -) -> None: - proj = Project.new(tmp_path, pixel_aspect_ratio=aspect_ratio) - assert proj.pixel_aspect_ratio == aspect_ratio - - -@pytest.mark.parametrize("start_frame", [1, 2, 10, 100]) -def test_project_start_frame(test_project_obj: Project, start_frame: int) -> None: - test_project_obj.start_frame = start_frame - assert test_project_obj.start_frame == start_frame - - -def test_project_end_frame_clip_simple(test_project_obj: Project) -> None: - test_project_obj.start_frame = 1 - - clip = test_project_obj.current_clip - layer = clip.add_layer("anim") - layer.convert_to_anim_layer() - - layer.add_instance(10) - - assert test_project_obj.end_frame == 10 - - -def test_project_end_frame_clip_mark_out(test_project_obj: Project) -> None: - test_project_obj.start_frame = 3 - - clip = test_project_obj.current_clip - clip.mark_in = 4 - clip.mark_out = 8 - - layer = clip.add_layer("anim") - layer.convert_to_anim_layer() - - # The instance exceeds the mark out so it's not taken into account in clip duration - layer.add_instance(10) - - assert test_project_obj.end_frame == 7 - - -@pytest.mark.parametrize("mark_in", [1, 2, 10, 100]) -@pytest.mark.parametrize("current_frame", [0, 1, 5, 50]) -@pytest.mark.parametrize("start_frame", [2, 5, 20]) -def test_project_current_frame( - test_project_obj: Project, mark_in: int, current_frame: int, start_frame: int -) -> None: - test_project_obj.start_frame = start_frame - test_project_obj.current_clip.mark_in = mark_in - test_project_obj.current_frame = current_frame - - assert test_project_obj.current_frame == current_frame - - -def test_project_clear_background(test_project_obj: Project) -> None: - test_project_obj.clear_background() - - mode, colors = george.tv_background_get() - assert mode == george.BackgroundMode.NONE - assert colors is None - - -@pytest.mark.parametrize( - "color", - [ - george.RGBColor(0, 255, 0), - george.RGBColor(255, 255, 0), - george.RGBColor(0, 255, 255), - ], -) -def test_project_set_background_solid_color( - test_project_obj: Project, color: george.RGBColor -) -> None: - test_project_obj.background_mode = george.BackgroundMode.COLOR - test_project_obj.background_colors = color - - mode, colors = george.tv_background_get() - assert test_project_obj.background_mode == mode - assert test_project_obj.background_colors == colors - - -@pytest.mark.parametrize( - "colors", - [ - (george.RGBColor(255, 255, 255), george.RGBColor(0, 0, 0)), - (george.RGBColor(0, 255, 0), george.RGBColor(0, 255, 255)), - ], -) -def test_project_set_background_checker_colors( - test_project_obj: Project, colors: tuple[george.RGBColor, george.RGBColor] -) -> None: - test_project_obj.background_mode = george.BackgroundMode.CHECK - test_project_obj.background_colors = colors - - mode, actual_colors = george.tv_background_get() - assert test_project_obj.background_mode == mode - assert test_project_obj.background_colors == actual_colors - - -@pytest.mark.parametrize("header", ["", "Hello", "THis is a project header"]) -def test_project_header_info(test_project_obj: Project, header: str) -> None: - test_project_obj.header_info = header - assert test_project_obj.header_info == header - - -@pytest.mark.parametrize("author", ["a", "Hello", "THis is a project author"]) -def test_project_author(test_project_obj: Project, author: str) -> None: - test_project_obj.author = author - assert test_project_obj.author == author - - -@pytest.mark.parametrize("notes", ["a", "Hello", "THis is a project notes"]) -def test_project_notes(test_project_obj: Project, notes: str) -> None: - test_project_obj.notes = notes - assert test_project_obj.notes == notes - - -def test_project_get_project(test_project_obj: Project) -> None: - assert Project.get_project(by_id=test_project_obj.id) == test_project_obj - assert Project.get_project(by_name=test_project_obj.name) == test_project_obj - - -def test_project_get_project_wrong_id(test_project_obj: Project) -> None: - res = Project.get_project(by_id="unknown") - assert res is None - - -def test_project_get_project_wrong_name(test_project_obj: Project) -> None: - res = Project.get_project(by_name="name") - assert res is None - - -def test_project_current_scene_ids( - test_project_obj: Project, - create_some_scenes: list[Scene], -) -> None: - ids = list(test_project_obj.current_scene_ids()) - assert ids == [s.id for s in create_some_scenes] - - -def test_project_current_scene( - test_project_obj: Project, test_scene_obj: Scene -) -> None: - assert test_project_obj.current_scene == test_scene_obj - - -def test_project_scenes( - test_project_obj: Project, - create_some_scenes: list[Scene], -) -> None: - assert list(test_project_obj.scenes) == create_some_scenes - - -def test_project_get_scene(test_project_obj: Project, test_scene_obj: Scene) -> None: - test_project_obj.get_scene(by_id=test_scene_obj.id) - - -def test_project_add_scene(test_project_obj: Project) -> None: - scene = test_project_obj.add_scene() - assert test_project_obj.current_scene == scene - - -@pytest.mark.parametrize("index", range(5)) -def test_project_current_clip( - test_project_obj: Project, create_some_clips: list[Clip], index: int -) -> None: - clip = create_some_clips[index] - clip.make_current() - assert test_project_obj.current_clip == clip - - -def test_project_clips( - test_project_obj: Project, create_some_clips: list[Clip] -) -> None: - clips = [clip for scene in test_project_obj.scenes for clip in scene.clips] - assert list(test_project_obj.clips) == clips - - -def test_project_get_clip_by_id(test_project_obj: Project, test_clip_obj: Clip) -> None: - assert test_project_obj.get_clip(by_id=test_clip_obj.id) == test_clip_obj - - -def test_project_get_clip_by_name( - test_project_obj: Project, - test_clip_obj: Clip, -) -> None: - assert test_project_obj.get_clip(by_name=test_clip_obj.name) == test_clip_obj - - -def test_project_get_clip_by_id_scene_id( - test_project_obj: Project, - test_scene_obj: Scene, - test_clip_obj: Clip, -) -> None: - clip = test_project_obj.get_clip( - by_id=test_clip_obj.id, - scene_id=test_scene_obj.id, - ) - assert clip == test_clip_obj - - -@pytest.mark.parametrize("name", ["l", "hello", "this is my clip"]) -def test_project_add_clip( - test_project_obj: Project, test_scene_obj: Scene, name: str -) -> None: - clip = test_project_obj.add_clip(name, test_scene_obj) - assert clip.scene == test_scene_obj - assert clip.name == name - - -@pytest.mark.parametrize("name", ["l", "hello", "this is my clip"]) -def test_project_add_clip_current_scene(test_project_obj: Project, name: str) -> None: - clip = test_project_obj.add_clip(name, scene=None) - assert clip.scene == test_project_obj.current_scene - assert clip.name == name - - -def test_project_sounds( - test_project_obj: Project, - create_some_project_sounds: list[ProjectSound], -) -> None: - assert list(test_project_obj.sounds) == create_some_project_sounds - - -def test_project_add_sound(test_project_obj: Project, wav_file: Path) -> None: - test_project_obj.add_sound(wav_file) - - -def test_project_current_project_id(test_project_obj: Project) -> None: - assert Project.current_project_id() == test_project_obj.id - - -def test_project_current_project(test_project_obj: Project) -> None: - assert Project.current_project() == test_project_obj - - -def test_project_opened_project_ids(create_some_projects: list[Project]) -> None: - expected_ids = [p.id for p in create_some_projects] - assert expected_ids == list(Project.open_projects_ids()) - - -def test_project_opened_projects(create_some_projects: list[Project]) -> None: - assert create_some_projects == list(Project.open_projects()) - - -@pytest.mark.parametrize("mark_in", [1, 2, 10, 100]) -def test_project_mark_in(test_project_obj: Project, mark_in: int) -> None: - test_project_obj.mark_in = mark_in - assert test_project_obj.mark_in == mark_in - - -@pytest.mark.parametrize("mark_out", [1, 2, 10, 100]) -def test_project_mark_out(test_project_obj: Project, mark_out: int) -> None: - test_project_obj.mark_in = mark_out - assert test_project_obj.mark_in == mark_out - - -def test_project_new(tmp_path: Path, cleanup_current_project: None) -> None: - proj = Project.new(tmp_path / "project.tvpp") - assert Project.current_project() == proj - - -def test_project_new_from_camera(test_project_obj: Project) -> None: - george.tv_camera_insert_point(0, 0, 0, 0, 1) - george.tv_camera_insert_point(5, 100, 150, 45, 1) - - test_project_obj.new_from_camera() - - -def test_project_duplicate( - test_project_obj: Project, -) -> None: - dup = test_project_obj.duplicate() - assert dup != test_project_obj - dup.close() - - -def test_project_close(test_project_obj: Project) -> None: - test_project_obj.close() - assert test_project_obj.is_closed - - # The project can't be refreshed - with pytest.raises(ValueError): - test_project_obj.refresh() - - -def test_project_close_all(create_some_projects: list[Project]) -> None: - Project.close_all() - assert all(p.is_closed for p in create_some_projects) - - -def test_project_load(test_project_obj: Project) -> None: - test_project_obj.save() - assert Project.load(test_project_obj.path) == test_project_obj - - -def test_project_save(test_project_obj: Project, tmp_path: Path) -> None: - test_project_obj.save(tmp_path / "save.tvpp") - - -def test_project_save_destination_does_not_exist( - test_project_obj: Project, tmp_path: Path -) -> None: - with pytest.raises(ValueError, match="folder does not exist"): - test_project_obj.save(tmp_path / "lo" / "save.tvpp") +# from pathlib import Path +# +# import pytest +# +# from pytvpaint import george +# from pytvpaint.clip import Clip +# from pytvpaint.george.grg_project import TVPProject +# from pytvpaint.project import Project +# from pytvpaint.scene import Scene +# from pytvpaint.sound import ProjectSound +# from tests.conftest import FixtureYield +# +# +# def test_project_init(test_project: TVPProject) -> None: +# project = Project(test_project.id) +# assert project.id == test_project.id +# +# +# def test_project_refresh(test_project_obj: Project) -> None: +# test_project_obj.refresh() +# +# +# def test_project_position(test_project_obj: Project) -> None: +# pos = test_project_obj.position +# assert george.tv_project_enum_id(pos) == test_project_obj.id +# +# +# def test_project_closed(test_project_obj: Project) -> None: +# assert not test_project_obj.is_closed +# george.tv_project_close(test_project_obj.id) +# assert test_project_obj.is_closed +# +# +# def test_project_exists_on_disk(test_project_obj: Project) -> None: +# assert not test_project_obj.exists +# george.tv_save_project(test_project_obj.path) +# assert test_project_obj.exists +# +# +# @pytest.fixture() +# def other_project(tmp_path: Path) -> FixtureYield[None]: +# other_project = george.tv_project_new( +# tmp_path / "other.tvpp", width=1234, height=567 +# ) +# yield +# george.tv_project_close(other_project) +# +# +# def test_project_is_current(test_project_obj: Project, other_project: None) -> None: +# assert not test_project_obj.is_current +# george.tv_project_select(test_project_obj.id) +# assert test_project_obj.is_current +# +# +# def test_project_make_current(test_project_obj: Project, other_project: None) -> None: +# test_project_obj.make_current() +# assert test_project_obj.is_current +# +# +# def test_project_path(test_project_obj: Project, tmp_path: Path) -> None: +# new_path = tmp_path / "new_location.tvpp" +# george.tv_save_project(new_path) +# assert test_project_obj.path == new_path +# +# +# def test_project_name(test_project_obj: Project, tmp_path: Path) -> None: +# new_path = tmp_path / "new_location.tvpp" +# george.tv_save_project(new_path) +# assert test_project_obj.name == new_path.stem +# +# +# def test_project_width_height(test_project_obj: Project, other_project: None) -> None: +# assert test_project_obj.width == 1920 +# assert test_project_obj.height == 1080 +# +# +# def test_project_resize_same_width_and_height(test_project_obj: Project) -> None: +# resized = test_project_obj.resize(test_project_obj.width, test_project_obj.height) +# assert resized == test_project_obj +# +# +# def test_project_resize( +# test_project_obj: Project, cleanup_current_project: None +# ) -> None: +# resized = test_project_obj.resize(100, 200) +# +# # The resized project is a new project +# assert resized != test_project_obj +# +# assert resized.width == 100 +# assert resized.height == 200 +# +# # It closes the project but it still exists on disk +# assert test_project_obj.is_closed +# +# +# def test_project_resize_overwrite( +# test_project_obj: Project, cleanup_current_project: None +# ) -> None: +# origin_path = test_project_obj.path +# +# resized = test_project_obj.resize(100, 200, overwrite=True) +# resized.close() +# +# # Verify that the original file is overwritten +# resized_load = Project.load(origin_path) +# assert resized == resized_load +# assert resized.width == resized_load.width +# assert resized.height == resized_load.height +# +# +# def test_project_fps(test_project_obj: Project) -> None: +# test_project_obj.set_fps(54) +# assert test_project_obj.fps == 54 +# +# +# def test_project_fps_preview(test_project_obj: Project) -> None: +# test_project_obj.set_fps(54, preview=True) +# assert test_project_obj.playback_fps == 54 +# +# +# @pytest.mark.parametrize("field_order", george.FieldOrder) +# def test_project_field_order( +# tmp_path: Path, +# field_order: george.FieldOrder, +# cleanup_current_project: None, +# ) -> None: +# proj = Project.new(tmp_path, field_order=field_order) +# assert proj.field_order == field_order +# +# +# @pytest.mark.parametrize("aspect_ratio", [0.5, 1, 4]) +# def test_project_pixel_aspect_ratio( +# tmp_path: Path, +# aspect_ratio: float, +# cleanup_current_project: None, +# ) -> None: +# proj = Project.new(tmp_path, pixel_aspect_ratio=aspect_ratio) +# assert proj.pixel_aspect_ratio == aspect_ratio +# +# +# @pytest.mark.parametrize("start_frame", [1, 2, 10, 100]) +# def test_project_start_frame(test_project_obj: Project, start_frame: int) -> None: +# test_project_obj.start_frame = start_frame +# assert test_project_obj.start_frame == start_frame +# +# +# def test_project_end_frame_clip_simple(test_project_obj: Project) -> None: +# test_project_obj.start_frame = 1 +# +# clip = test_project_obj.current_clip +# layer = clip.add_layer("anim") +# layer.convert_to_anim_layer() +# +# layer.add_instance(10) +# +# assert test_project_obj.end_frame == 10 +# +# +# def test_project_end_frame_clip_mark_out(test_project_obj: Project) -> None: +# test_project_obj.start_frame = 3 +# +# clip = test_project_obj.current_clip +# clip.mark_in = 4 +# clip.mark_out = 8 +# +# layer = clip.add_layer("anim") +# layer.convert_to_anim_layer() +# +# # The instance exceeds the mark out so it's not taken into account in clip duration +# layer.add_instance(10) +# +# assert test_project_obj.end_frame == 7 +# +# +# @pytest.mark.parametrize("mark_in", [1, 2, 10, 100]) +# @pytest.mark.parametrize("current_frame", [0, 1, 5, 50]) +# @pytest.mark.parametrize("start_frame", [2, 5, 20]) +# def test_project_current_frame( +# test_project_obj: Project, mark_in: int, current_frame: int, start_frame: int +# ) -> None: +# test_project_obj.start_frame = start_frame +# test_project_obj.current_clip.mark_in = mark_in +# test_project_obj.current_frame = current_frame +# +# assert test_project_obj.current_frame == current_frame +# +# +# def test_project_clear_background(test_project_obj: Project) -> None: +# test_project_obj.clear_background() +# +# mode, colors = george.tv_background_get() +# assert mode == george.BackgroundMode.NONE +# assert colors is None +# +# +# @pytest.mark.parametrize( +# "color", +# [ +# george.RGBColor(0, 255, 0), +# george.RGBColor(255, 255, 0), +# george.RGBColor(0, 255, 255), +# ], +# ) +# def test_project_set_background_solid_color( +# test_project_obj: Project, color: george.RGBColor +# ) -> None: +# test_project_obj.background_mode = george.BackgroundMode.COLOR +# test_project_obj.background_colors = color +# +# mode, colors = george.tv_background_get() +# assert test_project_obj.background_mode == mode +# assert test_project_obj.background_colors == colors +# +# +# @pytest.mark.parametrize( +# "colors", +# [ +# (george.RGBColor(255, 255, 255), george.RGBColor(0, 0, 0)), +# (george.RGBColor(0, 255, 0), george.RGBColor(0, 255, 255)), +# ], +# ) +# def test_project_set_background_checker_colors( +# test_project_obj: Project, colors: tuple[george.RGBColor, george.RGBColor] +# ) -> None: +# test_project_obj.background_mode = george.BackgroundMode.CHECK +# test_project_obj.background_colors = colors +# +# mode, actual_colors = george.tv_background_get() +# assert test_project_obj.background_mode == mode +# assert test_project_obj.background_colors == actual_colors +# +# +# @pytest.mark.parametrize("header", ["", "Hello", "THis is a project header"]) +# def test_project_header_info(test_project_obj: Project, header: str) -> None: +# test_project_obj.header_info = header +# assert test_project_obj.header_info == header +# +# +# @pytest.mark.parametrize("author", ["a", "Hello", "THis is a project author"]) +# def test_project_author(test_project_obj: Project, author: str) -> None: +# test_project_obj.author = author +# assert test_project_obj.author == author +# +# +# @pytest.mark.parametrize("notes", ["a", "Hello", "THis is a project notes"]) +# def test_project_notes(test_project_obj: Project, notes: str) -> None: +# test_project_obj.notes = notes +# assert test_project_obj.notes == notes +# +# +# def test_project_get_project(test_project_obj: Project) -> None: +# assert Project.get_project(by_id=test_project_obj.id) == test_project_obj +# assert Project.get_project(by_name=test_project_obj.name) == test_project_obj +# +# +# def test_project_get_project_wrong_id(test_project_obj: Project) -> None: +# res = Project.get_project(by_id="unknown") +# assert res is None +# +# +# def test_project_get_project_wrong_name(test_project_obj: Project) -> None: +# res = Project.get_project(by_name="name") +# assert res is None +# +# +# def test_project_current_scene_ids( +# test_project_obj: Project, +# create_some_scenes: list[Scene], +# ) -> None: +# ids = list(test_project_obj.current_scene_ids()) +# assert ids == [s.id for s in create_some_scenes] +# +# +# def test_project_current_scene( +# test_project_obj: Project, test_scene_obj: Scene +# ) -> None: +# assert test_project_obj.current_scene == test_scene_obj +# +# +# def test_project_scenes( +# test_project_obj: Project, +# create_some_scenes: list[Scene], +# ) -> None: +# assert list(test_project_obj.scenes) == create_some_scenes +# +# +# def test_project_get_scene(test_project_obj: Project, test_scene_obj: Scene) -> None: +# test_project_obj.get_scene(by_id=test_scene_obj.id) +# +# +# def test_project_add_scene(test_project_obj: Project) -> None: +# scene = test_project_obj.add_scene() +# assert test_project_obj.current_scene == scene +# +# +# @pytest.mark.parametrize("index", range(5)) +# def test_project_current_clip( +# test_project_obj: Project, create_some_clips: list[Clip], index: int +# ) -> None: +# clip = create_some_clips[index] +# clip.make_current() +# assert test_project_obj.current_clip == clip +# +# +# def test_project_clips( +# test_project_obj: Project, create_some_clips: list[Clip] +# ) -> None: +# clips = [clip for scene in test_project_obj.scenes for clip in scene.clips] +# assert list(test_project_obj.clips) == clips +# +# +# def test_project_get_clip_by_id(test_project_obj: Project, test_clip_obj: Clip) -> None: +# assert test_project_obj.get_clip(by_id=test_clip_obj.id) == test_clip_obj +# +# +# def test_project_get_clip_by_name( +# test_project_obj: Project, +# test_clip_obj: Clip, +# ) -> None: +# assert test_project_obj.get_clip(by_name=test_clip_obj.name) == test_clip_obj +# +# +# def test_project_get_clip_by_id_scene_id( +# test_project_obj: Project, +# test_scene_obj: Scene, +# test_clip_obj: Clip, +# ) -> None: +# clip = test_project_obj.get_clip( +# by_id=test_clip_obj.id, +# scene_id=test_scene_obj.id, +# ) +# assert clip == test_clip_obj +# +# +# @pytest.mark.parametrize("name", ["l", "hello", "this is my clip"]) +# def test_project_add_clip( +# test_project_obj: Project, test_scene_obj: Scene, name: str +# ) -> None: +# clip = test_project_obj.add_clip(name, test_scene_obj) +# assert clip.scene == test_scene_obj +# assert clip.name == name +# +# +# @pytest.mark.parametrize("name", ["l", "hello", "this is my clip"]) +# def test_project_add_clip_current_scene(test_project_obj: Project, name: str) -> None: +# clip = test_project_obj.add_clip(name, scene=None) +# assert clip.scene == test_project_obj.current_scene +# assert clip.name == name +# +# +# def test_project_sounds( +# test_project_obj: Project, +# create_some_project_sounds: list[ProjectSound], +# ) -> None: +# assert list(test_project_obj.sounds) == create_some_project_sounds +# +# +# def test_project_add_sound(test_project_obj: Project, wav_file: Path) -> None: +# test_project_obj.add_sound(wav_file) +# +# +# def test_project_current_project_id(test_project_obj: Project) -> None: +# assert Project.current_project_id() == test_project_obj.id +# +# +# def test_project_current_project(test_project_obj: Project) -> None: +# assert Project.current_project() == test_project_obj +# +# +# def test_project_opened_project_ids(create_some_projects: list[Project]) -> None: +# expected_ids = [p.id for p in create_some_projects] +# assert expected_ids == list(Project.open_projects_ids()) +# +# +# def test_project_opened_projects(create_some_projects: list[Project]) -> None: +# assert create_some_projects == list(Project.open_projects()) +# +# +# @pytest.mark.parametrize("mark_in", [1, 2, 10, 100]) +# def test_project_mark_in(test_project_obj: Project, mark_in: int) -> None: +# test_project_obj.mark_in = mark_in +# assert test_project_obj.mark_in == mark_in +# +# +# @pytest.mark.parametrize("mark_out", [1, 2, 10, 100]) +# def test_project_mark_out(test_project_obj: Project, mark_out: int) -> None: +# test_project_obj.mark_in = mark_out +# assert test_project_obj.mark_in == mark_out +# +# +# def test_project_new(tmp_path: Path, cleanup_current_project: None) -> None: +# proj = Project.new(tmp_path / "project.tvpp") +# assert Project.current_project() == proj +# +# +# def test_project_new_from_camera(test_project_obj: Project) -> None: +# george.tv_camera_insert_point(0, 0, 0, 0, 1) +# george.tv_camera_insert_point(5, 100, 150, 45, 1) +# +# test_project_obj.new_from_camera() +# +# +# def test_project_duplicate( +# test_project_obj: Project, +# ) -> None: +# dup = test_project_obj.duplicate() +# assert dup != test_project_obj +# dup.close() +# +# +# def test_project_close(test_project_obj: Project) -> None: +# test_project_obj.close() +# assert test_project_obj.is_closed +# +# # The project can't be refreshed +# with pytest.raises(ValueError): +# test_project_obj.refresh() +# +# +# def test_project_close_all(create_some_projects: list[Project]) -> None: +# Project.close_all() +# assert all(p.is_closed for p in create_some_projects) +# +# +# def test_project_load(test_project_obj: Project) -> None: +# test_project_obj.save() +# assert Project.load(test_project_obj.path) == test_project_obj +# +# +# def test_project_save(test_project_obj: Project, tmp_path: Path) -> None: +# test_project_obj.save(tmp_path / "save.tvpp") +# +# +# def test_project_save_destination_does_not_exist( +# test_project_obj: Project, tmp_path: Path +# ) -> None: +# with pytest.raises(ValueError, match="folder does not exist"): +# test_project_obj.save(tmp_path / "lo" / "save.tvpp") diff --git a/tests/test_project_sound.py b/tests/test_project_sound.py index 20950c5..c235eb2 100644 --- a/tests/test_project_sound.py +++ b/tests/test_project_sound.py @@ -1,21 +1,21 @@ -import pytest - -from pytvpaint.george.exceptions import GeorgeError -from pytvpaint.sound import ProjectSound - - -def test_project_sound_init(test_project_sound: ProjectSound) -> None: - assert ProjectSound(track_index=0) == test_project_sound - - -def test_project_sound_remove(test_project_sound: ProjectSound) -> None: - index = test_project_sound.track_index - test_project_sound.remove() - - # The project sound doesn't exist anymore - with pytest.raises(GeorgeError): - ProjectSound(track_index=index) - - -def test_project_sound_reload(test_project_sound: ProjectSound) -> None: - test_project_sound.reload() +# import pytest +# +# from pytvpaint.george.exceptions import GeorgeError +# from pytvpaint.sound import ProjectSound +# +# +# def test_project_sound_init(test_project_sound: ProjectSound) -> None: +# assert ProjectSound(track_index=0) == test_project_sound +# +# +# def test_project_sound_remove(test_project_sound: ProjectSound) -> None: +# index = test_project_sound.track_index +# test_project_sound.remove() +# +# # The project sound doesn't exist anymore +# with pytest.raises(GeorgeError): +# ProjectSound(track_index=index) +# +# +# def test_project_sound_reload(test_project_sound: ProjectSound) -> None: +# test_project_sound.reload() diff --git a/tests/test_scene.py b/tests/test_scene.py index 860f628..f3d9f3c 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -1,88 +1,88 @@ -import pytest - -from pytvpaint import george -from pytvpaint.clip import Clip -from pytvpaint.project import Project -from pytvpaint.scene import Scene - - -def test_scene_init(test_project_obj: Project, test_scene: int) -> None: - scene = Scene(test_scene, test_project_obj) - assert scene.id == test_scene - assert scene.project == test_project_obj - assert scene.position == 1 # It's the second scene - - -def test_scene_new_current_project(test_project_obj: Project) -> None: - scene = Scene.new() - # The scene's project is the current project - assert scene.project == test_project_obj - - -def test_scene_new_other_project(test_project_obj: Project) -> None: - scene = Scene.new(project=test_project_obj) - # The scene's project is the given project - assert scene.project == test_project_obj - - -def test_scene_current_scene_id_static(test_scene: int) -> None: - assert Scene.current_scene_id() == test_scene - - -def test_scene_current_scene_static(test_scene_obj: Scene) -> None: - assert Scene.current_scene() == test_scene_obj - - -def test_scene_is_current(test_project_obj: Project, test_scene_obj: Scene) -> None: - assert test_scene_obj.is_current - george.tv_scene_new() - assert not test_scene_obj.is_current - - -def test_scene_make_current(test_scene_obj: Scene) -> None: - george.tv_scene_new() - - # Because we created another scene which is current - assert not test_scene_obj.is_current - - test_scene_obj.make_current() - assert test_scene_obj.is_current - - -@pytest.mark.parametrize("pos", range(5)) -def test_scene_position_getter( - test_scene_obj: Scene, - create_some_scenes: None, - pos: int, -) -> None: - george.tv_scene_move(test_scene_obj.id, pos) - assert pos == test_scene_obj.position - - -@pytest.mark.parametrize("pos", range(5)) -def test_scene_position_setter( - test_scene_obj: Scene, - create_some_scenes: None, - pos: int, -) -> None: - test_scene_obj.position = pos - assert test_scene_obj.position == pos - - -def test_scene_clips( - test_scene_obj: Scene, - create_some_clips: list[Clip], -) -> None: - assert list(Scene.current_scene().clips) == create_some_clips - - -def test_scene_duplicate(test_scene_obj: Scene) -> None: - dup = test_scene_obj.duplicate() - assert test_scene_obj != dup - - -def test_scene_remove(test_scene_obj: Scene) -> None: - test_scene_obj.remove() - - with pytest.raises(ValueError, match="has been removed"): - test_scene_obj.id +# import pytest +# +# from pytvpaint import george +# from pytvpaint.clip import Clip +# from pytvpaint.project import Project +# from pytvpaint.scene import Scene +# +# +# def test_scene_init(test_project_obj: Project, test_scene: int) -> None: +# scene = Scene(test_scene, test_project_obj) +# assert scene.id == test_scene +# assert scene.project == test_project_obj +# assert scene.position == 1 # It's the second scene +# +# +# def test_scene_new_current_project(test_project_obj: Project) -> None: +# scene = Scene.new() +# # The scene's project is the current project +# assert scene.project == test_project_obj +# +# +# def test_scene_new_other_project(test_project_obj: Project) -> None: +# scene = Scene.new(project=test_project_obj) +# # The scene's project is the given project +# assert scene.project == test_project_obj +# +# +# def test_scene_current_scene_id_static(test_scene: int) -> None: +# assert Scene.current_scene_id() == test_scene +# +# +# def test_scene_current_scene_static(test_scene_obj: Scene) -> None: +# assert Scene.current_scene() == test_scene_obj +# +# +# def test_scene_is_current(test_project_obj: Project, test_scene_obj: Scene) -> None: +# assert test_scene_obj.is_current +# george.tv_scene_new() +# assert not test_scene_obj.is_current +# +# +# def test_scene_make_current(test_scene_obj: Scene) -> None: +# george.tv_scene_new() +# +# # Because we created another scene which is current +# assert not test_scene_obj.is_current +# +# test_scene_obj.make_current() +# assert test_scene_obj.is_current +# +# +# @pytest.mark.parametrize("pos", range(5)) +# def test_scene_position_getter( +# test_scene_obj: Scene, +# create_some_scenes: None, +# pos: int, +# ) -> None: +# george.tv_scene_move(test_scene_obj.id, pos) +# assert pos == test_scene_obj.position +# +# +# @pytest.mark.parametrize("pos", range(5)) +# def test_scene_position_setter( +# test_scene_obj: Scene, +# create_some_scenes: None, +# pos: int, +# ) -> None: +# test_scene_obj.position = pos +# assert test_scene_obj.position == pos +# +# +# def test_scene_clips( +# test_scene_obj: Scene, +# create_some_clips: list[Clip], +# ) -> None: +# assert list(Scene.current_scene().clips) == create_some_clips +# +# +# def test_scene_duplicate(test_scene_obj: Scene) -> None: +# dup = test_scene_obj.duplicate() +# assert test_scene_obj != dup +# +# +# def test_scene_remove(test_scene_obj: Scene) -> None: +# test_scene_obj.remove() +# +# with pytest.raises(ValueError, match="has been removed"): +# test_scene_obj.id diff --git a/tests/test_utils.py b/tests/test_utils.py index e860b59..ffb98aa 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,25 +1,25 @@ -from __future__ import annotations - -import pytest - -from pytvpaint.utils import get_unique_name - - -@pytest.mark.parametrize( - "test_case", - [ - (["cl001"], "cl", "cl002"), - (["cl"], "cl", "cl2"), - (["a01", "a001"], "a", "a002"), - (["a001"], "a001b", "a001b"), - (["a001"], "a001", "a002"), - (["cl001", "cl005"], "cl", "cl006"), - (["001", "003"], "003", "004"), - (["l"], "tset0", "tset0"), - ([], "tset0", "tset0"), - (["cl0001", "cl005"], "cl", "cl0006"), - ], -) -def test_get_unique_name(test_case: tuple[list[str], str, str]) -> None: - current_names, name, expected = test_case - assert get_unique_name(current_names, name) == expected +# from __future__ import annotations +# +# import pytest +# +# from pytvpaint.utils import get_unique_name +# +# +# @pytest.mark.parametrize( +# "test_case", +# [ +# (["cl001"], "cl", "cl002"), +# (["cl"], "cl", "cl2"), +# (["a01", "a001"], "a", "a002"), +# (["a001"], "a001b", "a001b"), +# (["a001"], "a001", "a002"), +# (["cl001", "cl005"], "cl", "cl006"), +# (["001", "003"], "003", "004"), +# (["l"], "tset0", "tset0"), +# ([], "tset0", "tset0"), +# (["cl0001", "cl005"], "cl", "cl0006"), +# ], +# ) +# def test_get_unique_name(test_case: tuple[list[str], str, str]) -> None: +# current_names, name, expected = test_case +# assert get_unique_name(current_names, name) == expected From 9e282b7400b6a9d09f3cf22a8256427a0a60d1af Mon Sep 17 00:00:00 2001 From: Radouane Lahmidi Date: Tue, 1 Apr 2025 10:27:07 +0200 Subject: [PATCH 02/26] CLEAN code and documentation --- docs/credits.md | 2 +- docs/limitations.md | 54 ++ poetry.lock | 1162 ++++++++++++------------- pytvpaint/camera.py | 52 +- pytvpaint/clip.py | 52 +- pytvpaint/george/client/parse.py | 14 +- pytvpaint/george/grg_base.py | 10 +- pytvpaint/george/grg_camera.py | 20 +- pytvpaint/george/grg_layer.py | 37 +- pytvpaint/george/grg_project.py | 17 +- pytvpaint/george/grg_scene.py | 3 +- pytvpaint/layer.py | 58 +- pytvpaint/project.py | 3 - pytvpaint/scene.py | 18 +- tests/george/test_grg_camera.py | 10 +- tests/george/test_grg_clip.py | 1355 +++++++++++++++--------------- 16 files changed, 1491 insertions(+), 1376 deletions(-) diff --git a/docs/credits.md b/docs/credits.md index 73ad9ae..97b438f 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -19,7 +19,7 @@ - The C++ plugin was inspired from existing codebase of Ynput's [OpenPype TVPaint plugin](https://github.com/ynput/OpenPype/tree/develop/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code). - 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. +- The TVPaint dev team for their patience and help with our questions and the [George commands documentation](https://doc.tvpaint.com/docs/george/instructions-commands) from TVPaint. ## :snake: Logo diff --git a/docs/limitations.md b/docs/limitations.md index 3977847..7f3a673 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -92,3 +92,57 @@ issues in the table below: | `tv_SoundClipReload` | Doesn't accept a proper clip id, only `0` seems to work for the current clip | | `tv_LayerSelectInfo` | Does not select frames as stated in the documentation and will also return non selected frames if attribute `full` is set to True | | `tv_ProjectSaveAudioDependencies` and `tv_ProjectSaveVideoDependencies` | Missing arguments in documentation rendering the function useless, thankfully someone provided the correct details [here](http://tvpaint.net/forum/viewtopic.php?p=136709) | + + +## TVPaint 12 Bugs and Breaking Changes + +TVPaint 12 introduces a lot of welcome changes (especially for the artists) and some new needed function for +developers. However it also introduces a lot of breaking changes and some new bugs. + +To deal with these changes as well as the new functions exclusive to TVP 12, we added a couple decorators and functions + +| Method | Description | +|:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| +| [`min_version_compatible`](api/george/misc.md#pytvpaint.george.grg_base.min_version_compatible) | When used as decorator, checks if the tvp instance making the call is above the miniumum version needed. | +| [`deprecated_warning`](api/george/misc.md#pytvpaint.george.grg_base.deprecated_warning) | When used as decorator, will log a warning message anytime the decorated function is called. | +| [`is_tvp_version_below_12`](api/george/misc.md#pytvpaint.george.grg_base.is_tvp_version_below_12) | Returns a True if the tvp instances version is below 12. | + + +Below is also a list of the current breaking changes and bugs we noticed during development + +### C++ Plugin : +* [BUG] plugin PIRF_HIDDEN_REQ doesn't seem to be working anymore, the plugin window is now visible and closing it kills the plugin with no way to restart it without restarting tvpaint. + +### Project : +* [DEPRECATED/BREAKING] [`tv_ProjectInfo`](api/george/project.md#pytvpaint.george.grg_project.tv_project_info) values of `field_order` have been removed so for now we provide it ourselves. + +### Scene : +* [BUG] [`tv_SceneCreate`](api/george/scene.md#pytvpaint.george.grg_scene.tv_scene_create) doesn't seem to work. + +### Layers : +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually tv_CTGGetSource without the `s` at the end. +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) actually requires and returns layer Ids not names. +* [FEATURE] Some function now return a clear error message which is much appreciated (Ex: [`tv_CTGSource`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_source_add)) +* child layers have no way of knowing if they are in a folder layer or which one. +* Folder layer has no way of knowing which layers are its children. +* [BUG] Creating a CTG layer from a folder crashes TVPaint. +* [BUG] Moving a layer that is already in a folder in the same folder crashes TVPaint. +* [`tv_LayerMove`](api/george/layer.md#pytvpaint.george.grg_layer.tv_layer_move) can now move layers into folders but the position is relative to the root and not the folder, + this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers in and out of folders. +* Not providing a `FolderID` to [`tv_LayerMove`](api/george/layer.md#pytvpaint.george.grg_layer.tv_layer_move) doesn't + move the layer to the root, you just need to move outside the folder range for it to work, which again is pretty tough to do since we can't get the child layers of a folder and therefore their positions. +* Moving a layer inside a folder can be done without providing a `FolderID`, just by moving the layer in the folder's range. +* [BUG] UI doesn't always update when values are set/updated via code, you either have to wait a few seconds for a refresh, or click somewhere else, this might lead to errors when users are using the UI at the same time. +* [BUG] CTG layer sometimes takes a while to update in the UI (a few seconds), which means they can be unintentionally edited or reset before the update. +* CameraLayer is not really a layer and most layer functions will ignore it, prefer use of PyTVPaint [`Camera`](api/objects/camera.md#pytvpaint.camera.Camera) object instead. +* [BUG] Selecting the Camera Layer in the UI now disables/greys out most layer related tools (this is a new behaviour), this causes a lot of errors when using pipeline tools or + TVPaint panels as TVPaint now raises an error messages anytime you try to use a tool that is not camera related when the camera layer is selected. + The problem is that the Camera layer is now also the default layer and selected by default when creating/opening any project. + +### Camera : +* [DEPRECATED/BREAKING] Camera.anti_aliasing always returns 1, [`Camera.fps`](api/objects/camera.md#pytvpaint.camera.Camera.fps) no longer returns/sets fps, now only fps is Project fps. +* [BUG] Most Camera values can still be edited/queried using [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) but they are not reflected in + UI, and so they can be unintentionally edited or reset. +* [DEPRECATED/BREAKING] [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) values of pixel aspect ratio and fps have been "swapped" (since camera fps is no longer provided). +* [BUG] [`tv_CameraInterpolation`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_interpolation) doesn't work properly in TVP12 and always returns an "empty" point. + diff --git a/poetry.lock b/poetry.lock index 884cf94..c897057 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,48 +1,66 @@ -# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "babel" -version = "2.14.0" +version = "2.17.0" description = "Internationalization utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[package.extras] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] + +[[package]] +name = "backrefs" +version = "5.8" +description = "A wrapper around re and regex that adds additional back references." +optional = false +python-versions = ">=3.9" files = [ - {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, - {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, + {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, + {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, + {file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"}, + {file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"}, + {file = "backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc"}, + {file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"}, ] [package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +extras = ["regex"] [[package]] name = "black" -version = "24.3.0" +version = "24.10.0" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {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"}, + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, ] [package.dependencies] @@ -56,129 +74,131 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.2.2" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -197,63 +217,74 @@ files = [ [[package]] name = "coverage" -version = "7.4.4" +version = "7.7.0" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {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"}, + {file = "coverage-7.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a538a23119d1e2e2ce077e902d02ea3d8e0641786ef6e0faf11ce82324743944"}, + {file = "coverage-7.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1586ad158523f4133499a4f322b230e2cfef9cc724820dbd58595a5a236186f4"}, + {file = "coverage-7.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b6c96d69928a3a6767fab8dc1ce8a02cf0156836ccb1e820c7f45a423570d98"}, + {file = "coverage-7.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f18d47641282664276977c604b5a261e51fefc2980f5271d547d706b06a837f"}, + {file = "coverage-7.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a1e18a85bd066c7c556d85277a7adf4651f259b2579113844835ba1a74aafd"}, + {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:70f0925c4e2bfc965369f417e7cc72538fd1ba91639cf1e4ef4b1a6b50439b3b"}, + {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b0fac2088ec4aaeb5468b814bd3ff5e5978364bfbce5e567c44c9e2854469f6c"}, + {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3e212a894d8ae07fde2ca8b43d666a6d49bbbddb10da0f6a74ca7bd31f20054"}, + {file = "coverage-7.7.0-cp310-cp310-win32.whl", hash = "sha256:f32b165bf6dfea0846a9c9c38b7e1d68f313956d60a15cde5d1709fddcaf3bee"}, + {file = "coverage-7.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2454b12a3f12cc4698f3508912e6225ec63682e2ca5a96f80a2b93cef9e63f3"}, + {file = "coverage-7.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0a207c87a9f743c8072d059b4711f8d13c456eb42dac778a7d2e5d4f3c253a7"}, + {file = "coverage-7.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d673e3add00048215c2cc507f1228a7523fd8bf34f279ac98334c9b07bd2656"}, + {file = "coverage-7.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f81fe93dc1b8e5673f33443c0786c14b77e36f1025973b85e07c70353e46882b"}, + {file = "coverage-7.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8c7524779003d59948c51b4fcbf1ca4e27c26a7d75984f63488f3625c328b9b"}, + {file = "coverage-7.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c124025430249118d018dcedc8b7426f39373527c845093132196f2a483b6dd"}, + {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f559c36d5cdc448ee13e7e56ed7b6b5d44a40a511d584d388a0f5d940977ba"}, + {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:37cbc7b0d93dfd133e33c7ec01123fbb90401dce174c3b6661d8d36fb1e30608"}, + {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7d2a65876274acf544703e943c010b60bd79404e3623a1e5d52b64a6e2728de5"}, + {file = "coverage-7.7.0-cp311-cp311-win32.whl", hash = "sha256:f5a2f71d6a91238e7628f23538c26aa464d390cbdedf12ee2a7a0fb92a24482a"}, + {file = "coverage-7.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae8006772c6b0fa53c33747913473e064985dac4d65f77fd2fdc6474e7cd54e4"}, + {file = "coverage-7.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:056d3017ed67e7ddf266e6f57378ece543755a4c9231e997789ab3bd11392c94"}, + {file = "coverage-7.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33c1394d8407e2771547583b66a85d07ed441ff8fae5a4adb4237ad39ece60db"}, + {file = "coverage-7.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fbb7a0c3c21908520149d7751cf5b74eb9b38b54d62997b1e9b3ac19a8ee2fe"}, + {file = "coverage-7.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb356e7ae7c2da13f404bf8f75be90f743c6df8d4607022e759f5d7d89fe83f8"}, + {file = "coverage-7.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce730d484038e97f27ea2dbe5d392ec5c2261f28c319a3bb266f6b213650135"}, + {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa4dff57fc21a575672176d5ab0ef15a927199e775c5e8a3d75162ab2b0c7705"}, + {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b667b91f4f714b17af2a18e220015c941d1cf8b07c17f2160033dbe1e64149f0"}, + {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:693d921621a0c8043bfdc61f7d4df5ea6d22165fe8b807cac21eb80dd94e4bbd"}, + {file = "coverage-7.7.0-cp312-cp312-win32.whl", hash = "sha256:52fc89602cde411a4196c8c6894afb384f2125f34c031774f82a4f2608c59d7d"}, + {file = "coverage-7.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ce8cf59e09d31a4915ff4c3b94c6514af4c84b22c4cc8ad7c3c546a86150a92"}, + {file = "coverage-7.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4545485fef7a8a2d8f30e6f79ce719eb154aab7e44217eb444c1d38239af2072"}, + {file = "coverage-7.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1393e5aa9441dafb0162c36c8506c648b89aea9565b31f6bfa351e66c11bcd82"}, + {file = "coverage-7.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:316f29cc3392fa3912493ee4c83afa4a0e2db04ff69600711f8c03997c39baaa"}, + {file = "coverage-7.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ffde1d6bc2a92f9c9207d1ad808550873748ac2d4d923c815b866baa343b3f"}, + {file = "coverage-7.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:416e2a8845eaff288f97eaf76ab40367deafb9073ffc47bf2a583f26b05e5265"}, + {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5efdeff5f353ed3352c04e6b318ab05c6ce9249c25ed3c2090c6e9cadda1e3b2"}, + {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:57f3bd0d29bf2bd9325c0ff9cc532a175110c4bf8f412c05b2405fd35745266d"}, + {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ab7090f04b12dc6469882ce81244572779d3a4b67eea1c96fb9ecc8c607ef39"}, + {file = "coverage-7.7.0-cp313-cp313-win32.whl", hash = "sha256:180e3fc68ee4dc5af8b33b6ca4e3bb8aa1abe25eedcb958ba5cff7123071af68"}, + {file = "coverage-7.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:55143aa13c49491f5606f05b49ed88663446dce3a4d3c5d77baa4e36a16d3573"}, + {file = "coverage-7.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc41374d2f27d81d6558f8a24e5c114580ffefc197fd43eabd7058182f743322"}, + {file = "coverage-7.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:89078312f06237417adda7c021c33f80f7a6d2db8572a5f6c330d89b080061ce"}, + {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b2f144444879363ea8834cd7b6869d79ac796cb8f864b0cfdde50296cd95816"}, + {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60e6347d1ed882b1159ffea172cb8466ee46c665af4ca397edbf10ff53e9ffaf"}, + {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb203c0afffaf1a8f5b9659a013f8f16a1b2cad3a80a8733ceedc968c0cf4c57"}, + {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ad0edaa97cb983d9f2ff48cadddc3e1fb09f24aa558abeb4dc9a0dbacd12cbb4"}, + {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c5f8a5364fc37b2f172c26a038bc7ec4885f429de4a05fc10fdcb53fb5834c5c"}, + {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4e09534037933bf6eb31d804e72c52ec23219b32c1730f9152feabbd7499463"}, + {file = "coverage-7.7.0-cp313-cp313t-win32.whl", hash = "sha256:1b336d06af14f8da5b1f391e8dec03634daf54dfcb4d1c4fb6d04c09d83cef90"}, + {file = "coverage-7.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b54a1ee4c6f1905a436cbaa04b26626d27925a41cbc3a337e2d3ff7038187f07"}, + {file = "coverage-7.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c8fbce80b2b8bf135d105aa8f5b36eae0c57d702a1cc3ebdea2a6f03f6cdde5"}, + {file = "coverage-7.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9710521f07f526de30ccdead67e6b236fe996d214e1a7fba8b36e2ba2cd8261"}, + {file = "coverage-7.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7789e700f33f2b133adae582c9f437523cd5db8de845774988a58c360fc88253"}, + {file = "coverage-7.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c36093aca722db73633cf2359026ed7782a239eb1c6db2abcff876012dc4cf"}, + {file = "coverage-7.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c075d167a6ec99b798c1fdf6e391a1d5a2d054caffe9593ba0f97e3df2c04f0e"}, + {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d013c07061751ae81861cae6ec3a4fe04e84781b11fd4b6b4201590234b25c7b"}, + {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:104bf640f408f4e115b85110047c7f27377e1a8b7ba86f7db4fa47aa49dc9a8e"}, + {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:39abcacd1ed54e2c33c54bdc488b310e8ef6705833f7148b6eb9a547199d375d"}, + {file = "coverage-7.7.0-cp39-cp39-win32.whl", hash = "sha256:8e336b56301774ace6be0017ff85c3566c556d938359b61b840796a0202f805c"}, + {file = "coverage-7.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:8c938c6ae59be67ac19a7204e079efc94b38222cd7d0269f96e45e18cddeaa59"}, + {file = "coverage-7.7.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:3b0e6e54591ae0d7427def8a4d40fca99df6b899d10354bab73cd5609807261c"}, + {file = "coverage-7.7.0-py3-none-any.whl", hash = "sha256:708f0a1105ef2b11c79ed54ed31f17e6325ac936501fc373f24be3e6a578146a"}, + {file = "coverage-7.7.0.tar.gz", hash = "sha256:cd879d4646055a573775a1cec863d00c9ff8c55860f8b17f6d8eee9140c06166"}, ] [package.dependencies] @@ -264,13 +295,13 @@ toml = ["tomli"] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -278,18 +309,15 @@ test = ["pytest (>=6)"] [[package]] name = "fileseq" -version = "2.0.0" +version = "2.1.2" description = "A Python library for parsing frame ranges and file sequences commonly used in VFX and Animation applications." optional = false python-versions = "*" files = [ - {file = "Fileseq-2.0.0-py3-none-any.whl", hash = "sha256:706db52ddccd02c5a5cff782f2a0c57817e2daf1cace050453645754866ad15c"}, - {file = "Fileseq-2.0.0.tar.gz", hash = "sha256:b32a99e63086adb7d8a8be4a69b48bab6739ae233fbf8c67ff280141c42d98ff"}, + {file = "Fileseq-2.1.2-py3-none-any.whl", hash = "sha256:b5d599b0bff72f0b82d31a465b3f6b28ef4d20fa097bb7ff4d1a047efe913932"}, + {file = "fileseq-2.1.2.tar.gz", hash = "sha256:f1d844d62ad017821d0188110d36f3b93ae9996782f0e890cb64419644e469f2"}, ] -[package.dependencies] -typing-extensions = "*" - [[package]] name = "ghp-import" version = "2.1.0" @@ -309,13 +337,13 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "0.42.1" +version = "1.6.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "griffe-0.42.1-py3-none-any.whl", hash = "sha256:7e805e35617601355edcac0d3511cedc1ed0cb1f7645e2d336ae4b05bbae7b3b"}, - {file = "griffe-0.42.1.tar.gz", hash = "sha256:57046131384043ed078692b85d86b76568a686266cc036b9b56b704466f803ce"}, + {file = "griffe-1.6.0-py3-none-any.whl", hash = "sha256:9f1dfe035d4715a244ed2050dfbceb05b1f470809ed4f6bb10ece5a7302f8dd1"}, + {file = "griffe-1.6.0.tar.gz", hash = "sha256:eb5758088b9c73ad61c7ac014f3cdfb4c57b5c2fcbfca69996584b702aefa354"}, ] [package.dependencies] @@ -323,33 +351,40 @@ colorama = ">=0.4" [[package]] name = "idna" -version = "3.6" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "8.6.1" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" @@ -364,13 +399,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -381,13 +416,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markdown" -version = "3.6" +version = "3.7" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, - {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, ] [package.dependencies] @@ -399,71 +434,72 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -479,44 +515,44 @@ files = [ [[package]] name = "mkdocs" -version = "1.5.3" +version = "1.6.1" description = "Project documentation with Markdown." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, - {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, ] [package.dependencies] click = ">=7.0" colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" -markdown = ">=3.2.1" +markdown = ">=3.3.6" markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" packaging = ">=20.5" pathspec = ">=0.11.1" -platformdirs = ">=2.2.0" pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" -version = "1.0.1" +version = "1.4.1" description = "Automatically link across pages in MkDocs." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"}, - {file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"}, + {file = "mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f"}, + {file = "mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079"}, ] [package.dependencies] @@ -524,32 +560,49 @@ Markdown = ">=3.3" markupsafe = ">=2.0.1" mkdocs = ">=1.1" +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + [[package]] name = "mkdocs-material" -version = "9.5.14" +version = "9.6.8" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.14-py3-none-any.whl", hash = "sha256:a45244ac221fda46ecf8337f00ec0e5cb5348ab9ffb203ca2a0c313b0d4dbc27"}, - {file = "mkdocs_material-9.5.14.tar.gz", hash = "sha256:2a1f8e67cda2587ab93ecea9ba42d0ca61d1d7b5fad8cf690eeaeb39dcd4b9af"}, + {file = "mkdocs_material-9.6.8-py3-none-any.whl", hash = "sha256:0a51532dd8aa80b232546c073fe3ef60dfaef1b1b12196ac7191ee01702d1cf8"}, + {file = "mkdocs_material-9.6.8.tar.gz", hash = "sha256:8de31bb7566379802532b248bd56d9c4bc834afc4625884bf5769f9412c6a354"}, ] [package.dependencies] babel = ">=2.10,<3.0" +backrefs = ">=5.7.post1,<6.0" colorama = ">=0.4,<1.0" jinja2 = ">=3.0,<4.0" markdown = ">=3.2,<4.0" -mkdocs = ">=1.5.3,<1.6.0" +mkdocs = ">=1.6,<2.0" mkdocs-material-extensions = ">=1.3,<2.0" paginate = ">=0.5,<1.0" pygments = ">=2.16,<3.0" pymdown-extensions = ">=10.2,<11.0" -regex = ">=2022.4" requests = ">=2.26,<3.0" [package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] @@ -566,13 +619,13 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.24.1" +version = "0.24.3" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings-0.24.1-py3-none-any.whl", hash = "sha256:b4206f9a2ca8a648e222d5a0ca1d36ba7dee53c88732818de183b536f9042b5d"}, - {file = "mkdocstrings-0.24.1.tar.gz", hash = "sha256:cc83f9a1c8724fc1be3c2fa071dd73d91ce902ef6a79710249ec8d0ee1064401"}, + {file = "mkdocstrings-0.24.3-py3-none-any.whl", hash = "sha256:5c9cf2a32958cd161d5428699b79c8b0988856b0d4a8c5baf8395fc1bf4087c3"}, + {file = "mkdocstrings-0.24.3.tar.gz", hash = "sha256:f327b234eb8d2551a306735436e157d0a22d45f79963c60a8b585d5f7a94c1d2"}, ] [package.dependencies] @@ -595,62 +648,68 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.8.0" +version = "1.10.0" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings_python-1.8.0-py3-none-any.whl", hash = "sha256:4209970cc90bec194568682a535848a8d8489516c6ed4adbe58bbc67b699ca9d"}, - {file = "mkdocstrings_python-1.8.0.tar.gz", hash = "sha256:1488bddf50ee42c07d9a488dddc197f8e8999c2899687043ec5dd1643d057192"}, + {file = "mkdocstrings_python-1.10.0-py3-none-any.whl", hash = "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc"}, + {file = "mkdocstrings_python-1.10.0.tar.gz", hash = "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828"}, ] [package.dependencies] -griffe = ">=0.37" -mkdocstrings = ">=0.20" +griffe = ">=0.44" +mkdocstrings = ">=0.24.2" [[package]] name = "mypy" -version = "1.9.0" +version = "1.15.0" description = "Optional static typing for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, + {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, + {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, + {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, + {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, + {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, + {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, + {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, + {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, + {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, + {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, + {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, + {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" +mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -668,25 +727,30 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "paginate" -version = "0.5.6" +version = "0.5.7" description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" files = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, ] +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + [[package]] name = "pathspec" version = "0.12.1" @@ -700,28 +764,29 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -730,46 +795,45 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pygments" -version = "2.17.2" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.7.1" +version = "10.14.3" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, - {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, + {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, + {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, ] [package.dependencies] -markdown = ">=3.5" +markdown = ">=3.6" pyyaml = "*" [package.extras] -extra = ["pygments (>=2.12)"] +extra = ["pygments (>=2.19.1)"] [[package]] name = "pytest" -version = "8.1.1" +version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [package.dependencies] @@ -777,11 +841,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -803,17 +867,17 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.12.0" +version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, - {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] -pytest = ">=5.0" +pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] @@ -834,61 +898,64 @@ six = ">=1.5" [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -905,117 +972,15 @@ files = [ [package.dependencies] pyyaml = "*" -[[package]] -name = "regex" -version = "2023.12.25" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.7" -files = [ - {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, - {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, - {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, - {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, - {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, - {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, - {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, - {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, - {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, - {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, - {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, - {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, - {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, - {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, - {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, - {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, - {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, -] - [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -1056,46 +1021,76 @@ files = [ [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" -version = "2.2.1" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] @@ -1106,40 +1101,41 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "watchdog" -version = "4.0.0" +version = "6.0.0" description = "Filesystem events monitoring" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, - {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, - {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, - {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, - {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, - {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, - {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, - {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] @@ -1147,36 +1143,40 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "websocket-client" -version = "1.7.0" +version = "1.8.0" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.8" files = [ - {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, - {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, ] [package.extras] -docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] [[package]] name = "zipp" -version = "3.18.1" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, - {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9" -content-hash = "c78f34543a8c3e7a9560afca1f8d6e7b9cec99e9502ae0fddcb870fc6fedd9e0" +content-hash = "5d02070af240a1bb5608ea9675687e8fb735b8caaa89faccf55b2a6ecff4ce43" diff --git a/pytvpaint/camera.py b/pytvpaint/camera.py index 3a4625e..ede0a89 100644 --- a/pytvpaint/camera.py +++ b/pytvpaint/camera.py @@ -18,12 +18,6 @@ from pytvpaint.layer import Layer -# FIXME Camera.anti_aliasing always returns 1, Camera.fps no longer returns/sets fps, now only fps is Project fps -# FIXME most Camera values can still be edited/queried using george.tv_camera_info_set() but they are not reflected in -# UI and so can be unintentionally edited or reset -# FIXME george.tv_camera_info_get() values of pixel aspect ratio and fps have been swapped - - class Camera(Refreshable): """The Camera class represents the camera in a TVPaint clip. @@ -194,14 +188,17 @@ def points(self) -> Iterator[CameraPoint]: @set_as_current def get_point_data_at(self, position: float) -> InterpolationCameraPoint: """Get the points data interpolated at that position (between 0 and 1).""" - position = max(0.0, min(position, 1.0)) - return InterpolationCameraPoint(position, self, george.tv_camera_interpolation(position)) + return InterpolationCameraPoint( + position, self, InterpolationCameraPoint.get_point_data_at(position) + ) + @george.min_version_compatible(min_version="12") @set_as_current def get_point_data_at_frame(self, frame: int) -> FrameCameraPoint: - """Get the points data interpolated at that position (between 0 and 1).""" - real_frame = (frame - self.clip.project.start_frame) - return FrameCameraPoint(frame, self, george.tv_camera_info_frame(real_frame)) + """Get the points data interpolated at the specified frame in the clip.""" + return FrameCameraPoint( + frame, self, FrameCameraPoint.get_point_data_at(self.clip, frame) + ) @set_as_current def remove_point(self, index: int) -> None: @@ -379,18 +376,24 @@ def __init__( def __repr__(self) -> str: """String representation of the camera point.""" - return f"CameraPoint({self.camera.clip.name})" + return f"CameraPoint({self.camera.clip.name})[READ-ONLY]" @property def interpolation_point(self) -> float: """Interpolation point (between 0 and 1) for this CameraPoint.""" return self._interpolation_point + @classmethod + def get_point_data_at(cls, position: float) -> george.TVPCameraPoint: + """Get the points data interpolated at that position (between 0 and 1).""" + position = max(0.0, min(position, 1.0)) + return george.tv_camera_interpolation(position) + def refresh(self) -> None: """Refreshes the camera point data at the interpolation point.""" if not self.refresh_on_call and self._data: return - self._data = self.camera.get_point_data_at(self._interpolation_point) + self._data = self.get_point_data_at(self._interpolation_point) def remove(self) -> None: """Remove the camera point. @@ -398,8 +401,10 @@ def remove(self) -> None: Warning: the FrameCameraPoint instance is read-only and cannot be removed as it doesn't really exist """ - log.warning("Read-Only InterpolationCameraPoint cannot be deleted as it doesn't really exist, " - "ignoring request.") + log.warning( + "Read-Only InterpolationCameraPoint cannot be deleted as it doesn't really exist, " + "ignoring request." + ) return @@ -420,19 +425,24 @@ def __init__( def __repr__(self) -> str: """String representation of the camera point.""" - return f"CameraPoint({self.camera.clip.name})" + return f"CameraPoint({self.camera.clip.name})[READ-ONLY]" @property def frame(self) -> float: """Frame for this CameraPoint.""" return self._frame + @classmethod + def get_point_data_at(cls, clip: Clip, frame: int) -> george.TVPCameraPoint: + """Get the points data interpolated at the specified frame in the clip.""" + real_frame = frame - clip.project.start_frame + return george.tv_camera_info_frame(real_frame) + def refresh(self) -> None: """Refreshes the camera point data at the frame.""" if not self.refresh_on_call and self._data: return - real_frame = (self._frame - self.camera.clip.project.start_frame) - self._data = george.tv_camera_info_frame(real_frame) + self._data = self.get_point_data_at(self.camera.clip, self._frame) def remove(self) -> None: """Remove the camera point. @@ -440,6 +450,8 @@ def remove(self) -> None: Warning: the FrameCameraPoint instance is read-only and cannot be removed as it doesn't really exist """ - log.warning("Read-Only FrameCameraPoint cannot be deleted as it doesn't really exist, " - "ignoring request.") + log.warning( + "Read-Only FrameCameraPoint cannot be deleted as it doesn't really exist, " + "ignoring request." + ) return diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 474b00a..723766b 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -326,11 +326,19 @@ def layer_ids(self) -> Iterator[int]: """Iterator over the layer ids.""" return utils.position_generator(lambda pos: george.tv_layer_get_id(pos)) - def _all_layers(self, filter_types: tuple[type, ...] | None = None, ignore_types: tuple[type, ...] | None = None) -> Iterator[Layer]: + def get_layers( + self, + filter_types: tuple[type, ...] | None = None, + ignore_types: tuple[type, ...] | None = None, + ) -> Iterator[Layer]: """Iterator over the clip's layers. + Args: + filter_types: list of layer types to return, default is None, meaning all. + ignore_types: list of layer types to ignore, default is None, meaning all. + Note: - This function is only available in TVPaint version 12 and above. + This function is mainly here for TVPaint 12 and above, when using previous versions, prefer Clip.layers. """ for layer_id in self.layer_ids: layer_data = george.tv_layer_info(layer_id) @@ -341,7 +349,10 @@ def _all_layers(self, filter_types: tuple[type, ...] | None = None, ignore_types elif layer_data.type == george.LayerType.CAMERA: layer_class = CameraLayer # handle CTG layers like regular layers in TVP versions < 12 - elif not george.is_tvp_version_below_12() and layer_data.type is george.LayerType.SCRIBBLES: + elif ( + not george.is_tvp_version_below_12() + and layer_data.type is george.LayerType.SCRIBBLES + ): layer_class = CTGLayer if filter_types and layer_class not in filter_types: @@ -351,23 +362,10 @@ def _all_layers(self, filter_types: tuple[type, ...] | None = None, ignore_types yield layer_class(layer_id, clip=self, data=layer_data) - @property - @george.min_version_compatible(min_version="12") - def all_layers(self) -> Iterator[Layer]: - """Iterator over the clip's layers regardless of their type. - - Note: - This function is only available in TVPaint version 12 and above. - - Raises: - NotImplemented: if used in tvpaint version inferior to 12 - """ - yield from self._all_layers() - @property def layers(self) -> Iterator[Layer]: - """Iterator over the clip's layers, ignores all Folder, Camera and CTG layers.""" - yield from self._all_layers(ignore_types=(LayerFolder, CameraLayer, CTGLayer)) + """Iterator over the clip's animation layers, ignores all Folder, Camera and CTG layers.""" + yield from self.get_layers(ignore_types=(LayerFolder, CameraLayer, CTGLayer)) @property @george.min_version_compatible(min_version="12") @@ -380,7 +378,8 @@ def ctg_layers(self) -> Iterator[CTGLayer]: Raises: NotImplemented: if used in tvpaint version inferior to 12 """ - yield from self._all_layers(filter_types=(CTGLayer, )) + for layer in self.get_layers(filter_types=(CTGLayer,)): + yield cast(CTGLayer, layer) @property @george.min_version_compatible(min_version="12") @@ -393,7 +392,8 @@ def folders(self) -> Iterator[LayerFolder]: Raises: NotImplemented: if used in tvpaint version inferior to 12 """ - yield from self._all_layers(filter_types=(LayerFolder, )) + for layer in self.get_layers(filter_types=(LayerFolder,)): + yield cast(LayerFolder, layer) @property @george.min_version_compatible(min_version="12") @@ -406,19 +406,21 @@ def camera_layer(self) -> CameraLayer | None: Raises: NotImplemented: if used in tvpaint version inferior to 12 """ - camera_layer = next(self._all_layers(filter_types=(CameraLayer, )), None) + camera_layer = next(self.get_layers(filter_types=(CameraLayer,)), None) if camera_layer: return cast(CameraLayer, camera_layer) # if we're here then the camera layer has probably been deleted, let's recreate it by switching to the camera. george.tv_set_active_shape(george.TVPShape.CAMERA) - return cast(CameraLayer, next(self._all_layers(filter_types=(CameraLayer, )), None)) + return cast( + CameraLayer, next(self.get_layers(filter_types=(CameraLayer,)), None) + ) @property @set_as_current def layer_names(self) -> Iterator[str]: """Iterator over the clip's layer names.""" - for layer in self._all_layers(): + for layer in self.get_layers(): yield layer.name @property @@ -428,7 +430,7 @@ def current_layer(self) -> Layer: Raises: ValueError: if no current layer in clip """ - for layer in self._all_layers(): + for layer in self.get_layers(): if layer.is_current: return layer raise ValueError("Couldn't find a current layer") @@ -439,7 +441,7 @@ def get_layer( by_name: str | None = None, ) -> Layer | None: """Get a specific layer by id or name.""" - return utils.get_tvp_element(self._all_layers(), by_id, by_name) + return utils.get_tvp_element(self.get_layers(), by_id, by_name) @set_as_current def add_layer(self, layer_name: str) -> Layer: diff --git a/pytvpaint/george/client/parse.py b/pytvpaint/george/client/parse.py index ed8696c..bd727ba 100644 --- a/pytvpaint/george/client/parse.py +++ b/pytvpaint/george/client/parse.py @@ -113,9 +113,15 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: if get_origin(cast_type) in (tuple, list): # Split by space and convert each member to the right type - value_type = get_args(cast_type)[0] - values_types = [tv_cast_to_type(v, value_type) for v in value.split()] - return cast(T, get_origin(cast_type)(values_types)) + values_types = [] + sub_value_types = get_args(cast_type) + for index, sub_value in enumerate(value.split()): + sub_value_type = sub_value_types[index] + cast_sub_value = tv_cast_to_type(sub_value, sub_value_type) + values_types.append(cast_sub_value) + + origin_type = get_origin(cast_type) + return cast(T, origin_type(values_types)) # type: ignore[misc] if cast_type == bool: return cast(T, value.lower() in ["1", "on", "true"]) @@ -292,7 +298,7 @@ def args_dict_to_list(args: dict[str, Any]) -> list[Any]: def validate_args_list(optional_args: Sequence[Value | tuple[Value, ...]]) -> list[Any]: - """Validates *args equivalent for tvpaint + """Validates *args equivalent for tvpaint. Some George functions only accept a list of values and not key:value pairs. If for instance, you need to set the last positional argument for a function call, you need to provide all the preceding arguments. diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index ab8f4ba..ff7d8b0 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -649,9 +649,9 @@ def is_tvp_version_below_12() -> bool: def min_version_compatible(min_version: str) -> Callable[[T], T]: - """Decorator to apply on object methods. + """Decorator to apply on methods. - Given a minimum version, checks if the current tvpaint version if above the minimum requirement otherwise it + Given a minimum version, checks if the current tvpaint version is above the minimum requirement otherwise it raises a NotImplemented error Args: @@ -680,9 +680,9 @@ def applicator(*args: Any, **kwargs: Any) -> Any: def deprecated_warning(msg: str) -> Callable[[T], T]: - """Decorator to apply on object methods. + """Decorator to apply on methods. - Prints a deprecation message when decorated function is called + Prints a deprecation message/warning when decorated function is called Args: msg (str): deprecation message @@ -1132,7 +1132,7 @@ def tv_pen_brush_get(tool_mode: bool = False) -> TVPPenBrush: result = send_cmd("tv_PenBrush", *args) # Remove the first value which is tv_penbrush - result = result[len("tv_penbrush") + 1:] + result = result[(len("tv_penbrush") + 1) :] res = tv_parse_dict(result, with_fields=TVPPenBrush) return TVPPenBrush(**res) diff --git a/pytvpaint/george/grg_camera.py b/pytvpaint/george/grg_camera.py index 6fdb2e2..752f34f 100644 --- a/pytvpaint/george/grg_camera.py +++ b/pytvpaint/george/grg_camera.py @@ -13,6 +13,7 @@ validate_args_list, ) from pytvpaint.george.grg_base import FieldOrder, GrgErrorValue, is_tvp_version_below_12 +from pytvpaint.george.grg_project import tv_frame_rate_get @dataclass(frozen=True) @@ -39,13 +40,17 @@ class TVPCameraPoint: def tv_camera_info_get() -> TVPCamera: """Get the information of the camera.""" - if is_tvp_version_below_12(): - fields = TVPCamera - else: # values of pixel aspect ratio and fps have been swapped in versions > 12 - fields = get_dataclass_fields(cast(DataclassInstance, TVPCamera)) + fields = get_dataclass_fields(cast(DataclassInstance, TVPCamera)) + if not is_tvp_version_below_12(): + # values of pixel aspect ratio and fps have been swapped in versions > 12 fields_keys = list(dict(fields).keys()) - pixel_aspect_index, fps_index = fields_keys.index('pixel_aspect_ratio'), fields_keys.index('frame_rate') - fields[pixel_aspect_index], fields[fps_index] = fields[fps_index], fields[pixel_aspect_index] + pixel_aspect_index, fps_index = fields_keys.index( + "pixel_aspect_ratio" + ), fields_keys.index("frame_rate") + fields[pixel_aspect_index], fields[fps_index] = ( + fields[fps_index], + fields[pixel_aspect_index], + ) return TVPCamera(**tv_parse_list(send_cmd("tv_CameraInfo"), with_fields=fields)) @@ -89,9 +94,8 @@ def tv_camera_interpolation(position: float) -> TVPCameraPoint: def tv_camera_info_frame(frame: int) -> TVPCameraPoint: """Get the position/angle/scale values at the given frame.""" errors = ["Given frame out of camera layer's range"] - res = send_cmd("tv_CameraInfoFrame", frame, error_values=errors) res = tv_parse_list( - res, + send_cmd("tv_CameraInfoFrame", frame, error_values=errors), with_fields=TVPCameraPoint, ) return TVPCameraPoint(**res) diff --git a/pytvpaint/george/grg_layer.py b/pytvpaint/george/grg_layer.py index 38bbf25..d4ee801 100644 --- a/pytvpaint/george/grg_layer.py +++ b/pytvpaint/george/grg_layer.py @@ -265,7 +265,7 @@ def tv_layer_move(position: int, folder_id: int | None = None) -> None: ) args = [position] - if not is_tvp_version_below_12(): + if not is_tvp_version_below_12() and folder_id: args.append(folder_id) send_cmd("tv_LayerMove", *args) @@ -1288,14 +1288,14 @@ def tv_clear(fill_b_pen: bool = False) -> None: @min_version_compatible(min_version="12") def tv_layer_is_ctg_source() -> list[int]: - """Returns list of CTG layers (ids) that use the current layer as a source""" + """Returns list of CTG layers (ids) that use the current layer as a source.""" res = send_cmd("tv_LayerIsCTGSource", error_values=[GrgErrorValue.EMPTY]) with contextlib.suppress(Exception): layer_ids = [int(layer_id) for layer_id in res.split()] + if any([layer_id < 0 for layer_id in layer_ids]): return [] - else: - return layer_ids + return layer_ids return [] @@ -1333,7 +1333,7 @@ def tv_ctg_load_structure(ctg_layer_id: int) -> None: @min_version_compatible(min_version="12") def tv_ctg_apply_changes(ctg_layer_id: int, apply: bool) -> bool: - """Set Apply Changes value on CTG layer + """Set Apply Changes value on CTG layer. Args: ctg_layer_id: ctg layer id @@ -1348,7 +1348,7 @@ def tv_ctg_apply_changes(ctg_layer_id: int, apply: bool) -> bool: @min_version_compatible(min_version="12") def tv_ctg_squiggles_visible(ctg_layer_id: int, visible: bool) -> bool: - """Set squiggles visibility on CTG layer + """Set squiggles visibility on CTG layer. Args: ctg_layer_id: ctg layer id @@ -1363,7 +1363,7 @@ def tv_ctg_squiggles_visible(ctg_layer_id: int, visible: bool) -> bool: @min_version_compatible(min_version="12") def tv_ctg_get_source(ctg_layer_id: int) -> list[int]: - """Get a CTG layer's sources + """Get a CTG layer's sources. Args: ctg_layer_id: ctg layer id @@ -1380,13 +1380,26 @@ def tv_ctg_get_source(ctg_layer_id: int) -> list[int]: @min_version_compatible(min_version="12") def tv_ctg_source_add(ctg_layer_id: int, source_ids: list[int]) -> None: - send_cmd("tv_CTGSource", "add", ctg_layer_id, *source_ids) + """Add layers as sources for the CTG layer. + Args: + ctg_layer_id: ctg layer id + source_ids: list of layer Ids to add as sources for the CTG layer -@min_version_compatible(min_version="12") -def tv_ctg_source_remove(ctg_layer_id: int, source_ids: list[int]) -> None: - send_cmd("tv_CTGSource", "remove", ctg_layer_id, *source_ids) - + Raises: + ValueError: if layer Id(s) already set as source + """ + res = send_cmd("tv_CTGSource", "add", ctg_layer_id, *source_ids) + if not res == "0": + raise ValueError(res) +@min_version_compatible(min_version="12") +def tv_ctg_source_remove(ctg_layer_id: int, source_ids: list[int]) -> None: + """Remove layers from sources for the CTG layer. + Args: + ctg_layer_id: ctg layer id + source_ids: list of layer Ids to remove as sources for the CTG layer + """ + send_cmd("tv_CTGSource", "remove", ctg_layer_id, *source_ids) diff --git a/pytvpaint/george/grg_project.py b/pytvpaint/george/grg_project.py index a9d1c8c..e750fba 100644 --- a/pytvpaint/george/grg_project.py +++ b/pytvpaint/george/grg_project.py @@ -66,14 +66,17 @@ def tv_project_info(project_id: str) -> TVPProject: """ result = send_cmd("tv_ProjectInfo", project_id, error_values=[GrgErrorValue.EMPTY]) - if is_tvp_version_below_12(): - fields = TVPProject - else: + fields = get_dataclass_fields(cast(DataclassInstance, TVPProject)) + if not is_tvp_version_below_12(): # values of field_order have been removed in versions > 12 so for now we provide it ourselves - fields = get_dataclass_fields(cast(DataclassInstance, TVPProject)) - fields_keys = list(dict(fields).keys()) - field_order_index, start_frame_index = fields_keys.index('field_order'), fields_keys.index('start_frame') - fields[field_order_index], fields[start_frame_index] = fields[start_frame_index], fields[field_order_index] + fields_keys: list[str] = list(dict(fields).keys()) + field_order_index, start_frame_index = fields_keys.index( + "field_order" + ), fields_keys.index("start_frame") + fields[field_order_index], fields[start_frame_index] = ( + fields[start_frame_index], + fields[field_order_index], + ) result = f"{result} {tv_get_field().value}" project = tv_parse_list(result, with_fields=fields) diff --git a/pytvpaint/george/grg_scene.py b/pytvpaint/george/grg_scene.py index 50bc47f..1a54679 100644 --- a/pytvpaint/george/grg_scene.py +++ b/pytvpaint/george/grg_scene.py @@ -52,6 +52,8 @@ def tv_scene_create(clips: list[str]) -> int: Returns: scene_id: new scene id + Warning: + This function doesn't seem to work for now. """ return int(send_cmd("tv_SceneCreate", *clips)) @@ -68,4 +70,3 @@ def tv_scene_split(scene_id: int) -> list[int]: """ return tv_cast_to_type(send_cmd("tv_SceneSplit", scene_id), list[int]) - diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index 006a7e8..00fd8da 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -27,22 +27,6 @@ from pytvpaint.scene import Scene -# FIXME folder layer has no way of knowing which layers are it's children -# FIXME child layers have no way of knowing if they are in a folder layer or which one -# FIXME CameraLayer is not a layer and most layer functions will ignore, prefer use of Camera object instead -# FIXME CTG layer sometimes takes a while to update in the UI, don't know if incident is isolated to me or generalised -# FIXME tv_CTGGetSources doesn't seem to work, maybe some missing/undocumented attributes -# FIXME tv_CTGGetSources is actually misspelled and is actually tv_CTGGetSource without the `s` at the end -# FIXME tv_CTGGetSources actually requires and returns layer Ids not names -# FIXME creating a CTG layer from a folder crashes TVPaint -# FIXME moving a layer that is already in a folder in the same folder crashes TVPaint (check again) -# FIXME tv_layer_move position is relative to root and not folder, this is not ideal -# FIXME not providing a FolderID to tv_LayerMove doesn't move the layer to the root, you just need to move outside the -# folder range for it to work -# FIXME moving a layer inside a folder can be done without providing a FolderID, just by moving the layer in the folder's range -# FIXME not knowing whether a layer is in a folder or not is not great, same for folder not knowing it's children - - @dataclass class LayerInstance: """A layer instance is a frame where there is a drawing. It only has a start frame. @@ -458,11 +442,19 @@ def set_folder_position(self, folder: LayerFolder, position: int) -> None: @property @george.min_version_compatible(min_version="12") def folder(self) -> None: - raise NotImplementedError("There is currently no way to get the parent folder from a Layer.") + """The layer's parent folder if any. + + Raises: + NotImplementedError: as there is currently no way to get the parent folder from a Layer. + """ + raise NotImplementedError( + "There is currently no way to get the parent folder from a Layer." + ) @folder.setter @george.min_version_compatible(min_version="12") - def folder(self, folder: LayerFolder): + def folder(self, folder: LayerFolder) -> None: + """Set the layer's parent folder.""" george.tv_layer_move(self.position, folder.id) @refreshed_property @@ -1424,8 +1416,11 @@ def new( clip = clip or Clip.current_clip() clip.make_current() + sources = sources or [] name = utils.get_unique_name(clip.layer_names, name) - layer_id = george.tv_ctg_layer_create(name, sources=[layer.name for layer in sources]) + layer_id = george.tv_ctg_layer_create( + name, sources=[layer.name for layer in sources] + ) layer = cls(layer_id=layer_id, clip=clip) if color: @@ -1436,6 +1431,7 @@ def new( @property @set_as_current def squiggles_visible(self) -> bool: + """Get squiggles visibility.""" prev_value = george.tv_ctg_squiggles_visible(self.id, True) # reset value george.tv_ctg_squiggles_visible(self.id, prev_value) @@ -1444,11 +1440,13 @@ def squiggles_visible(self) -> bool: @squiggles_visible.setter @set_as_current def squiggles_visible(self, value: bool) -> None: + """Set squiggles visibility.""" george.tv_ctg_squiggles_visible(self.id, value) @property @set_as_current def apply_changes(self) -> bool: + """Get CTG Layer's apply changes value.""" prev_value = george.tv_ctg_apply_changes(self.id, True) # reset value george.tv_ctg_apply_changes(self.id, prev_value) @@ -1457,19 +1455,21 @@ def apply_changes(self) -> bool: @apply_changes.setter @set_as_current def apply_changes(self, value: bool) -> None: + """Set CTG Layer's apply changes value.""" george.tv_ctg_apply_changes(self.id, value) @property def sources(self) -> list[Layer]: + """Get this CTG layer's source layers.""" sources = [] # TODO commenting this since it was used when tv_CTGGetSources "wasn't" working # for layer in self.clip.layers: # if not layer.is_ctg_source: - # continue + # continue # noqa: ERA001 # if self.id not in [ctg_layer.id for ctg_layer in layer.sourced_ctg_layers]: - # continue - # sources.append(layer) + # continue # noqa: ERA001 + # sources.append(layer) # noqa: ERA001 for layer_id in george.tv_ctg_get_source(self.id): layer = self.clip.get_layer(by_id=layer_id) @@ -1479,13 +1479,19 @@ def sources(self) -> list[Layer]: return sources def add_sources(self, sources: list[Layer]) -> None: - george.tv_ctg_source_add(self.id, [layer.id for layer in sources if self not in layer.sourced_ctg_layers]) + """Add a list of Layers as sources for this CTG layer.""" + george.tv_ctg_source_add( + self.id, + [layer.id for layer in sources if self not in layer.sourced_ctg_layers], + ) def remove_sources(self, sources: list[Layer]) -> None: - george.tv_ctg_source_remove(self.id, [layer.id for layer in sources if self in layer.sourced_ctg_layers]) + """Remove a list of Layers from the sources for this CTG layer.""" + george.tv_ctg_source_remove( + self.id, [layer.id for layer in sources if self in layer.sourced_ctg_layers] + ) @set_as_current def load_structure(self) -> None: + """Load CTG layer structure.""" george.tv_ctg_load_structure(self.id) - - diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 7bad2b5..49b07ca 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -23,9 +23,6 @@ from pytvpaint.scene import Scene -# FIXME george.tv_project_info() values of field_order have been removed in versions > 12 so for now we provide it ourselves - - class Project(Refreshable, Renderable): """A TVPaint project is the highest/root object that contains everything in the data hierarchy. diff --git a/pytvpaint/scene.py b/pytvpaint/scene.py index f344212..eefea3d 100644 --- a/pytvpaint/scene.py +++ b/pytvpaint/scene.py @@ -12,8 +12,6 @@ set_as_current, ) -# FIXME tv_scene_create doesn't seem to work - class Scene(Removable): """A Scene is a collection of clips. A Scene is parented to a project.""" @@ -47,7 +45,9 @@ def current_scene() -> Scene: ) @classmethod - def new(cls, project: Project | None = None, clips: list[str] | None = None) -> Scene: + def new( + cls, project: Project | None = None, clips: list[str] | None = None + ) -> Scene: """Creates a new scene in the provided project. Args: @@ -62,18 +62,18 @@ def new(cls, project: Project | None = None, clips: list[str] | None = None) -> # TODO commenting this for now until george.tv_scene_create is fixed by TVP devs # if not clips or george.is_tvp_version_below_12(): - # george.tv_scene_new() - # new_scene = cls.current_scene() + # george.tv_scene_new() # noqa: ERA001 + # new_scene = cls.current_scene() # noqa: ERA001 # # if clips and george.is_tvp_version_below_12(): # for clip_name in clips: - # new_scene.add_clip(clip_name) + # new_scene.add_clip(clip_name) # noqa: ERA001 # - # return new_scene + # return new_scene # noqa: ERA001 # # # if here, then we are using tvp 12 or superior - # scene_id = george.tv_scene_create(clips) - # return project.get_scene(by_id=scene_id) + # scene_id = george.tv_scene_create(clips) # noqa: ERA001 + # return project.get_scene(by_id=scene_id) # noqa: ERA001 george.tv_scene_new() new_scene = cls.current_scene() diff --git a/tests/george/test_grg_camera.py b/tests/george/test_grg_camera.py index ca0ca68..601bc5a 100644 --- a/tests/george/test_grg_camera.py +++ b/tests/george/test_grg_camera.py @@ -60,6 +60,9 @@ def test_tv_camera_info_set( for attr, arg in zip(attrs_check, args): current = getattr(camera, attr) err_msg = f"Error checking {attr} (expected: {arg}, current: {current})" + + if not is_tvp_version_below_12() and attr == 'frame_rate': + continue assert current == arg, err_msg @@ -77,6 +80,7 @@ def map_value(start: int, end: int, ratio: float) -> float: return start + (end - start) * ratio +@pytest.mark.skipif(not is_tvp_version_below_12(), reason="Function no longer works properly in TVP 12") def test_tv_camera_interpolation(test_project: TVPProject) -> None: start_x = 0 start_y = 0 @@ -89,11 +93,13 @@ def test_tv_camera_interpolation(test_project: TVPProject) -> None: steps = 10 for i in range(steps + 1): ratio = i / steps - inter = tv_camera_interpolation(i / steps) + inter = tv_camera_interpolation(ratio) assert round(inter.x) == map_value(start_x, end_x, ratio) assert round(inter.y) == map_value(start_y, end_y, ratio) +@pytest.mark.skipif(not is_tvp_version_below_12(), + reason="Skipping since tv_camera_interpolation no longer works properly in TVP 12") def test_tv_camera_insert_point(test_project: TVPProject) -> None: point = TVPCameraPoint(50, 26, 0, scale=0.0) tv_camera_insert_point(0, point.x, point.y, point.angle, point.angle) @@ -109,6 +115,8 @@ def test_tv_camera_remove_point(test_project: TVPProject) -> None: tv_camera_enum_points(0) +@pytest.mark.skipif(not is_tvp_version_below_12(), + reason="Skipping since tv_camera_enum_points no longer works properly in TVP 12") def test_tv_camera_set_point() -> None: tv_camera_insert_point(0, 50, 25, 0, 0.0) new_point = TVPCameraPoint(67, 34, 1, 0.5) diff --git a/tests/george/test_grg_clip.py b/tests/george/test_grg_clip.py index bb2427c..c9e46ad 100644 --- a/tests/george/test_grg_clip.py +++ b/tests/george/test_grg_clip.py @@ -1,676 +1,685 @@ -# from __future__ import annotations -# -# import itertools -# import re -# from collections.abc import Iterable, Iterator -# from pathlib import Path -# from typing import Any -# -# import pytest -# -# from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError -# from pytvpaint.george.grg_base import ( -# FieldOrder, -# SaveFormat, -# SpriteLayout, -# tv_save_mode_get, -# ) -# from pytvpaint.george.grg_clip import ( -# PSDSaveMode, -# TVPClip, -# tv_bookmark_clear, -# tv_bookmark_next, -# tv_bookmark_prev, -# tv_bookmark_set, -# tv_bookmarks_enum, -# tv_clip_action_get, -# tv_clip_action_set, -# tv_clip_close, -# tv_clip_color_get, -# tv_clip_color_set, -# tv_clip_current_id, -# tv_clip_dialog_get, -# tv_clip_dialog_set, -# tv_clip_duplicate, -# tv_clip_enum_id, -# tv_clip_hidden_get, -# tv_clip_hidden_set, -# tv_clip_info, -# tv_clip_move, -# tv_clip_name_get, -# tv_clip_name_set, -# tv_clip_new, -# tv_clip_note_get, -# tv_clip_note_set, -# tv_clip_save_structure_csv, -# tv_clip_save_structure_flix, -# tv_clip_save_structure_json, -# tv_clip_save_structure_psd, -# tv_clip_save_structure_sprite, -# tv_clip_select, -# tv_clip_selection_get, -# tv_clip_selection_set, -# tv_first_image, -# tv_last_image, -# tv_layer_image, -# tv_layer_image_get, -# tv_load_sequence, -# tv_save_clip, -# tv_save_display, -# tv_save_sequence, -# tv_sound_clip_adjust, -# tv_sound_clip_info, -# tv_sound_clip_new, -# tv_sound_clip_reload, -# tv_sound_clip_remove, -# ) -# from pytvpaint.george.grg_layer import ( -# LayerType, -# TVPLayer, -# tv_instance_get_name, -# tv_layer_current_id, -# tv_layer_display_set, -# tv_layer_get_id, -# tv_layer_info, -# tv_layer_kill, -# tv_layer_rename, -# tv_layer_set, -# ) -# from pytvpaint.george.grg_project import TVPProject, tv_save_project -# from pytvpaint.george.grg_scene import tv_scene_current_id, tv_scene_new -# from tests.conftest import FixtureYield, test_scene -# -# -# def test_tv_clip_info(test_clip: TVPClip) -> None: -# assert tv_clip_info(test_clip.id) -# -# -# def test_tv_clip_info_wrong_id(test_clip: TVPClip) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_clip_info(-2) -# -# -# def test_tv_clip_enum_id(test_scene: int) -> None: -# clips: list[int] = [] -# -# for i in range(5): -# tv_clip_new(f"clip_{i}") -# clips.append(tv_clip_current_id()) -# -# for i, clip_id in enumerate(clips): -# # We offset 1 because of default clip -# assert tv_clip_enum_id(test_scene, i + 1) == clip_id -# -# -# def test_tv_clip_enum_id_wrong_scene_id() -> None: -# with pytest.raises(GeorgeError): -# tv_clip_enum_id(-2, 0) -# -# -# @pytest.mark.parametrize("pos", [-1, 1, 50]) -# def test_tv_clip_enum_id_wrong_clip_pos(test_scene: int, pos: int) -> None: -# with pytest.raises(GeorgeError): -# tv_clip_enum_id(test_scene, pos) -# -# -# def test_tv_clip_current_id() -> None: -# assert tv_clip_current_id() -# -# -# @pytest.mark.parametrize("name", ["", "a", "0", "lfseflj0", "clip with spaces"]) -# def test_tv_clip_new(test_scene: int, name: str) -> None: -# tv_clip_new(name) -# assert tv_clip_info(tv_clip_current_id()).name == name -# -# -# def test_tv_clip_duplicate(test_scene: int, test_clip: TVPClip) -> None: -# tv_clip_duplicate(test_clip.id) -# dup_clip = tv_clip_info(tv_clip_current_id()) -# assert dup_clip == test_clip -# -# -# def test_tv_clip_close(test_clip: TVPClip) -> None: -# tv_clip_close(test_clip.id) -# with pytest.raises(NoObjectWithIdError): -# tv_clip_info(test_clip.id) -# -# -# def test_tv_clip_name_get(test_clip: TVPClip) -> None: -# assert tv_clip_name_get(test_clip.id) == test_clip.name -# -# -# def test_tv_clip_name_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_clip_name_get(-1) -# -# -# @pytest.mark.parametrize("name", ["_", "a", "0", "lfseflj0"]) -# def test_tv_clip_name_set(test_clip: TVPClip, name: str) -> None: -# tv_clip_name_set(test_clip.id, name) -# assert tv_clip_info(test_clip.id).name == name -# -# -# def test_tv_clip_name_set_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_clip_name_get(-1) -# -# -# # "Copy" the fixture to use it twice in the above test -# # See: https://stackoverflow.com/questions/36100624/pytest-use-same-fixture-twice-in-one-function -# destination_scene = test_scene -# -# -# @pytest.mark.parametrize("new_pos", range(5)) -# def test_tv_clip_move( -# test_scene: int, test_clip: TVPClip, destination_scene: int, new_pos: int -# ) -> None: -# for i in range(5): -# tv_clip_new(f"other_clip_{i}") -# -# tv_clip_move(test_clip.id, destination_scene, new_pos) -# -# # Ensure that it's not in the first scene -# with pytest.raises(GeorgeError): -# tv_clip_enum_id(test_scene, 1) -# -# # Ensure that it's at the right position in the other scene -# tv_clip_enum_id(destination_scene, new_pos) -# -# -# def test_tv_clip_hidden_get(test_clip: TVPClip) -> None: -# assert tv_clip_hidden_get(test_clip.id) == test_clip.is_hidden -# -# -# def test_tv_clip_hidden_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_clip_hidden_get(-1) -# -# -# @pytest.mark.parametrize("hidden", [True, False]) -# def test_tv_clip_hidden_set(test_clip: TVPClip, hidden: bool) -> None: -# tv_clip_hidden_set(test_clip.id, hidden) -# assert tv_clip_info(test_clip.id).is_hidden == hidden -# -# -# @pytest.mark.parametrize("hidden", [True, False]) -# def test_tv_clip_hidden_set_wrong_id(hidden: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_clip_hidden_set(-1, hidden) -# -# -# def test_tv_clip_select(test_scene: int, test_clip: TVPClip) -> None: -# first_clip = tv_clip_enum_id(test_scene, 0) -# tv_clip_select(first_clip) -# -# assert not tv_clip_info(test_clip.id).is_current -# tv_clip_select(test_clip.id) -# assert tv_clip_info(test_clip.id).is_current -# -# -# def test_tv_clip_select_also_selects_scene( -# test_project: TVPProject, -# test_scene: int, -# test_clip: TVPClip, -# ) -> None: -# tv_scene_new() -# assert tv_scene_current_id() != test_scene -# tv_clip_select(test_clip.id) -# assert tv_scene_current_id() == test_scene -# -# -# def test_tv_clip_selection_get(test_clip: TVPClip) -> None: -# assert tv_clip_selection_get(test_clip.id) == test_clip.is_selected -# -# -# def test_tv_clip_selection_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_clip_selection_get(-1) -# -# -# @pytest.mark.parametrize("select", [True, False]) -# def test_tv_clip_selection_set(test_clip: TVPClip, select: bool) -> None: -# tv_clip_selection_set(test_clip.id, select) -# assert tv_clip_info(test_clip.id).is_selected == select -# -# -# @pytest.mark.parametrize("select", [True, False]) -# def test_tv_clip_selection_set_wrong_id(test_clip: TVPClip, select: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_clip_selection_set(-1, select) -# -# -# def test_tv_first_image(test_clip: TVPClip) -> None: -# assert tv_first_image() == test_clip.first_frame -# -# -# def test_tv_last_image(test_clip: TVPClip) -> None: -# assert tv_last_image() == test_clip.last_frame -# -# -# @pytest.mark.parametrize("offset_count", [None, *itertools.product([0, 1], [0, 1])]) -# @pytest.mark.parametrize("field_order", [None, *FieldOrder]) -# @pytest.mark.parametrize("stretch", [False, True]) -# @pytest.mark.parametrize("time_stretch", [False, True]) -# @pytest.mark.parametrize("preload", [False, True]) -# def test_tv_load_sequence( -# ppm_sequence: list[Path], -# test_clip: TVPClip, -# offset_count: tuple[int, int] | None, -# field_order: FieldOrder | None, -# stretch: bool, -# time_stretch: bool, -# preload: bool, -# ) -> None: -# total_images = len(ppm_sequence) -# first_image = ppm_sequence[0] -# -# images_loaded = tv_load_sequence( -# first_image, -# offset_count, -# field_order, -# stretch, -# time_stretch, -# preload, -# ) -# -# if offset_count: -# offset, count = offset_count -# should_load = total_images if count <= 0 else min(count, total_images - offset) -# else: -# should_load = total_images -# -# assert should_load == images_loaded -# -# # Find the extension at the end (including file number) -# ext_match = re.search(r"\d+\.[a-z0-9]+$", str(first_image)) -# assert ext_match -# ext_start, _ = ext_match.span() -# -# layer_name_cut = str(first_image)[:ext_start] -# -# layer = tv_layer_info(tv_layer_current_id()) -# -# assert layer.name == layer_name_cut -# assert layer.type == LayerType.SEQUENCE -# assert layer.first_frame == 0 -# assert layer.last_frame == should_load - 1 -# -# -# def test_tv_load_sequence_sequence_does_not_exist(tmp_path: Path) -> None: -# with pytest.raises(FileNotFoundError, match="File not found"): -# tv_load_sequence(tmp_path / "file.001.png") -# -# -# @pytest.fixture -# def bookmarks(test_clip: TVPClip) -> FixtureYield[Iterable[int]]: -# n_bookmarks = 5 -# for frame in range(n_bookmarks): -# tv_bookmark_set(frame) -# yield range(n_bookmarks) -# -# -# def test_tv_bookmarks_enum(bookmarks: Iterable[int]) -> None: -# for pos, mark in enumerate(bookmarks): -# assert tv_bookmarks_enum(pos) == mark -# -# -# @pytest.mark.parametrize("frame", range(5)) -# def test_tv_bookmark_set(test_clip: TVPClip, frame: int) -> None: -# tv_bookmark_set(frame) -# assert tv_bookmarks_enum(0) == frame -# -# -# @pytest.mark.parametrize("frame", range(5)) -# def test_tv_bookmark_clear(test_clip: TVPClip, frame: int) -> None: -# tv_bookmark_set(frame) -# assert tv_bookmarks_enum(0) == frame -# -# tv_bookmark_clear(frame) -# -# with pytest.raises(GeorgeError, match="No bookmark"): -# tv_bookmarks_enum(0) -# -# -# @pytest.mark.parametrize("frame", range(5)) -# def test_tv_bookmark_next(test_clip: TVPClip, frame: int) -> None: -# tv_bookmark_set(frame) -# tv_bookmark_next() -# assert tv_layer_image_get() == frame -# -# -# @pytest.mark.parametrize("frame", range(5)) -# def test_tv_bookmark_prev(test_clip: TVPClip, frame: int) -> None: -# # Set the current image far away -# tv_layer_image(50) -# -# tv_bookmark_set(frame) -# tv_bookmark_prev() -# -# assert tv_layer_image_get() == frame -# -# -# def test_tv_clip_color_get(test_clip: TVPClip) -> None: -# assert tv_clip_color_get(test_clip.id) in range(27) -# -# -# @pytest.mark.parametrize("color_index", range(27)) -# def test_tv_clip_color_set(test_clip: TVPClip, color_index: int) -> None: -# tv_clip_color_set(test_clip.id, color_index) -# assert tv_clip_color_get(test_clip.id) == color_index -# -# -# def test_tv_clip_action_get(test_clip: TVPClip) -> None: -# assert tv_clip_action_get(test_clip.id) == "" -# -# -# def test_tv_clip_action_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_clip_action_get(-3) -# -# -# # Note: we removed the \n test because there's a bug with TVPaint that handle control characters -# TEST_TEXTS = ["", "l", "0", "ab", "a0l", "ap*"] # "a\nb"] -# -# -# @pytest.mark.parametrize("text", TEST_TEXTS) -# def test_tv_clip_action_set(test_clip: TVPClip, text: str) -> None: -# tv_clip_action_set(test_clip.id, text) -# assert tv_clip_action_get(test_clip.id) == text -# -# -# def test_tv_clip_action_set_wrong_id() -> None: -# with pytest.raises(GeorgeError): -# tv_clip_action_set(-2, "test") -# -# -# def test_tv_clip_dialog_get(test_clip: TVPClip) -> None: -# assert tv_clip_dialog_get(test_clip.id) == "" -# -# -# def test_tv_clip_dialog_get_wrong_id() -> None: -# with pytest.raises(GeorgeError): -# tv_clip_dialog_get(-2) -# -# -# @pytest.mark.parametrize("text", TEST_TEXTS) -# def test_tv_clip_dialog_set(test_clip: TVPClip, text: str) -> None: -# tv_clip_dialog_set(test_clip.id, text) -# assert tv_clip_dialog_get(test_clip.id) == text -# -# -# def test_tv_clip_dialog_set_wrong_id() -> None: -# with pytest.raises(GeorgeError): -# tv_clip_dialog_set(-2, "test") -# -# -# def test_tv_clip_note_get(test_clip: TVPClip) -> None: -# assert tv_clip_note_get(test_clip.id) == "" -# -# -# def test_tv_clip_note_get_wrong_id() -> None: -# with pytest.raises(GeorgeError): -# tv_clip_dialog_get(-2) -# -# -# @pytest.mark.parametrize("text", TEST_TEXTS) -# def test_tv_clip_note_set(test_clip: TVPClip, text: str) -> None: -# tv_clip_note_set(test_clip.id, text) -# assert tv_clip_note_get(test_clip.id) == text -# -# -# def test_tv_clip_note_set_wrong_id() -> None: -# with pytest.raises(GeorgeError): -# tv_clip_note_set(-2, "test") -# -# -# def test_tv_save_clip(tmp_path: Path) -> None: -# clip_tvp = tmp_path / "clip.tvp" -# tv_save_clip(clip_tvp) -# assert clip_tvp.exists() -# -# -# @pytest.mark.skip("Will block the UI") -# def test_tv_save_clip_folder_does_not_exist(tmp_path: Path) -> None: -# with pytest.raises(GeorgeError, match="Can't create file"): -# tv_save_clip(tmp_path / "folder" / "out.tvpx") -# -# -# def test_tv_save_display(tmp_path: Path) -> None: -# save_ext, _ = tv_save_mode_get() -# ext = "jpg" if save_ext == SaveFormat.JPG else save_ext.value -# out_display = (tmp_path / "out").with_suffix("." + ext) -# tv_save_display(out_display) -# assert out_display.exists() -# -# -# def current_clip_layers() -> Iterator[TVPLayer]: -# """Iterates through the current clip layers""" -# pos = 0 -# while True: -# try: -# lid = tv_layer_get_id(pos) -# yield tv_layer_info(lid) -# except GeorgeError: -# break -# pos += 1 -# -# -# def get_instance_frames() -> Iterator[tuple[int, str]]: -# """Iterates through the instances of the current layer""" -# layer = tv_layer_current_id() -# frame = 0 -# while True: -# try: -# name = tv_instance_get_name(layer, frame) -# yield frame, name -# except NoObjectWithIdError: -# break -# frame += 1 -# -# -# def apply_folder_pattern(initial_pattern: str | None, layer: TVPLayer) -> str: -# # This is the default folder pattern -# if initial_pattern is None: -# return f"[{layer.position:03d}] {layer.name}" -# -# patterns = { -# r"%li": str(layer.position), -# r"%ln": layer.name, -# r"%fi": r"%fi", # Couldn't make it work -# } -# -# for pattern, rep in patterns.items(): -# initial_pattern = initial_pattern.replace(pattern, rep) -# -# return initial_pattern -# -# -# def apply_file_pattern( -# initial_pattern: str | None, -# layer: TVPLayer, -# image_index: int, -# image_name: str, -# file_index: int, -# ) -> str: -# # This is the default file pattern -# if initial_pattern is None: -# return f"[{image_index:03d}] {layer.name}" -# -# # When the instance name is empty it takes the image index -# if image_name == "": -# image_name = str(image_index) -# -# patterns = { -# r"%li": str(layer.position), -# r"%ln": layer.name, -# r"%ii": str(image_index), -# r"%in": image_name, -# r"%fi": str(file_index), -# } -# -# for pattern, rep in patterns.items(): -# initial_pattern = initial_pattern.replace(pattern, rep) -# -# return initial_pattern -# -# -# def load_sequence_with_name(first_frame: Path, name: str, count: int) -> int: -# """Load an image sequence and rename the layer""" -# tv_load_sequence(first_frame, offset_count=(0, count)) -# layer = tv_layer_current_id() -# tv_layer_rename(layer, name) -# return layer -# -# -# @pytest.mark.parametrize( -# "mark_in, mark_out", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)] -# ) -# def test_tv_save_sequence( -# test_project: TVPProject, -# tmp_path: Path, -# ppm_sequence: list[Path], -# mark_in: int | None, -# mark_out: int | None, -# ) -> None: -# tv_load_sequence(ppm_sequence[0]) -# -# save_ext, _ = tv_save_mode_get() -# out_sequence = tmp_path / "out" -# tv_save_sequence(out_sequence, mark_in, mark_out) -# -# clip = tv_clip_info(tv_clip_current_id()) -# start, end = ( -# (mark_in, mark_out) -# if mark_in is not None and mark_out is not None -# else (clip.first_frame, clip.last_frame) -# ) -# -# for i in range(end - start): -# image_name = f"{out_sequence.name}{i:05d}" -# image_ext = "." + ("jpg" if save_ext == SaveFormat.JPG else save_ext.value) -# image_path = out_sequence.with_name(f"{image_name}{image_ext}") -# assert image_path.exists() -# -# -# def test_tv_save_sequence_wrong_path(tmp_path: Path) -> None: -# with pytest.raises(NotADirectoryError): -# tv_save_sequence(tmp_path / "folder" / "out") -# -# -# @pytest.mark.parametrize( -# "file_format", -# [ -# SaveFormat.PNG, -# # SaveFormat.JPG, -# # SaveFormat.BMP, -# # SaveFormat.TGA, -# # SaveFormat.TIFF, -# ], -# ) -# @pytest.mark.parametrize("fill_background", [False, True]) -# @pytest.mark.parametrize("folder_pattern", [r"folder_%li_%ln_%fi"]) -# @pytest.mark.parametrize("file_pattern", [r"file_%li_%ln_%ii_%in_%fi"]) -# @pytest.mark.parametrize("visible_layers_only", [False, True]) -# @pytest.mark.parametrize("all_images", [False, True]) -# def test_tv_clip_save_structure_json( -# test_project: TVPProject, -# ppm_sequence: list[Path], -# tmp_path: Path, -# file_format: SaveFormat, -# fill_background: bool, -# folder_pattern: str, -# file_pattern: str, -# visible_layers_only: bool, -# all_images: bool, -# ) -> None: -# # Import some frames -# load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) -# load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=3) -# -# # Hide that layer -# seq_3 = load_sequence_with_name(ppm_sequence[0], name="sequence_3", count=3) -# tv_layer_display_set(seq_3, False) -# -# # Remove the first layer (which is in the last position) -# last_layer = list(current_clip_layers())[-1] -# tv_layer_kill(last_layer.id) -# -# out_json_path = tmp_path / "clip.json" -# tv_clip_save_structure_json( -# out_json_path, -# file_format, -# fill_background, -# folder_pattern, -# file_pattern, -# visible_layers_only, -# all_images, -# ) -# assert out_json_path.exists() -# -# for layer in current_clip_layers(): -# if visible_layers_only and not layer.visibility: -# continue -# -# # Check that the layer folders exist -# layer_folder_name = apply_folder_pattern(folder_pattern, layer) -# layer_folder = tmp_path / layer_folder_name -# assert layer_folder.exists() -# -# tv_layer_set(layer.id) -# for i, instance in enumerate(get_instance_frames()): -# frame, name = instance -# -# image_name = apply_file_pattern( -# file_pattern, -# layer, -# image_index=frame + 1, -# image_name=name, -# file_index=i, -# ) -# -# # Check that the images exist -# ext = "." + file_format.value -# image_path = (layer_folder / image_name).with_suffix(ext) -# assert image_path.exists() -# -# -# def test_tv_clip_save_structure_json_file_doesnt_exist(tmp_path: Path) -> None: -# with pytest.raises(ValueError, match="destination folder doesn't exist"): -# tv_clip_save_structure_json(tmp_path / "folder" / "out.json", SaveFormat.PNG) -# -# -# @pytest.mark.parametrize( -# "test_case", -# [ -# (PSDSaveMode.ALL, {}), -# (PSDSaveMode.IMAGE, {"image": 0}), -# (PSDSaveMode.MARKIN, {"mark_in": 0, "mark_out": 5}), -# ], -# ) -# def test_tv_clip_save_structure_psd( -# test_project: TVPProject, -# ppm_sequence: list[Path], -# tmp_path: Path, -# test_case: tuple[PSDSaveMode, dict[str, Any]], -# ) -> None: -# out_psd = tmp_path / "out.psd" -# -# load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) -# load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=2) -# -# mode, args = test_case -# tv_clip_save_structure_psd(out_psd, mode, **args) -# -# if mode == PSDSaveMode.MARKIN: -# # It's a sequence of numbered PSD files -# for i, _ in enumerate(get_instance_frames()): -# out_psd_frame = out_psd.with_stem(f"{out_psd.stem}{i:05d}") -# assert out_psd_frame.exists() -# else: -# # It's a single PSD file -# assert out_psd.exists() -# -# +from __future__ import annotations + +import itertools +import re +from collections.abc import Iterable, Iterator +from pathlib import Path +from typing import Any + +import pytest + +from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError +from pytvpaint.george.grg_base import ( + FieldOrder, + SaveFormat, + SpriteLayout, + tv_save_mode_get, +) +from pytvpaint.george.grg_clip import ( + PSDSaveMode, + TVPClip, + tv_bookmark_clear, + tv_bookmark_next, + tv_bookmark_prev, + tv_bookmark_set, + tv_bookmarks_enum, + tv_clip_action_get, + tv_clip_action_set, + tv_clip_close, + tv_clip_color_get, + tv_clip_color_set, + tv_clip_current_id, + tv_clip_dialog_get, + tv_clip_dialog_set, + tv_clip_duplicate, + tv_clip_enum_id, + tv_clip_hidden_get, + tv_clip_hidden_set, + tv_clip_info, + tv_clip_move, + tv_clip_name_get, + tv_clip_name_set, + tv_clip_new, + tv_clip_note_get, + tv_clip_note_set, + tv_clip_save_structure_csv, + tv_clip_save_structure_flix, + tv_clip_save_structure_json, + tv_clip_save_structure_psd, + tv_clip_save_structure_sprite, + tv_clip_select, + tv_clip_selection_get, + tv_clip_selection_set, + tv_first_image, + tv_last_image, + tv_layer_image, + tv_layer_image_get, + tv_load_sequence, + tv_save_clip, + tv_save_display, + tv_save_sequence, + tv_sound_clip_adjust, + tv_sound_clip_info, + tv_sound_clip_new, + tv_sound_clip_reload, + tv_sound_clip_remove, +) +from pytvpaint.george.grg_layer import ( + LayerType, + TVPLayer, + tv_instance_get_name, + tv_layer_current_id, + tv_layer_display_set, + tv_layer_get_id, + tv_layer_info, + tv_layer_kill, + tv_layer_rename, + tv_layer_set, +) +from pytvpaint.george.grg_project import TVPProject, tv_save_project +from pytvpaint.george.grg_scene import tv_scene_current_id, tv_scene_new +from tests.conftest import FixtureYield, test_scene + + +def test_tv_clip_info(test_clip: TVPClip) -> None: + assert tv_clip_info(test_clip.id) + + +def test_tv_clip_info_wrong_id(test_clip: TVPClip) -> None: + with pytest.raises(NoObjectWithIdError): + tv_clip_info(-2) + + +def test_tv_clip_enum_id(test_scene: int) -> None: + clips: list[int] = [] + + for i in range(5): + tv_clip_new(f"clip_{i}") + clips.append(tv_clip_current_id()) + + for i, clip_id in enumerate(clips): + # We offset 1 because of default clip + assert tv_clip_enum_id(test_scene, i + 1) == clip_id + + +def test_tv_clip_enum_id_wrong_scene_id() -> None: + with pytest.raises(GeorgeError): + tv_clip_enum_id(-2, 0) + + +@pytest.mark.parametrize("pos", [-1, 1, 50]) +def test_tv_clip_enum_id_wrong_clip_pos(test_scene: int, pos: int) -> None: + with pytest.raises(GeorgeError): + tv_clip_enum_id(test_scene, pos) + + +def test_tv_clip_current_id() -> None: + assert tv_clip_current_id() + + +@pytest.mark.parametrize("name", ["", "a", "0", "lfseflj0", "clip with spaces"]) +def test_tv_clip_new(test_scene: int, name: str) -> None: + tv_clip_new(name) + assert tv_clip_info(tv_clip_current_id()).name == name + + +def test_tv_clip_duplicate(test_scene: int, test_clip: TVPClip) -> None: + tv_clip_duplicate(test_clip.id) + dup_clip = tv_clip_info(tv_clip_current_id()) + assert dup_clip == test_clip + + +def test_tv_clip_close(test_clip: TVPClip) -> None: + tv_clip_close(test_clip.id) + with pytest.raises(NoObjectWithIdError): + tv_clip_info(test_clip.id) + + +def test_tv_clip_name_get(test_clip: TVPClip) -> None: + assert tv_clip_name_get(test_clip.id) == test_clip.name + + +def test_tv_clip_name_get_wrong_id() -> None: + with pytest.raises(NoObjectWithIdError): + tv_clip_name_get(-1) + + +@pytest.mark.parametrize("name", ["_", "a", "0", "lfseflj0"]) +def test_tv_clip_name_set(test_clip: TVPClip, name: str) -> None: + tv_clip_name_set(test_clip.id, name) + assert tv_clip_info(test_clip.id).name == name + + +def test_tv_clip_name_set_wrong_id() -> None: + with pytest.raises(NoObjectWithIdError): + tv_clip_name_get(-1) + + +# "Copy" the fixture to use it twice in the above test +# See: https://stackoverflow.com/questions/36100624/pytest-use-same-fixture-twice-in-one-function +destination_scene = test_scene + + +@pytest.mark.parametrize("new_pos", range(5)) +def test_tv_clip_move( + test_scene: int, test_clip: TVPClip, destination_scene: int, new_pos: int +) -> None: + for i in range(5): + tv_clip_new(f"other_clip_{i}") + + tv_clip_move(test_clip.id, destination_scene, new_pos) + + # Ensure that it's not in the first scene + with pytest.raises(GeorgeError): + tv_clip_enum_id(test_scene, 1) + + # Ensure that it's at the right position in the other scene + tv_clip_enum_id(destination_scene, new_pos) + + +def test_tv_clip_hidden_get(test_clip: TVPClip) -> None: + assert tv_clip_hidden_get(test_clip.id) == test_clip.is_hidden + + +def test_tv_clip_hidden_get_wrong_id() -> None: + with pytest.raises(NoObjectWithIdError): + tv_clip_hidden_get(-1) + + +@pytest.mark.parametrize("hidden", [True, False]) +def test_tv_clip_hidden_set(test_clip: TVPClip, hidden: bool) -> None: + tv_clip_hidden_set(test_clip.id, hidden) + assert tv_clip_info(test_clip.id).is_hidden == hidden + + +@pytest.mark.parametrize("hidden", [True, False]) +def test_tv_clip_hidden_set_wrong_id(hidden: bool) -> None: + with pytest.raises(NoObjectWithIdError): + tv_clip_hidden_set(-1, hidden) + + +def test_tv_clip_select(test_scene: int, test_clip: TVPClip) -> None: + first_clip = tv_clip_enum_id(test_scene, 0) + tv_clip_select(first_clip) + + assert not tv_clip_info(test_clip.id).is_current + tv_clip_select(test_clip.id) + assert tv_clip_info(test_clip.id).is_current + + +def test_tv_clip_select_also_selects_scene( + test_project: TVPProject, + test_scene: int, + test_clip: TVPClip, +) -> None: + tv_scene_new() + assert tv_scene_current_id() != test_scene + tv_clip_select(test_clip.id) + assert tv_scene_current_id() == test_scene + + +def test_tv_clip_selection_get(test_clip: TVPClip) -> None: + assert tv_clip_selection_get(test_clip.id) == test_clip.is_selected + + +def test_tv_clip_selection_get_wrong_id() -> None: + with pytest.raises(NoObjectWithIdError): + tv_clip_selection_get(-1) + + +@pytest.mark.parametrize("select", [True, False]) +def test_tv_clip_selection_set(test_clip: TVPClip, select: bool) -> None: + tv_clip_selection_set(test_clip.id, select) + assert tv_clip_info(test_clip.id).is_selected == select + + +@pytest.mark.parametrize("select", [True, False]) +def test_tv_clip_selection_set_wrong_id(test_clip: TVPClip, select: bool) -> None: + with pytest.raises(NoObjectWithIdError): + tv_clip_selection_set(-1, select) + + +def test_tv_first_image(test_clip: TVPClip) -> None: + assert tv_first_image() == test_clip.first_frame + + +def test_tv_last_image(test_clip: TVPClip) -> None: + assert tv_last_image() == test_clip.last_frame + + +@pytest.mark.parametrize("offset_count", [None, *itertools.product([0, 1], [0, 1])]) +@pytest.mark.parametrize("field_order", [None, *FieldOrder]) +@pytest.mark.parametrize("stretch", [False, True]) +@pytest.mark.parametrize("time_stretch", [False, True]) +@pytest.mark.parametrize("preload", [False, True]) +def test_tv_load_sequence( + ppm_sequence: list[Path], + test_clip: TVPClip, + offset_count: tuple[int, int] | None, + field_order: FieldOrder | None, + stretch: bool, + time_stretch: bool, + preload: bool, +) -> None: + total_images = len(ppm_sequence) + first_image = ppm_sequence[0] + + images_loaded = tv_load_sequence( + first_image, + offset_count, + field_order, + stretch, + time_stretch, + preload, + ) + + if offset_count: + offset, count = offset_count + should_load = total_images if count <= 0 else min(count, total_images - offset) + else: + should_load = total_images + + assert should_load == images_loaded + + # Find the extension at the end (including file number) + ext_match = re.search(r"\d+\.[a-z0-9]+$", str(first_image)) + assert ext_match + ext_start, _ = ext_match.span() + + layer_name_cut = str(first_image)[:ext_start] + + layer = tv_layer_info(tv_layer_current_id()) + + assert layer.name == layer_name_cut + assert layer.type == LayerType.SEQUENCE + assert layer.first_frame == 0 + assert layer.last_frame == should_load - 1 + + +def test_tv_load_sequence_sequence_does_not_exist(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError, match="File not found"): + tv_load_sequence(tmp_path / "file.001.png") + + +@pytest.fixture +def bookmarks(test_clip: TVPClip) -> FixtureYield[Iterable[int]]: + n_bookmarks = 5 + for frame in range(n_bookmarks): + tv_bookmark_set(frame) + yield range(n_bookmarks) + + +def test_tv_bookmarks_enum(bookmarks: Iterable[int]) -> None: + for pos, mark in enumerate(bookmarks): + assert tv_bookmarks_enum(pos) == mark + + +@pytest.mark.parametrize("frame", range(5)) +def test_tv_bookmark_set(test_clip: TVPClip, frame: int) -> None: + tv_bookmark_set(frame) + assert tv_bookmarks_enum(0) == frame + + +@pytest.mark.parametrize("frame", range(5)) +def test_tv_bookmark_clear(test_clip: TVPClip, frame: int) -> None: + tv_bookmark_set(frame) + assert tv_bookmarks_enum(0) == frame + + tv_bookmark_clear(frame) + + with pytest.raises(GeorgeError, match="No bookmark"): + tv_bookmarks_enum(0) + + +@pytest.mark.parametrize("frame", range(5)) +def test_tv_bookmark_next(test_clip: TVPClip, frame: int) -> None: + tv_bookmark_set(frame) + tv_bookmark_next() + assert tv_layer_image_get() == frame + + +@pytest.mark.parametrize("frame", range(5)) +def test_tv_bookmark_prev(test_clip: TVPClip, frame: int) -> None: + # Set the current image far away + tv_layer_image(50) + + tv_bookmark_set(frame) + tv_bookmark_prev() + + assert tv_layer_image_get() == frame + + +def test_tv_clip_color_get(test_clip: TVPClip) -> None: + assert tv_clip_color_get(test_clip.id) in range(27) + + +@pytest.mark.parametrize("color_index", range(27)) +def test_tv_clip_color_set(test_clip: TVPClip, color_index: int) -> None: + tv_clip_color_set(test_clip.id, color_index) + assert tv_clip_color_get(test_clip.id) == color_index + + +def test_tv_clip_action_get(test_clip: TVPClip) -> None: + assert tv_clip_action_get(test_clip.id) == "" + + +def test_tv_clip_action_get_wrong_id() -> None: + with pytest.raises(NoObjectWithIdError): + tv_clip_action_get(-3) + + +# Note: we removed the \n test because there's a bug with TVPaint that handle control characters +TEST_TEXTS = ["", "l", "0", "ab", "a0l", "ap*"] # "a\nb"] + + +@pytest.mark.parametrize("text", TEST_TEXTS) +def test_tv_clip_action_set(test_clip: TVPClip, text: str) -> None: + tv_clip_action_set(test_clip.id, text) + assert tv_clip_action_get(test_clip.id) == text + + +def test_tv_clip_action_set_wrong_id() -> None: + with pytest.raises(GeorgeError): + tv_clip_action_set(-2, "test") + + +def test_tv_clip_dialog_get(test_clip: TVPClip) -> None: + assert tv_clip_dialog_get(test_clip.id) == "" + + +def test_tv_clip_dialog_get_wrong_id() -> None: + with pytest.raises(GeorgeError): + tv_clip_dialog_get(-2) + + +@pytest.mark.parametrize("text", TEST_TEXTS) +def test_tv_clip_dialog_set(test_clip: TVPClip, text: str) -> None: + tv_clip_dialog_set(test_clip.id, text) + assert tv_clip_dialog_get(test_clip.id) == text + + +def test_tv_clip_dialog_set_wrong_id() -> None: + with pytest.raises(GeorgeError): + tv_clip_dialog_set(-2, "test") + + +def test_tv_clip_note_get(test_clip: TVPClip) -> None: + assert tv_clip_note_get(test_clip.id) == "" + + +def test_tv_clip_note_get_wrong_id() -> None: + with pytest.raises(GeorgeError): + tv_clip_dialog_get(-2) + + +@pytest.mark.parametrize("text", TEST_TEXTS) +def test_tv_clip_note_set(test_clip: TVPClip, text: str) -> None: + tv_clip_note_set(test_clip.id, text) + assert tv_clip_note_get(test_clip.id) == text + + +def test_tv_clip_note_set_wrong_id() -> None: + with pytest.raises(GeorgeError): + tv_clip_note_set(-2, "test") + + +def test_tv_save_clip(tmp_path: Path) -> None: + clip_tvp = tmp_path / "clip.tvp" + tv_save_clip(clip_tvp) + assert clip_tvp.exists() + + +@pytest.mark.skip("Will block the UI") +def test_tv_save_clip_folder_does_not_exist(tmp_path: Path) -> None: + with pytest.raises(GeorgeError, match="Can't create file"): + tv_save_clip(tmp_path / "folder" / "out.tvpx") + + +def test_tv_save_display(tmp_path: Path) -> None: + save_ext, _ = tv_save_mode_get() + ext = "jpg" if save_ext == SaveFormat.JPG else save_ext.value + out_display = (tmp_path / "out").with_suffix("." + ext) + tv_save_display(out_display) + assert out_display.exists() + + +def current_clip_layers() -> Iterator[TVPLayer]: + """Iterates through the current clip layers""" + pos = 0 + while True: + try: + lid = tv_layer_get_id(pos) + yield tv_layer_info(lid) + except GeorgeError: + break + pos += 1 + + +def get_instance_frames() -> Iterator[tuple[int, str]]: + """Iterates through the instances of the current layer""" + layer = tv_layer_current_id() + frame = 0 + while True: + try: + name = tv_instance_get_name(layer, frame) + yield frame, name + except NoObjectWithIdError: + break + frame += 1 + + +def apply_folder_pattern(initial_pattern: str | None, layer: TVPLayer) -> str: + # This is the default folder pattern + if initial_pattern is None: + return f"[{layer.position:03d}] {layer.name}" + + patterns = { + r"%li": str(layer.position), + r"%ln": layer.name, + r"%fi": r"%fi", # Couldn't make it work + } + + for pattern, rep in patterns.items(): + initial_pattern = initial_pattern.replace(pattern, rep) + + return initial_pattern + + +def apply_file_pattern( + initial_pattern: str | None, + layer: TVPLayer, + image_index: int, + image_name: str, + file_index: int, +) -> str: + # This is the default file pattern + if initial_pattern is None: + return f"[{image_index:03d}] {layer.name}" + + # When the instance name is empty it takes the image index + if image_name == "": + image_name = str(image_index) + + patterns = { + r"%li": str(layer.position), + r"%ln": layer.name, + r"%ii": str(image_index), + r"%in": image_name, + r"%fi": str(file_index), + } + + for pattern, rep in patterns.items(): + initial_pattern = initial_pattern.replace(pattern, rep) + + return initial_pattern + + +def load_sequence_with_name(first_frame: Path, name: str, count: int) -> int: + """Load an image sequence and rename the layer""" + tv_load_sequence(first_frame, offset_count=(0, count)) + layer = tv_layer_current_id() + tv_layer_rename(layer, name) + return layer + + +@pytest.mark.parametrize( + "mark_in, mark_out", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)] +) +def test_tv_save_sequence( + test_project: TVPProject, + tmp_path: Path, + ppm_sequence: list[Path], + mark_in: int | None, + mark_out: int | None, +) -> None: + tv_load_sequence(ppm_sequence[0]) + + save_ext, _ = tv_save_mode_get() + out_sequence = tmp_path / "out" + tv_save_sequence(out_sequence, mark_in, mark_out) + + clip = tv_clip_info(tv_clip_current_id()) + start, end = ( + (mark_in, mark_out) + if mark_in is not None and mark_out is not None + else (clip.first_frame, clip.last_frame) + ) + + for i in range(end - start): + image_name = f"{out_sequence.name}{i:05d}" + image_ext = "." + ("jpg" if save_ext == SaveFormat.JPG else save_ext.value) + image_path = out_sequence.with_name(f"{image_name}{image_ext}") + assert image_path.exists() + + +def test_tv_save_sequence_wrong_path(tmp_path: Path) -> None: + with pytest.raises(NotADirectoryError): + tv_save_sequence(tmp_path / "folder" / "out") + + +@pytest.mark.parametrize( + "file_format", + [ + SaveFormat.PNG, + # SaveFormat.JPG, + # SaveFormat.BMP, + # SaveFormat.TGA, + # SaveFormat.TIFF, + ], +) +@pytest.mark.parametrize("fill_background", [False, True]) +@pytest.mark.parametrize("folder_pattern", [r"folder_%li_%ln_%fi"]) +@pytest.mark.parametrize("file_pattern", [r"file_%li_%ln_%ii_%in_%fi"]) +@pytest.mark.parametrize("visible_layers_only", [False, True]) +@pytest.mark.parametrize("all_images", [False, True]) +def test_tv_clip_save_structure_json( + test_project: TVPProject, + ppm_sequence: list[Path], + tmp_path: Path, + file_format: SaveFormat, + fill_background: bool, + folder_pattern: str, + file_pattern: str, + visible_layers_only: bool, + all_images: bool, +) -> None: + # Import some frames + load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) + load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=3) + + # Hide that layer + seq_3 = load_sequence_with_name(ppm_sequence[0], name="sequence_3", count=3) + tv_layer_display_set(seq_3, False) + + # Remove the first layer (which is in the last position) + clip_layers = [] + for layer in current_clip_layers(): + if layer.type == LayerType.CAMERA: + tv_layer_display_set(layer.id, False) + continue + clip_layers.append(layer) + + last_layer = clip_layers[-1] + # assert True is False + tv_layer_kill(last_layer.id) + + out_json_path = tmp_path / "clip.json" + tv_clip_save_structure_json( + out_json_path, + file_format, + fill_background, + folder_pattern, + file_pattern, + visible_layers_only, + all_images, + ) + assert out_json_path.exists() + + for layer in clip_layers: + if visible_layers_only and not layer.visibility: + continue + + # Check that the layer folders exist + layer_folder_name = apply_folder_pattern(folder_pattern, layer) + print('layer_folder_name ===> ', layer_folder_name) + layer_folder = tmp_path / layer_folder_name + assert layer_folder.exists() + + tv_layer_set(layer.id) + for i, instance in enumerate(get_instance_frames()): + frame, name = instance + + image_name = apply_file_pattern( + file_pattern, + layer, + image_index=frame + 1, + image_name=name, + file_index=i, + ) + + # Check that the images exist + ext = "." + file_format.value + image_path = (layer_folder / image_name).with_suffix(ext) + assert image_path.exists() + + +def test_tv_clip_save_structure_json_file_doesnt_exist(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="destination folder doesn't exist"): + tv_clip_save_structure_json(tmp_path / "folder" / "out.json", SaveFormat.PNG) + + +@pytest.mark.parametrize( + "test_case", + [ + (PSDSaveMode.ALL, {}), + (PSDSaveMode.IMAGE, {"image": 0}), + (PSDSaveMode.MARKIN, {"mark_in": 0, "mark_out": 5}), + ], +) +def test_tv_clip_save_structure_psd( + test_project: TVPProject, + ppm_sequence: list[Path], + tmp_path: Path, + test_case: tuple[PSDSaveMode, dict[str, Any]], +) -> None: + out_psd = tmp_path / "out.psd" + + load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) + load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=2) + + mode, args = test_case + tv_clip_save_structure_psd(out_psd, mode, **args) + + if mode == PSDSaveMode.MARKIN: + # It's a sequence of numbered PSD files + for i, _ in enumerate(get_instance_frames()): + out_psd_frame = out_psd.with_stem(f"{out_psd.stem}{i:05d}") + assert out_psd_frame.exists() + else: + # It's a single PSD file + assert out_psd.exists() + + # def test_tv_tv_clip_save_structure_psd_file_does_not_exist(tmp_path: Path) -> None: # with pytest.raises(ValueError, match="destination folder doesn't exist"): # tv_clip_save_structure_psd(tmp_path / "folder" / "out.psd", PSDSaveMode.ALL) From c70a54e6f39a9fbad66e165a30f1ab542070a950 Mon Sep 17 00:00:00 2001 From: Radouane Lahmidi Date: Fri, 2 May 2025 20:40:03 +0200 Subject: [PATCH 03/26] CLEAN code --- pytvpaint/clip.py | 4 ++-- pytvpaint/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 723766b..0938e4d 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -574,12 +574,12 @@ def render( FileNotFoundError: if the render failed and no files were found on disk or missing frames Note: - This functions uses the clip's range as a basis (start-end). This is different from a project range, which + This function uses the clip's range as a basis (start-end). This is different from the project range, which uses the project timeline. For more details on the differences in frame ranges and the timeline in TVPaint, please check the `Usage/Rendering` section of the documentation. Warning: - Even tough pytvpaint does a pretty good job of correcting the frame ranges for rendering, we're still + Even though pytvpaint does a pretty good job of correcting the frame ranges for rendering, we're still encountering some weird edge cases where TVPaint will consider the range invalid for seemingly no reason. """ default_start = self.mark_in or self.start diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index 37aee3e..eea2842 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -356,7 +356,7 @@ def render_context( should_be_visible = not layer_selection or layer in layer_selection layer.is_visible = should_be_visible - # Do the render + # Do the rendering yield # Restore the previous values From 276ec0484e5fe3b152b9d2c73082d30e6ae66988 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 19 Aug 2025 18:07:40 +0200 Subject: [PATCH 04/26] ADD search by regex in appropriate functions --- pytvpaint/clip.py | 75 ++++++++++++++++++++++++++++++++++---------- pytvpaint/project.py | 68 +++++++++++++++++++++++++++------------ pytvpaint/scene.py | 20 +++++++++--- pytvpaint/utils.py | 33 ++++++++++++------- 4 files changed, 143 insertions(+), 53 deletions(-) diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 0938e4d..7b7fd52 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from collections.abc import Iterator from pathlib import Path from typing import TYPE_CHECKING, cast @@ -97,6 +98,7 @@ def refresh(self) -> None: def make_current(self) -> None: """Make the clip the current one.""" + self.project.make_current() if george.tv_clip_current_id() == self.id: return george.tv_clip_select(self.id) @@ -439,9 +441,22 @@ def get_layer( self, by_id: int | None = None, by_name: str | None = None, + by_regex: re.Pattern[str] | None = None, ) -> Layer | None: - """Get a specific layer by id or name.""" - return utils.get_tvp_element(self.get_layers(), by_id, by_name) + """Get a specific layer by id or name. + + Args: + by_id: search by id. Defaults to None. + by_name: search by name, search is case-insensitive. Defaults to None. + by_regex: search by name using a compiled regex, case-sensitivity is left to the regex. Defaults to None. + + Raises: + ValueError: if none of the search arguments where provided + + Returns: + Layer | None: the searched element or None if search was unsuccessful + """ + return utils.get_tvp_element(self.get_layers(), by_id=by_id, by_name=by_name, by_regex=by_regex) @set_as_current def add_layer(self, layer_name: str) -> Layer: @@ -952,24 +967,38 @@ def get_layer_color( self, by_index: int | None = None, by_name: str | None = None, + by_regex: re.Pattern[str] | None = None, ) -> LayerColor | None: """Get a layer color by index or name. + Args: + by_index: search by color index. Defaults to None. + by_name: search by name, search is case-insensitive. Defaults to None. + by_regex: search by name using a compiled regex, case-sensitivity is left to the regex. Defaults to None. + Raises: - ValueError: if none of the arguments `by_index` and `by_name` where provided + ValueError: if none of the search arguments where provided """ - if not by_index and by_name: - raise ValueError( - "At least one value (by_index or by_name) must be provided" - ) + values = (by_index, by_name, by_regex) + if not any(v is not None for v in values): + raise ValueError(f"At least one value ({' or '.join(values)} must be provided") if by_index is not None: return next(c for i, c in enumerate(self.layer_colors) if i == by_index) - try: - return next(c for c in self.layer_colors if c.name == by_name) - except StopIteration: - return None + if by_name is not None: + try: + return next(c for c in self.layer_colors if c.name.lower() == by_name.lower()) + except StopIteration: + return None + + if by_regex is not None: + try: + return next(c for c in self.layer_colors if by_regex.search(c.name)) + except StopIteration: + return None + + return None @property def bookmarks(self) -> Iterator[int]: @@ -1016,17 +1045,31 @@ def sounds(self) -> Iterator[ClipSound]: def get_sound( self, - by_id: int | None = None, + by_track_index: int | None = None, by_path: Path | str | None = None, ) -> ClipSound | None: - """Get a clip sound by id or by path. + """Get a clip sound by track index or path. + + Args: + by_track_index: search by track index. Defaults to None. + by_path: search by path. Defaults to None. Raises: - ValueError: if sound object could not be found in clip + ValueError: if none of the search arguments where provided + + Returns: + ClipSound | None: the searched element or None if search was unsuccessful """ + values = (by_track_index, by_path) + if not any(v is not None for v in values): + raise ValueError(f"At least one value ({' or '.join(values)} must be provided") + for sound in self.sounds: - if (by_id and sound.id == by_id) or (by_path and sound.path == by_path): - return sound + if by_track_index is not None and sound.by_track_index != by_track_index: + continue + if by_path is not None and sound.path != Path(by_path): + continue + return sound return None diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 49b07ca..89d3499 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -2,8 +2,9 @@ from __future__ import annotations -from collections.abc import Iterator +import re from pathlib import Path +from collections.abc import Iterator from typing import TYPE_CHECKING from fileseq.filesequence import FileSequence @@ -317,13 +318,26 @@ def get_project( cls, by_id: str | None = None, by_name: str | None = None, + by_regex: re.Pattern[str] | None = None, + by_path: str | Path | None = None, ) -> Project | None: - """Find a project by id or by name.""" - for project in Project.open_projects(): - if (by_id and project.id == by_id) or (by_name and project.name == by_name): - return project + """Find a project by id or by name or by path. - return None + Args: + by_id: search by id. Defaults to None. + by_name: search by name, search is case-insensitive. Defaults to None. + by_regex: search by name using a compiled regex, case-sensitivity is left to the regex. Defaults to None. + by_path: search by path. Defaults to None. + + Raises: + ValueError: if none of the search arguments where provided + + Returns: + Project | None: the searched element or None if search was unsuccessful + """ + + return utils.get_tvp_element(Project.open_projects(), by_id=by_id, by_name=by_name, + by_regex=by_regex, by_path=by_path) @staticmethod def current_scene_ids() -> Iterator[int]: @@ -351,15 +365,19 @@ def scenes(self) -> Iterator[Scene]: for scene_id in self.current_scene_ids(): yield Scene(scene_id, self) - def get_scene( - self, - by_id: int | None = None, - by_name: str | None = None, - ) -> Scene | None: - """Find a scene in the project by id or name.""" + def get_scene(self, scene_id: int) -> Scene | None: + """Find a scene in the project by id. + + Args: + scene_id: scene id + + Returns: + Scene | None: the searched element or None if search was unsuccessful + """ for scene in self.scenes: - if (by_id and scene.id == by_id) or (by_name and scene.name == by_name): - return scene + if scene.id != scene_id: + continue + return scene return None @@ -399,19 +417,29 @@ def get_clip( self, by_id: int | None = None, by_name: str | None = None, + by_regex: re.Pattern[str] | None = None, scene_id: int | None = None, ) -> Clip | None: - """Find a clip by id, name or scene_id.""" + """Find a clip by id or name, filter search by scene_id if needed. + + Args: + by_id: search by id. Defaults to None. + by_name: search by name, search is case-insensitive. Defaults to None. + by_regex: search by name using a compiled regex, case-sensitivity is left to the regex. Defaults to None. + scene_id: parent scene id. Defaults to None. + + Raises: + ValueError: if none of the search arguments where provided + + Returns: + Clip | None: the searched element or None if search was unsuccessful + """ clips = self.clips if scene_id: selected_scene = self.get_scene(by_id=scene_id) clips = selected_scene.clips if selected_scene else clips - for clip in clips: - if (by_id and clip.id == by_id) or (by_name and clip.name == by_name): - return clip - - return None + return utils.get_tvp_element(clips, by_id=by_id, by_name=by_name, by_regex=by_regex) def add_clip(self, clip_name: str, scene: Scene | None = None) -> Clip: """Add a new clip in the given scene or the current one if no scene provided.""" diff --git a/pytvpaint/scene.py b/pytvpaint/scene.py index eefea3d..72b368f 100644 --- a/pytvpaint/scene.py +++ b/pytvpaint/scene.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from collections.abc import Iterator from pytvpaint import george, utils @@ -143,13 +144,22 @@ def get_clip( self, by_id: int | None = None, by_name: str | None = None, + by_regex: re.Pattern[str] | None = None, ) -> Clip | None: - """Find a clip by id or by name.""" - for clip in self.clips: - if (by_id and clip.id == by_id) or (by_name and clip.name == by_name): - return clip + """Find a clip by id or by name. - return None + Args: + by_id: search by id. Defaults to None. + by_name: search by name, search is case-insensitive. Defaults to None. + by_regex: search by name using a compiled regex, case-sensitivity is left to the regex. Defaults to None. + + Raises: + ValueError: if none of the search arguments where provided + + Returns: + Clip | None: the searched element or None if search was unsuccessful + """ + return utils.get_tvp_element(self.clips, by_id=by_id, by_name=by_name, by_regex=by_regex) @set_as_current def add_clip(self, clip_name: str) -> Clip: diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index eea2842..51fcfb0 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -2,8 +2,8 @@ from __future__ import annotations -import contextlib import re +import contextlib from abc import ABC, abstractmethod from collections.abc import Generator, Iterable, Iterator from pathlib import Path @@ -332,7 +332,6 @@ def render_context( format_opts: the custom format options as strings. Defaults to None. layer_selection: the layers to render. Defaults to None. """ - from pytvpaint.clip import Clip # Save the current state pre_alpha_save_mode = george.tv_alpha_save_mode_get() @@ -349,7 +348,7 @@ def render_context( layers_visibility = [] if layer_selection: - clip = Clip.current_clip() + clip = layer_selection[0].clip layers_visibility = [(layer, layer.is_visible) for layer in clip.layers] # Show and hide the clip layers to render for layer, _ in layers_visibility: @@ -414,39 +413,49 @@ def id(self) -> int | str: ... def name(self) -> str: ... -TVPElementType = TypeVar("TVPElementType", bound=_TVPElement) +class _TVPElementWithPath(_TVPElement): + + @property + def path(self) -> Path: ... + + +_TVPElementType = TypeVar("_TVPElementType", bound=_TVPElement) +_TVPElementWithPathType = TypeVar("_TVPElementWithPathType", bound=_TVPElementWithPath) def get_tvp_element( - tvp_elements: Iterator[TVPElementType], + tvp_elements: Iterator[_TVPElementType | _TVPElementWithPathType], by_id: int | str | None = None, by_name: str | None = None, + by_regex: re.Pattern[str] | None = None, by_path: str | Path | None = None, -) -> TVPElementType | None: +) -> _TVPElementType | _TVPElementWithPathType | None: """Search for a TVPaint element by attributes. Args: tvp_elements: a collection of TVPaint objects by_id: search by id. Defaults to None. by_name: search by name, search is case-insensitive. Defaults to None. + by_regex: search by name using a compiled regex, case-sensitivity is left to the regex. Defaults to None. by_path: search by path. Defaults to None. Raises: - ValueError: if bad arguments were given + ValueError: if none of the search arguments where provided Returns: - TVPElementType | None: the found element + _TVPElementType | None: the searched element or None if search was unsuccessful """ - if by_id is None and by_name is None: - raise ValueError( - "At least one of the values (id or name) must be provided, none found !" - ) + values = (by_id, by_name, by_regex, by_path) + if not any(v is not None for v in values): + raise ValueError(f"At least one value ({' or '.join(values)} must be provided") for element in tvp_elements: if by_id is not None and element.id != by_id: continue if by_name is not None and element.name.lower() != by_name.lower(): continue + if by_regex is not None and element.name != by_regex.search(element.name): + continue if by_path is not None and getattr(element, "path") != Path(by_path): continue return element From 4407261a491ae72e769f8d421060f04943a76192 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 19 Aug 2025 18:08:03 +0200 Subject: [PATCH 05/26] ADD tv_panning layer function --- pytvpaint/george/grg_layer.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pytvpaint/george/grg_layer.py b/pytvpaint/george/grg_layer.py index d4ee801..61b5928 100644 --- a/pytvpaint/george/grg_layer.py +++ b/pytvpaint/george/grg_layer.py @@ -330,11 +330,11 @@ def tv_layer_select_info(full: bool = False) -> tuple[int, int]: """Get Selected frames in a layer. Args: - full: Always get the selection range, even on a non anim/ctg layer + full: returns the layers full range (from the first to the last instance), even on a non anim/ctg layer Returns: - frame: the start frame of the selection - count: the number of frames in the selection + frame: the start frame of the selection. If full is set to True will return the first frame in the layer + count: the number of frames in the selection. If full is set to True will return the number of frames in the layer Bug: The official documentation states that this functions selects the layer frames, it does not, it simply @@ -1403,3 +1403,16 @@ def tv_ctg_source_remove(ctg_layer_id: int, source_ids: list[int]) -> None: source_ids: list of layer Ids to remove as sources for the CTG layer """ send_cmd("tv_CTGSource", "remove", ctg_layer_id, *source_ids) + + +def tv_panning(x: int, y: int, move_fill: bool = False, anti_aliasing: bool = False) -> None: + """Apply a panning FX to teh current layer. + + Args: + x: new x position of the layer (position is calculated from the top left corner of the layer frame) + y: new y position of the layer (position is calculated from the top left corner of the layer frame) + move_fill: True to moved and fill all screen, False to only move images + anti_aliasing: apply antialiasing + """ + anti_aliasing = 0 if not anti_aliasing else 2 + send_cmd("tv_Panning ", x, y, int(move_fill), anti_aliasing) From 24116785528eeb3f9a3376ea0520fed377a9869e Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 19 Aug 2025 18:15:37 +0200 Subject: [PATCH 06/26] ADD cut, copy and pan to Layer class --- pytvpaint/layer.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index 00fd8da..6c551f2 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -177,7 +177,7 @@ def paste(self, at_frame: int | None) -> None: def select(self) -> None: """Select all frames in this instance.""" - self.layer.select_frames(self.start, (self.length - 1)) + self.layer.select_frames(self.start, self.end) @property def next(self) -> LayerInstance | None: @@ -1141,6 +1141,17 @@ def clear_marks(self) -> None: for frame, _ in self.marks: self.remove_mark(frame) + @set_as_current + def pan(self, position: tuple[int, int], move_fill: bool = False, anti_aliasing: bool = False) -> None: + """Apply a panning FX to teh current layer. + + Args: + position: new position of the layer (position is calculated from the top left corner of the layer frame) + move_fill: True to moved and fill all screen, False to only move images + anti_aliasing: apply antialiasing + """ + george.tv_panning(position[0], position[1], move_fill, anti_aliasing) + @set_as_current def select_frames(self, start: int, end: int) -> None: """Select the frames from a start and count. @@ -1188,11 +1199,24 @@ def copy_selection(self) -> None: """Copy the selected instances.""" george.tv_layer_copy() - @set_as_current - def paste_selection(self) -> None: - """Paste the previously copied instances.""" + def paste_selection(self, target_layer: Layer | None = None) -> None: + """Paste the layer into another layer (regardless of the project).""" + if target_layer: + target_layer.make_current() george.tv_layer_paste() + @set_as_current + def cut(self) -> None: + """Copy the layer (cuts all instances in layer).""" + self.select_all_frames() + self.cut_selection() + + @set_as_current + def copy(self) -> None: + """Copy the layer (copies all instances in layer).""" + self.select_all_frames() + self.copy_selection() + @refreshed_property @set_as_current def instances(self) -> Iterator[LayerInstance]: @@ -1249,6 +1273,7 @@ def get_instances(self, from_frame: int, to_frame: int) -> Iterator[LayerInstanc if layer_instance.start > to_frame: break + @set_as_current def add_instance( self, start: int | None = None, From e805c6894b80f2784406d2c1baea460076e3707e Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Wed, 7 Jan 2026 10:45:27 +0100 Subject: [PATCH 07/26] ADD guidelines support and misc fixes/improvements --- docs/credits.md | 2 +- pytvpaint/camera.py | 22 +- pytvpaint/clip.py | 87 ++-- pytvpaint/george/__init__.py | 1 + pytvpaint/george/client/parse.py | 33 +- pytvpaint/george/grg_base.py | 82 ++-- pytvpaint/george/grg_camera.py | 1 - pytvpaint/george/grg_clip.py | 9 +- pytvpaint/george/grg_guideline.py | 697 ++++++++++++++++++++++++++++++ pytvpaint/george/grg_layer.py | 4 +- pytvpaint/george/grg_project.py | 28 +- pytvpaint/guideline.py | 455 +++++++++++++++++++ pytvpaint/layer.py | 63 +-- pytvpaint/project.py | 78 +++- pytvpaint/utils.py | 33 +- 15 files changed, 1348 insertions(+), 247 deletions(-) create mode 100644 pytvpaint/george/grg_guideline.py create mode 100644 pytvpaint/guideline.py diff --git a/docs/credits.md b/docs/credits.md index 97b438f..4ee0cbf 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -3,7 +3,7 @@ ## :computer: Brunch Dev Team - [:simple-github:](https://github.com/rlahmidi) Radouane Lahmidi -- [:simple-github:](https://github.com/jhenrybrunch) Joseph Henry +- [:simple-github:](https://github.com/johhnry) Joseph Henry - [:simple-firefoxbrowser:](https://www.chloeoternaud.com/about) Chloe Oternaud - [:simple-github:](https://github.com/aprayez) Alexis Prayez diff --git a/pytvpaint/camera.py b/pytvpaint/camera.py index ede0a89..f6695ff 100644 --- a/pytvpaint/camera.py +++ b/pytvpaint/camera.py @@ -179,26 +179,20 @@ def points(self) -> Iterator[CameraPoint]: "results." ) - points_data = utils.position_generator( - lambda pos: george.tv_camera_enum_points(pos) - ) + points_data = utils.position_generator(lambda pos: george.tv_camera_enum_points(pos)) for index, point_data in enumerate(points_data): yield CameraPoint(index, camera=self, data=point_data) @set_as_current def get_point_data_at(self, position: float) -> InterpolationCameraPoint: """Get the points data interpolated at that position (between 0 and 1).""" - return InterpolationCameraPoint( - position, self, InterpolationCameraPoint.get_point_data_at(position) - ) + return InterpolationCameraPoint(position, self, InterpolationCameraPoint.get_point_data_at(position)) @george.min_version_compatible(min_version="12") @set_as_current def get_point_data_at_frame(self, frame: int) -> FrameCameraPoint: """Get the points data interpolated at the specified frame in the clip.""" - return FrameCameraPoint( - frame, self, FrameCameraPoint.get_point_data_at(self.clip, frame) - ) + return FrameCameraPoint(frame, self, FrameCameraPoint.get_point_data_at(self.clip, frame)) @set_as_current def remove_point(self, index: int) -> None: @@ -246,7 +240,7 @@ def __eq__(self, other: object) -> bool: other.refresh() return self._data == other._data - @property + @refreshed_property def data(self) -> george.TVPCameraPoint: """Returns the raw data of the point.""" return self._data @@ -402,8 +396,7 @@ def remove(self) -> None: the FrameCameraPoint instance is read-only and cannot be removed as it doesn't really exist """ log.warning( - "Read-Only InterpolationCameraPoint cannot be deleted as it doesn't really exist, " - "ignoring request." + "Read-Only InterpolationCameraPoint cannot be deleted as it doesn't really exist, " "ignoring request." ) return @@ -450,8 +443,5 @@ def remove(self) -> None: Warning: the FrameCameraPoint instance is read-only and cannot be removed as it doesn't really exist """ - log.warning( - "Read-Only FrameCameraPoint cannot be deleted as it doesn't really exist, " - "ignoring request." - ) + log.warning("Read-Only FrameCameraPoint cannot be deleted as it doesn't really exist, " "ignoring request.") return diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 7b7fd52..4f1c253 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -166,7 +166,9 @@ def name(self, value: str) -> None: """Set the clip name.""" if self.name == value: return - value = utils.get_unique_name(self.project.clip_names, value) + + clip_names = [clip.name for clip in self.project.clips if clip != clip] + value = utils.get_unique_name(clip_names, value) george.tv_clip_name_set(self.id, value) @refreshed_property @@ -310,7 +312,10 @@ def duplicate(self) -> Clip: """ george.tv_clip_duplicate(self.id) new_clip = self.project.current_clip - new_clip.name = utils.get_unique_name(self.project.clip_names, new_clip.name) + + clip_names = [clip.name for clip in self.project.clips if clip != new_clip] + new_clip.name = utils.get_unique_name(clip_names, new_clip.name) + return new_clip def remove(self) -> None: @@ -351,10 +356,7 @@ def get_layers( elif layer_data.type == george.LayerType.CAMERA: layer_class = CameraLayer # handle CTG layers like regular layers in TVP versions < 12 - elif ( - not george.is_tvp_version_below_12() - and layer_data.type is george.LayerType.SCRIBBLES - ): + elif not george.is_tvp_version_below_12() and layer_data.type is george.LayerType.SCRIBBLES: layer_class = CTGLayer if filter_types and layer_class not in filter_types: @@ -414,9 +416,7 @@ def camera_layer(self) -> CameraLayer | None: # if we're here then the camera layer has probably been deleted, let's recreate it by switching to the camera. george.tv_set_active_shape(george.TVPShape.CAMERA) - return cast( - CameraLayer, next(self.get_layers(filter_types=(CameraLayer,)), None) - ) + return cast(CameraLayer, next(self.get_layers(filter_types=(CameraLayer,)), None)) @property @set_as_current @@ -539,9 +539,7 @@ def _validate_range(self, start: int, end: int) -> None: (clip_mark_out if clip_mark_out else clip_end), ) if start < clip_full_range[0] or end > clip_full_range[1]: - raise ValueError( - f"Render ({start}-{end}) not in clip range ({clip_full_range})" - ) + raise ValueError(f"Render ({start}-{end}) not in clip range ({clip_full_range})") def _get_real_range(self, start: int, end: int) -> tuple[int, int]: # get project start to get real values @@ -630,9 +628,7 @@ def export_tvp(self, export_path: Path | str) -> None: george.tv_save_clip(export_path) if not export_path.exists(): - raise FileNotFoundError( - f"Could not find output at : {export_path.as_posix()}" - ) + raise FileNotFoundError(f"Could not find output at : {export_path.as_posix()}") @set_as_current def export_json( @@ -668,13 +664,9 @@ def export_json( export_path = Path(export_path) export_path.parent.mkdir(exist_ok=True, parents=True) - fill_background = bool( - background_mode not in [None, george.BackgroundMode.NONE] - ) + fill_background = bool(background_mode not in [None, george.BackgroundMode.NONE]) - with utils.render_context( - alpha_mode, background_mode, save_format, format_opts, layer_selection - ): + with utils.render_context(alpha_mode, background_mode, save_format, format_opts, layer_selection): george.tv_clip_save_structure_json( export_path, save_format, @@ -687,9 +679,7 @@ def export_json( ) if not export_path.exists(): - raise FileNotFoundError( - f"Could not find output at : {export_path.as_posix()}" - ) + raise FileNotFoundError(f"Could not find output at : {export_path.as_posix()}") @set_as_current def export_psd( @@ -745,9 +735,7 @@ def export_psd( assert FileSequence.findSequenceOnDisk(check_path) else: if not export_path.exists(): - raise FileNotFoundError( - f"Could not find output at : {export_path.as_posix()}" - ) + raise FileNotFoundError(f"Could not find output at : {export_path.as_posix()}") @set_as_current def export_csv( @@ -782,15 +770,11 @@ def export_csv( if export_path.suffix != ".csv": raise ValueError("Export path must have .csv extension") - with utils.render_context( - alpha_mode, background_mode, save_format, format_opts, layer_selection - ): + with utils.render_context(alpha_mode, background_mode, save_format, format_opts, layer_selection): george.tv_clip_save_structure_csv(export_path, all_images, exposure_label) if not export_path.exists(): - raise FileNotFoundError( - f"Could not find output at : {export_path.as_posix()}" - ) + raise FileNotFoundError(f"Could not find output at : {export_path.as_posix()}") @set_as_current def export_sprites( @@ -820,15 +804,11 @@ def export_sprites( export_path = Path(export_path) save_format = george.SaveFormat.from_extension(export_path.suffix) - with utils.render_context( - alpha_mode, background_mode, save_format, format_opts, layer_selection - ): + with utils.render_context(alpha_mode, background_mode, save_format, format_opts, layer_selection): george.tv_clip_save_structure_sprite(export_path, layout, space) if not export_path.exists(): - raise FileNotFoundError( - f"Could not find output at : {export_path.as_posix()}" - ) + raise FileNotFoundError(f"Could not find output at : {export_path.as_posix()}") @set_as_current def export_flix( @@ -868,18 +848,13 @@ def export_flix( raise ValueError("Export path must have .xml extension") original_file = self.project.path - import_parameters = ( - import_parameters - or 'waitForSource="1" multipleSetups="1" replaceSelection="0"' - ) + import_parameters = import_parameters or 'waitForSource="1" multipleSetups="1" replaceSelection="0"' # The project needs to be saved self.project.save() # save alpha mode and save format values - with utils.render_context( - alpha_mode, background_mode, None, format_opts, layer_selection - ): + with utils.render_context(alpha_mode, background_mode, None, format_opts, layer_selection): george.tv_clip_save_structure_flix( export_path, start, @@ -891,9 +866,7 @@ def export_flix( ) if not export_path.exists(): - raise FileNotFoundError( - f"Could not find output at : {export_path.as_posix()}" - ) + raise FileNotFoundError(f"Could not find output at : {export_path.as_posix()}") @property @set_as_current @@ -916,9 +889,7 @@ def mark_in(self, value: int | None) -> None: value = value frame = value - self.project.start_frame - george.tv_mark_in_set( - reference=george.MarkReference.CLIP, frame=frame, action=action - ) + george.tv_mark_in_set(reference=george.MarkReference.CLIP, frame=frame, action=action) @property @set_as_current @@ -959,9 +930,7 @@ def set_layer_color(self, layer_color: LayerColor) -> None: Args: layer_color: the layer color instance. """ - george.tv_layer_color_set_color( - self.id, layer_color.index, layer_color.color, layer_color.name - ) + george.tv_layer_color_set_color(self.id, layer_color.index, layer_color.color, layer_color.name) def get_layer_color( self, @@ -1003,9 +972,7 @@ def get_layer_color( @property def bookmarks(self) -> Iterator[int]: """Iterator over the clip bookmarks.""" - bookmarks_iter = utils.position_generator( - lambda pos: george.tv_bookmarks_enum(pos) - ) + bookmarks_iter = utils.position_generator(lambda pos: george.tv_bookmarks_enum(pos)) project_start_frame = self.project.start_frame return (frame + project_start_frame for frame in bookmarks_iter) @@ -1036,9 +1003,7 @@ def go_to_next_bookmark(self) -> None: @property def sounds(self) -> Iterator[ClipSound]: """Iterates through the clip's soundtracks.""" - sounds_data = utils.position_generator( - lambda pos: george.tv_sound_clip_info(self.id, pos) - ) + sounds_data = utils.position_generator(lambda pos: george.tv_sound_clip_info(self.id, pos)) for track_index, _ in enumerate(sounds_data): yield ClipSound(track_index, clip=self) diff --git a/pytvpaint/george/__init__.py b/pytvpaint/george/__init__.py index a032544..9b71c1a 100644 --- a/pytvpaint/george/__init__.py +++ b/pytvpaint/george/__init__.py @@ -7,6 +7,7 @@ from pytvpaint.george.grg_base import * # noqa: F403 from pytvpaint.george.grg_camera import * # noqa: F403 from pytvpaint.george.grg_clip import * # noqa: F403 +from pytvpaint.george.grg_guideline import * # noqa: F403 from pytvpaint.george.grg_layer import * # noqa: F403 from pytvpaint.george.grg_project import * # noqa: F403 from pytvpaint.george.grg_scene import * # noqa: F403 diff --git a/pytvpaint/george/client/parse.py b/pytvpaint/george/client/parse.py index bd727ba..e0f2480 100644 --- a/pytvpaint/george/client/parse.py +++ b/pytvpaint/george/client/parse.py @@ -84,7 +84,7 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: if issubclass(cast_type, Enum): value = value.strip().strip('"') - # Find all enum members that matches the value (lower case) + # Find all enum members that matche the value (lower case) matches = [m for m in cast_type if value.lower() == m.value.lower()] try: # If the unmodified value is in the enum, return that first @@ -94,22 +94,18 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: if matches: return cast(T, matches[0]) - # It didn't work, it can be the enum index + # if the above fails, then maybe the value is the index in the enum try: index = int(value) except ValueError: - raise ValueError( - f"{value} is not a valid Enum index since it can't be parsed as int" - ) + raise ValueError(f"{value} is not a valid Enum index since it can't be parsed as int") - # We get the enum member at that index + # get the enum member at the index enum_members = list(cast_type) if index < len(enum_members): return cast(T, enum_members[index]) - raise ValueError( - f"Enum index {index} is out of bounds (max {len(enum_members) - 1})" - ) + raise ValueError(f"Enum index {index} is out of bounds (max {len(enum_members) - 1})") if get_origin(cast_type) in (tuple, list): # Split by space and convert each member to the right type @@ -132,7 +128,7 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: return cast(T, cast_type(value)) -FieldTypes: TypeAlias = list[tuple[str, Any]] +FieldTypes: TypeAlias = list[tuple[tuple[str, str], Any]] def get_dataclass_fields( @@ -148,7 +144,7 @@ def get_dataclass_fields( """ type_hints = get_type_hints(datacls) return [ - (f.name, type_hints[f.name]) + ((f.name, f.metadata.get("alt_name", f.name)), type_hints[f.name]) for f in fields(datacls) if f.metadata.get("parsed", True) ] @@ -180,7 +176,11 @@ def tv_parse_dict( search_start = 0 for i, (field_name, field_type) in enumerate(with_fields): - current_key_pascal = camel_to_pascal(field_name) + # handle different naming schemes + alt_field_name = field_name + if isinstance(field_name, tuple): + field_name, alt_field_name = field_name + current_key_pascal = camel_to_pascal(alt_field_name) # Search for the key from the end search_text = input_text.lower() @@ -191,7 +191,10 @@ def tv_parse_dict( if i < (len(with_fields) - 1): # Search for the next key also from the end - next_key_pascal = camel_to_pascal(with_fields[i + 1][0]) + next_key = with_fields[i + 1][0] + if isinstance(next_key, tuple): + next_key = next_key[1] + next_key_pascal = camel_to_pascal(next_key) end = search_text.rfind(" " + next_key_pascal.lower(), search_start) else: end = len(input_text) @@ -272,6 +275,10 @@ def tv_parse_list( # Cast each token to a type and construct the dict tokens_dict: dict[str, Any] = {} for token, (field_name, field_type) in zip(tokens, with_fields): + # handle different naming schemes + if isinstance(field_name, tuple): + field_name, _ = field_name + token = tv_cast_to_type(token, field_type) tokens_dict[field_name] = token diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index ff7d8b0..2c48f7e 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -5,7 +5,7 @@ import contextlib import functools from collections.abc import Generator -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import Any, Callable, TypeVar, cast, overload @@ -344,9 +344,7 @@ def from_extension(cls, extension: str) -> SaveFormat: """Returns the correct tvpaint format value from a string extension.""" extension = extension.replace(".", "").upper() if not hasattr(SaveFormat, extension): - raise ValueError( - f"Could not find format ({extension}) in accepted formats ({SaveFormat})" - ) + raise ValueError(f"Could not find format ({extension}) in accepted formats ({SaveFormat})") return cast(SaveFormat, getattr(cls, extension.upper())) @classmethod @@ -383,6 +381,13 @@ class RGBColor: b: int +@dataclass(frozen=True) +class RGBAColor(RGBColor): + """RGBA color with 0-255 range values.""" + + a: int + + @dataclass(frozen=True) class HSLColor: """HSL color. Maximum values are (360, 100, 100) for h, s, l.""" @@ -573,10 +578,10 @@ class TVPPenBrush: power: int opacity: int dry: bool - aaliasing: bool + anti_aliasing: bool = field(metadata={"alt_name": "aaliasing"}) gradient: bool - csize: str - cpower: str + c_size: str + c_power: str @dataclass(frozen=True) @@ -722,21 +727,7 @@ def tv_menu_hide() -> None: send_cmd("tv_MenuHide") -def add_some_magic( - i_am_a_badass: bool = False, magic_number: int | None = None -) -> None: - """Don't use this function ! It just might change your life forever...""" - if not i_am_a_badass: - log.warning("Sorry, you're not enough of a badass for this function...") - - magic_number = magic_number or 14 - send_cmd("tv_MagicNumber", magic_number) - log.info("Totally worth it, right ?! ^^") - - -def tv_menu_show( - menu_element: MenuElement | None = None, *menu_options: Any, current: bool = False -) -> None: +def tv_menu_show(menu_element: MenuElement | None = None, *menu_options: Any, current: bool = False) -> None: """For the complete documentation, see: https://www.tvpaint.com/doc/tvpaint-animation-11/george-commands#tv_menushow.""" cmd_args: list[str] = [] @@ -749,6 +740,16 @@ def tv_menu_show( send_cmd("tv_MenuShow", *cmd_args, *menu_options) +def add_some_magic(i_am_a_badass: bool = False, magic_number: int | None = None) -> None: + """Don't use this function ! It just might change your life forever...""" + if not i_am_a_badass: + log.warning("Sorry, you're not enough of a badass for this function...") + + magic_number = magic_number if magic_number is not None else 14 + send_cmd("tv_MagicNumber", magic_number) + log.info("Totally worth it, right ?! ^^") + + def tv_request(msg: str, confirm_text: str = "Yes", cancel_text: str = "No") -> bool: """Open a confirmation prompt with a message. @@ -763,9 +764,7 @@ def tv_request(msg: str, confirm_text: str = "Yes", cancel_text: str = "No") -> return bool(int(send_cmd("tv_Request", msg, confirm_text, cancel_text))) -def tv_req_num( - value: int, min: int, max: int, title: str = "Enter Value" -) -> int | None: +def tv_req_num(value: int, min: int, max: int, title: str = "Enter Value") -> int | None: """Open a prompt to request an integer (within a range). Args: @@ -781,9 +780,7 @@ def tv_req_num( return None if res.lower() == "cancel" else int(res) -def tv_req_angle( - value: float, min: float, max: float, title: str = "Enter Value" -) -> float | None: +def tv_req_angle(value: float, min: float, max: float, title: str = "Enter Value") -> float | None: """Open a prompt to request an angle (in degree). Args: @@ -799,9 +796,7 @@ def tv_req_angle( return None if res.lower() == "cancel" else float(res) -def tv_req_float( - value: float, min: float, max: float, title: str = "Enter value" -) -> float | None: +def tv_req_float(value: float, min: float, max: float, title: str = "Enter value") -> float | None: """Open a prompt to request a float. Args: @@ -937,9 +932,7 @@ def tv_save_mode_get() -> tuple[SaveFormat, list[str]]: return save_format, res_split -def tv_save_mode_set( - save_format: SaveFormat, *format_options: str | int | float -) -> None: +def tv_save_mode_set(save_format: SaveFormat, *format_options: str | int | float) -> None: """Set the saving alpha mode.""" send_cmd("tv_SaveMode", save_format.value, *format_options) @@ -989,9 +982,7 @@ def tv_mark_out_get( return _tv_mark(MarkType.MARKOUT, reference) -def tv_mark_out_set( - reference: MarkReference, frame: int | None, action: MarkAction -) -> tuple[int, MarkAction]: +def tv_mark_out_set(reference: MarkReference, frame: int | None, action: MarkAction) -> tuple[int, MarkAction]: """Set markout of the project / clip.""" return _tv_mark(MarkType.MARKOUT, reference, frame, action) @@ -1086,9 +1077,7 @@ def _tv_set_ab_pen( res = send_cmd(f"tv_Set{pen.upper()}Pen", *args) fmt, r, g, b = res.split(" ") - color_type: type[RGBColor] | type[HSLColor] = ( - RGBColor if a is not None or fmt == "rgb" else HSLColor - ) + color_type: type[RGBColor] | type[HSLColor] = RGBColor if a is not None or fmt == "rgb" else HSLColor return color_type(int(r), int(g), int(b)) @@ -1272,3 +1261,16 @@ def tv_fast_line( ) -> None: """Draw a line (1 pixel size and not antialiased).""" send_cmd("tv_fastline", x1, y1, x2, y2, r, g, b, a) + + +def tv_pick_color() -> tuple[int, RGBAColor]: + """Pick a color from the UI using the mouse. + + Returns: + mouse_click: mouse click (-1: cancel, 0: left button mouse, 1: right button mouse) + color: the selected RGBA Color + """ + result = send_cmd("tv_PicColor") + mouse_click, r, g, b, a = result.split() + + return int(mouse_click), RGBAColor(*[int(c) for c in (r, g, b, a)]) diff --git a/pytvpaint/george/grg_camera.py b/pytvpaint/george/grg_camera.py index 752f34f..1195e24 100644 --- a/pytvpaint/george/grg_camera.py +++ b/pytvpaint/george/grg_camera.py @@ -13,7 +13,6 @@ validate_args_list, ) from pytvpaint.george.grg_base import FieldOrder, GrgErrorValue, is_tvp_version_below_12 -from pytvpaint.george.grg_project import tv_frame_rate_get @dataclass(frozen=True) diff --git a/pytvpaint/george/grg_clip.py b/pytvpaint/george/grg_clip.py index ea445c6..ca9dc4c 100644 --- a/pytvpaint/george/grg_clip.py +++ b/pytvpaint/george/grg_clip.py @@ -605,7 +605,14 @@ def tv_sound_clip_info(clip_id: int, track_index: int) -> TVPSound: def tv_sound_clip_new(sound_path: Path | str) -> None: - """Add a new soundtrack.""" + """Add a new soundtrack. + + Args: + sound_path: sound path + + Raises: + ValueError: if sound file not found + """ path = Path(sound_path) if not path.exists(): raise ValueError(f"Sound file not found at : {path.as_posix()}") diff --git a/pytvpaint/george/grg_guideline.py b/pytvpaint/george/grg_guideline.py new file mode 100644 index 0000000..478cebb --- /dev/null +++ b/pytvpaint/george/grg_guideline.py @@ -0,0 +1,697 @@ +"""Guideline related George functions.""" + +from __future__ import annotations + +from enum import Enum +from pathlib import Path +from dataclasses import dataclass, field +from typing import cast + +from pytvpaint.george.client import send_cmd +from pytvpaint.george.grg_base import GrgErrorValue, RGBAColor +from pytvpaint.george.client.parse import ( + DataclassInstance, + get_dataclass_fields, + args_dict_to_list, + tv_parse_dict, +) + + +class GuidelineType(Enum): + """All guideline types. + + Attributes: + IMAGE: + LINE: + SEGMENT: + CIRCLE: + ELLIPSE: + GRID: + MARKS: + FIELD_CHART: + ANIMATOR_FIELD: + SAFE_AREA: + VANISH_POINT_1: + VANISH_POINT_2: + VANISH_POINT_3: + """ + + IMAGE = "image" + LINE = "line" + SEGMENT = "segment" + CIRCLE = "circle" + ELLIPSE = "ellipse" + GRID = "grid" + MARKS = "marks" + FIELD_CHART = "fieldchart" + ANIMATOR_FIELD = "animatorfield" + SAFE_AREA = "safearea" + VANISH_POINT_1 = "vanishpoint1" + VANISH_POINT_2 = "vanishpoint2" + VANISH_POINT_3 = "vanishpoint3" + + +class FlipDirection(Enum): + """Filp Direction. + + Attributes: + X: + Y: + XY: + """ + + X = "x" + Y = "y" + XY = "xy" + NONE = "none" + + +class GuidelineAlphaMode(Enum): + """The alpha load mode. + + Attributes: + PREMULTIPLY: + NO_PREMULTIPLY: + """ + + PREMULTIPLY = "premultiply" + NO_PREMULTIPLY = "nopremultiply" + + +@dataclass(frozen=True) +class TVPGuidelineImage: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + path: Path + x: float + y: float + rotation: float + scale: float + flip: FlipDirection + + +@dataclass(frozen=True) +class TVPGuidelineLine: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + x: float + y: float + angle: float + + +@dataclass(frozen=True) +class TVPGuidelineSegment: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + x1: float + y1: float + x2: float + y2: float + + +@dataclass(frozen=True) +class TVPGuidelineCircle: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + x: float + y: float + radius: float + + +@dataclass(frozen=True) +class TVPGuidelineEllipse: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + x: float + y: float + radius_a: float + radius_b: float + + +@dataclass(frozen=True) +class TVPGuidelineGrid: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + x: float + y: float + width: float = field(metadata={"alt_name": "w"}) + height: float = field(metadata={"alt_name": "h"}) + + +@dataclass(frozen=True) +class TVPGuidelineMarks: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + count_x: float + count_y: float + + +@dataclass(frozen=True) +class TVPGuidelineSafeArea: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + sf_out: float = field(metadata={"alt_name": "out"}) + sf_in: float = field(metadata={"alt_name": "in"}) + + +@dataclass(frozen=True) +class TVPGuidelineVanishPoint1: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + x: float + y: float + ray: int + grid: bool + + +@dataclass(frozen=True) +class TVPGuidelineVanishPoint2: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + x1: float + y1: float + x2: float + y2: float + ray: int + + +@dataclass(frozen=True) +class TVPGuidelineVanishPoint3: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + x1: float + y1: float + x2: float + y2: float + x3: float + y3: float + ray: int + + +@dataclass(frozen=True) +class TVPGuideField: + """TVPaint project info values.""" + + position: int = field(metadata={"parsed": False}) + + +def tv_guideline_enum(position: int, guideline_type: GuidelineType) -> int: + """Check the existence of a guideline at the given position. + + Raises: + GeorgeError: if given an invalid scene id or clip position or elements have been removed + """ + return int( + send_cmd( + "tv_GuidelineEnum", + position, + guideline_type.value, + error_values=[-1, -2, GrgErrorValue.NONE], + ) + ) + + +def tv_guideline_remove_all(guideline_type: GuidelineType) -> None: + """Removes all guidelines of the given type.""" + send_cmd( + "tv_GuidelineRemove", + "all", + guideline_type.value, + error_values=[-1, -2], + ) + + +def tv_guideline_remove(position: int, guideline_type: GuidelineType) -> None: + """Removes the guidelines at the given position and type.""" + send_cmd( + "tv_GuidelineRemove", + guideline_type.value, + position, + error_values=[GrgErrorValue.NONE], + ) + + +def tv_guideline_name_get(position: int) -> str: + """Get the name of the guideline at the given position.""" + return str( + send_cmd( + "tv_GuidelineName", + position, + error_values=[GrgErrorValue.ERROR], + ) + ).strip() + + +def tv_guideline_name_set(position: int, name: str) -> None: + """Set the name of the guideline at the given position.""" + send_cmd( + "tv_GuidelineName", + position, + name, + error_values=[GrgErrorValue.ERROR], + ) + + +def tv_guideline_visibility_get(position: int, on_global: bool = False) -> bool: + """Get the visibility of the guideline at the given position.""" + return bool( + int( + send_cmd( + "tv_GuidelineVisible", + position if not on_global else "global", + error_values=[-1, -2], + ) + ) + ) + + +def tv_guideline_visibility_set(position: int, is_visible: bool, on_global: bool = False) -> None: + """Set the visibility of the guideline at the given position.""" + send_cmd( + "tv_GuidelineVisible", + position if not on_global else "global", + int(is_visible), + error_values=[-1, -2], + ) + + +def tv_guideline_visibility_set_all( + guideline_type: GuidelineType | None, is_visible: bool, apply_all: bool = False +) -> None: + """Set the visibility of the guideline of the given type or all of them regardless of type. + + Args: + guideline_type: the guideline type or None if `apply_all` is used. + is_visible: True to set as visible, False otherwise. + apply_all: True to apply to all guidelines regardless of type. + """ + if guideline_type is not None and not apply_all: + apply_to = guideline_type.value + else: + apply_to = "all" + + send_cmd( + "tv_GuidelineVisible", + apply_to, + int(is_visible), + error_values=[-1, -2], + ) + + +def tv_guideline_margin_get(position: int, on_global: bool = False) -> int: + """Get the margin of the guideline at the given position.""" + return int( + send_cmd( + "tv_GuidelineMarge", + position if not on_global else "global", + error_values=[-1, -2], + ) + ) + + +def tv_guideline_margin_set(position: int, margin: int, on_global: bool = False) -> None: + """Set the margin of the guideline at the given position.""" + send_cmd( + "tv_GuidelineMarge", + position if not on_global else "global", + margin, + error_values=[-1, -2], + ) + + +def tv_guideline_margin_set_all(guideline_type: GuidelineType | None, margin: int, apply_all: bool = False) -> None: + """Set the margin of the guideline of the given type or all of them regardless of type. + + Args: + guideline_type: the guideline type or None if `apply_all` is used. + margin: the margin to apply. + apply_all: True to apply to all guidelines regardless of type. + """ + if guideline_type is not None and not apply_all: + apply_to = guideline_type.value + else: + apply_to = "all" + + send_cmd( + "tv_GuidelineMarge", + apply_to, + margin, + error_values=[-1, -2], + ) + + +def tv_guideline_color_get(position: int, on_global: bool = False) -> RGBAColor: + """Get the color of the guideline at the given position.""" + result = send_cmd( + "tv_GuidelineColor", + position if not on_global else "global", + error_values=[-1, -2], + ) + r, g, b, a = result.split() + return RGBAColor(*[int(c) for c in (r, g, b, a)]) + + +def tv_guideline_color_set(position: int, color: RGBAColor, on_global: bool = False) -> None: + """Set the color of the guideline at the given position.""" + send_cmd( + "tv_GuidelineColor", + position if not on_global else "global", + color.r, + color.g, + color.b, + color.a, + error_values=[-1, -2], + ) + + +def tv_guideline_color_set_all(guideline_type: GuidelineType | None, color: RGBAColor, apply_all: bool = False) -> None: + """Set the color of the guideline of the given type or all of them regardless of type. + + Args: + guideline_type: the guideline type or None if `apply_all` is used. + color: the color to apply. + apply_all: True to apply to all guidelines regardless of type. + """ + if guideline_type is not None and not apply_all: + apply_to = guideline_type.value + else: + apply_to = "all" + + send_cmd( + "tv_GuidelineColor", + apply_to, + color.r, + color.g, + color.b, + color.a, + error_values=[-1, -2], + ) + + +def tv_guideline_snap_get(position: int, on_global: bool = False) -> bool: + """Get the snap status of the guideline at the given position.""" + return bool( + int( + send_cmd( + "tv_GuidelineSnap", + position if not on_global else "global", + error_values=[-1, -2], + ) + ) + ) + + +def tv_guideline_snap_set(position: int, snap: bool, on_global: bool = False) -> None: + """Set the guideline snap at the given position.""" + send_cmd( + "tv_GuidelineSnap", + position if not on_global else "global", + int(snap), + error_values=[-1, -2], + ) + + +def tv_guideline_snap_set_all(guideline_type: GuidelineType | None, snap: bool, apply_all: bool = False) -> None: + """Set the guideline snap for the given type or all of them regardless of type. + + Args: + guideline_type: the guideline type or None if `apply_all` is used. + snap: True to snap, False otherwise. + apply_all: True to apply to all guidelines regardless of type. + """ + if guideline_type is not None and not apply_all: + apply_to = guideline_type.value + else: + apply_to = "all" + + send_cmd( + "tv_GuidelineSnap", + apply_to, + int(snap), + error_values=[-1, -2], + ) + + +def tv_guideline_collapse_get(position: int) -> bool: + """Get the collapse status of the guideline at the given position.""" + return bool( + int( + send_cmd( + "tv_guidelineCollapse", + position, + error_values=[-1, -2], + ) + ) + ) + + +def tv_guideline_collapse_set(position: int, collapse: bool) -> None: + """Set the guideline collapse at the given position.""" + send_cmd( + "tv_guidelineCollapse", + position, + int(collapse), + error_values=[-1, -2], + ) + + +def tv_guideline_add_image( + img_path: Path | str | None = None, + x: float | None = None, + y: float | None = None, + rotation: float | None = None, + scale: float | None = None, + flip: FlipDirection | None = None, + alpha_mode: GuidelineAlphaMode | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "img_path": img_path, + "x": x, + "y": y, + "rotation": rotation, + "scale": scale, + "flip": flip, + "alphamode": alpha_mode, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "image", + *args, + error_values=[-1, -2], + ) + ) + + +def tv_guideline_modify_image_get(position: int) -> TVPGuidelineImage: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineImage) + guideline["position"] = position + return TVPGuidelineImage(**guideline) + + +def tv_guideline_modify_image_set( + position: int, + img_path: Path | str | None = None, + x: float | None = None, + y: float | None = None, + rotation: float | None = None, + scale: float | None = None, + flip: FlipDirection | None = None, +) -> TVPGuidelineImage: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "img_path": img_path, + "x": x, + "y": y, + "rotation": rotation, + "scale": scale, + "flip": flip, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + fields = get_dataclass_fields(cast(DataclassInstance, TVPGuidelineImage)) + guideline = tv_parse_dict(result, with_fields=fields) + guideline["position"] = position + return TVPGuidelineImage(**guideline) + + +def tv_guideline_modify_line_get(position: int) -> TVPGuidelineLine: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineLine) + guideline["position"] = position + return TVPGuidelineLine(**guideline) + + +def tv_guideline_modify_segment_get(position: int) -> TVPGuidelineSegment: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineSegment) + guideline["position"] = position + return TVPGuidelineSegment(**guideline) + + +def tv_guideline_modify_circle_get(position: int) -> TVPGuidelineCircle: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineCircle) + guideline["position"] = position + return TVPGuidelineCircle(**guideline) + + +def tv_guideline_modify_ellipse_get(position: int) -> TVPGuidelineEllipse: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineEllipse) + guideline["position"] = position + return TVPGuidelineEllipse(**guideline) + + +def tv_guideline_modify_grid_get(position: int) -> TVPGuidelineGrid: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineGrid) + guideline["position"] = position + return TVPGuidelineGrid(**guideline) + + +def tv_guideline_modify_marks_get(position: int) -> TVPGuidelineMarks: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineMarks) + guideline["position"] = position + return TVPGuidelineMarks(**guideline) + + +def tv_guideline_modify_safe_area_get(position: int) -> TVPGuidelineSafeArea: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineSafeArea) + guideline["position"] = position + return TVPGuidelineSafeArea(**guideline) + + +def tv_guideline_modify_vanish_point_1_get(position: int) -> TVPGuidelineVanishPoint1: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint1) + guideline["position"] = position + return TVPGuidelineVanishPoint1(**guideline) + + +def tv_guideline_modify_vanish_point_2_get(position: int) -> TVPGuidelineVanishPoint2: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint2) + guideline["position"] = position + return TVPGuidelineVanishPoint2(**guideline) + + +def tv_guideline_modify_vanish_point_3_get(position: int) -> TVPGuidelineVanishPoint3: + """Get info for the image guideline at the given position.""" + + result = send_cmd( + "tv_GuidelineModify", + position, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint3) + guideline["position"] = position + return TVPGuidelineVanishPoint3(**guideline) diff --git a/pytvpaint/george/grg_layer.py b/pytvpaint/george/grg_layer.py index 61b5928..eb45b8c 100644 --- a/pytvpaint/george/grg_layer.py +++ b/pytvpaint/george/grg_layer.py @@ -1102,14 +1102,14 @@ def tv_layer_color_visible(color_index: int) -> bool: Raises: NoObjectWithIdError: if given an invalid layer id """ - return bool( + return bool(int( send_cmd( "tv_LayerColor", LayerColorAction.VISIBLE.value, color_index, error_values=[-1], ) - ) + )) @try_cmd( diff --git a/pytvpaint/george/grg_project.py b/pytvpaint/george/grg_project.py index e750fba..bead6a8 100644 --- a/pytvpaint/george/grg_project.py +++ b/pytvpaint/george/grg_project.py @@ -70,9 +70,7 @@ def tv_project_info(project_id: str) -> TVPProject: if not is_tvp_version_below_12(): # values of field_order have been removed in versions > 12 so for now we provide it ourselves fields_keys: list[str] = list(dict(fields).keys()) - field_order_index, start_frame_index = fields_keys.index( - "field_order" - ), fields_keys.index("start_frame") + field_order_index, start_frame_index = fields_keys.index("field_order"), fields_keys.index("start_frame") fields[field_order_index], fields[start_frame_index] = ( fields[start_frame_index], fields[field_order_index], @@ -84,9 +82,7 @@ def tv_project_info(project_id: str) -> TVPProject: return TVPProject(**project) -def tv_background_get() -> ( - tuple[BackgroundMode, tuple[RGBColor, RGBColor] | RGBColor | None] -): +def tv_background_get() -> tuple[BackgroundMode, tuple[RGBColor, RGBColor] | RGBColor | None]: """Get the background mode of the project, and the color(s) if in `color` or `check` mode. Returns: @@ -317,9 +313,7 @@ def tv_frame_rate_get() -> tuple[float, float]: return project_fps, playback_fps -def tv_frame_rate_set( - frame_rate: float, time_stretch: bool = False, preview: bool = False -) -> None: +def tv_frame_rate_set(frame_rate: float, time_stretch: bool = False, preview: bool = False) -> None: """Get the framerate of the current project.""" args: list[Any] = [] if time_stretch: @@ -379,23 +373,17 @@ def tv_save_palette(palette_path: Path | str) -> None: if not palette_path.parent.exists(): parent_path = palette_path.parent.as_posix() - raise NotADirectoryError( - f"Can't save palette because parent folder doesn't exist: {parent_path}" - ) + raise NotADirectoryError(f"Can't save palette because parent folder doesn't exist: {parent_path}") send_cmd("tv_SavePalette", palette_path.as_posix()) -def tv_project_save_video_dependencies( - project_id: str, on_save: bool = True, now: bool = False -) -> int: +def tv_project_save_video_dependencies(project_id: str, on_save: bool = True, now: bool = False) -> int: """Saves current project video dependencies.""" args: list[Any] = [project_id] if not now: args.append(int(on_save)) - return int( - send_cmd("tv_ProjectSaveVideoDependencies", *args, error_values=[-1, -2]) - ) + return int(send_cmd("tv_ProjectSaveVideoDependencies", *args, error_values=[-1, -2])) def tv_project_save_audio_dependencies(project_id: str, on_save: bool = True) -> int: @@ -412,9 +400,7 @@ def tv_project_save_audio_dependencies(project_id: str, on_save: bool = True) -> def tv_sound_project_info(project_id: str, track_index: int) -> TVPSound: """Get information about a project soundtrack.""" - res = send_cmd( - "tv_SoundProjectInfo", project_id, track_index, error_values=[-1, -2, -3] - ) + res = send_cmd("tv_SoundProjectInfo", project_id, track_index, error_values=[-1, -2, -3]) res_parse = tv_parse_list(res, with_fields=TVPSound) return TVPSound(**res_parse) diff --git a/pytvpaint/guideline.py b/pytvpaint/guideline.py new file mode 100644 index 0000000..5cecc24 --- /dev/null +++ b/pytvpaint/guideline.py @@ -0,0 +1,455 @@ +"""Guideline related objects and classes.""" + +from __future__ import annotations + +from pathlib import Path +from typing import TypeVar, Generic +from typing import TYPE_CHECKING + +from pytvpaint import george +from pytvpaint.utils import ( + Removable, + refreshed_property, +) + +if TYPE_CHECKING: + from pytvpaint.project import Project + +GuidelineDT = TypeVar("GuidelineDT") +GuidelineT = TypeVar("GuidelineT", bound=george.GuidelineType) + + +class Guideline(Removable, Generic[GuidelineDT, GuidelineT]): + """A Guideline is an image or shape used as guideline for artists.""" + + TYPE: GuidelineT = None + + def __init__( + self, + position: int, + project: Project, + data: GuidelineDT | None = None, + ) -> None: + super().__init__() + self._position: int = position + self._project: Project = project + self._data = data + + def refresh(self) -> None: + """Refreshes the guideline data.""" + super().refresh() + + def __repr__(self) -> str: + """String representation of the camera point.""" + return f"{self.__class__.__name__}({self.name})" + + def __eq__(self, other: object) -> bool: + """Two camera points are equal if their internal data is the same.""" + if not isinstance(other, self.__class__): + return NotImplemented + + self.refresh() + other.refresh() + return self.position == other.position and self._data == other._data + + @property + def position(self) -> int: + """The position of the guideline.""" + return self._position + + @property + def data(self) -> GuidelineDT: + """Returns the raw data of the guideline.""" + return self._data + + @property + def project(self) -> Project: + """The project instance the guideline belongs to.""" + return self._project + + @property + def name(self) -> str: + return george.tv_guideline_name_get(self.position) + + @name.setter + def name(self, value: str) -> None: + george.tv_guideline_name_set(self.position, value) + + @property + def is_visible(self) -> bool: + return george.tv_guideline_visibility_get(self.position) + + @is_visible.setter + def is_visible(self, value: bool) -> None: + george.tv_guideline_visibility_set(self.position, value) + + @property + def margin(self) -> int: + return george.tv_guideline_margin_get(self.position) + + @margin.setter + def margin(self, value: int) -> None: + george.tv_guideline_margin_set(self.position, value) + + @property + def color(self) -> george.RGBAColor: + return george.tv_guideline_color_get(self.position) + + @color.setter + def color(self, value: george.RGBAColor) -> None: + george.tv_guideline_color_set(self.position, value) + + @property + def snap(self) -> bool: + return george.tv_guideline_snap_get(self.position) + + @snap.setter + def snap(self, value: bool) -> None: + george.tv_guideline_snap_set(self.position, value) + + @property + def collapse(self) -> bool: + return george.tv_guideline_collapse_get(self.position) + + @collapse.setter + def collapse(self, value: bool) -> None: + george.tv_guideline_collapse_set(self.position, value) + + def remove(self) -> None: + """Remove the guideline. + + Warning: + the guideline instance won't be usable after removal + """ + if self.TYPE: + george.tv_guideline_remove(self.position, self.TYPE) + self.mark_removed() + + +class GuidelineImage(Guideline[george.TVPGuidelineImage, george.GuidelineType]): + + TYPE = george.GuidelineType.IMAGE + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineImage | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_image_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_image_get(self._position) + + @refreshed_property + def path(self) -> Path: + """The x coordinate of the point.""" + return self._data.path + + @path.setter + def path(self, value: Path | str) -> None: + george.tv_guideline_modify_image_set(self.position, img_path=value) + + @refreshed_property + def x(self) -> float: + """The x coordinate of the point.""" + return self._data.x + + @x.setter + def x(self, value: float) -> None: + george.tv_guideline_modify_image_set(self.position, x=value) + + @refreshed_property + def y(self) -> float: + """The y coordinate of the point.""" + return self._data.y + + @y.setter + def y(self, value: float) -> None: + george.tv_guideline_modify_image_set(self.position, y=value) + + @refreshed_property + def rotation(self) -> float: + """The angle of the camera at that point.""" + return self._data.rotation + + @rotation.setter + def rotation(self, value: float) -> None: + george.tv_guideline_modify_image_set(self.position, rotation=value) + + @refreshed_property + def scale(self) -> float: + """The scale of the camera at that point.""" + return self._data.scale + + @scale.setter + def scale(self, value: float) -> None: + george.tv_guideline_modify_image_set(self.position, scale=value) + + @refreshed_property + def flip(self) -> george.FlipDirection: + """The scale of the camera at that point.""" + return self._data.flip + + @flip.setter + def flip(self, value: george.FlipDirection) -> None: + george.tv_guideline_modify_image_set(self.position, flip=value) + + @classmethod + def new( + cls, + project: Project, + img_path: Path | str | None = None, + x: float | None = None, + y: float | None = None, + rotation: float | None = None, + scale: float | None = None, + flip: george.FlipDirection | None = None, + alpha_mode: george.GuidelineAlphaMode | None = None, + ) -> GuidelineImage: + """Create a new point and add it to the camera path at that index.""" + project.make_current() + + position = george.tv_guideline_add_image(img_path, x, y, rotation, scale, flip, alpha_mode) + return cls(position, project) + + +class GuidelineLine(Guideline[george.TVPGuidelineLine, george.GuidelineType]): + + TYPE = george.GuidelineType.LINE + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineLine | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_line_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_line_get(self._position) + + +class GuidelineSegment(Guideline[george.TVPGuidelineSegment, george.GuidelineType]): + + TYPE = george.GuidelineType.SEGMENT + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineSegment | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_segment_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_segment_get(self._position) + + +class GuidelineCircle(Guideline[george.TVPGuidelineCircle, george.GuidelineType]): + + TYPE = george.GuidelineType.CIRCLE + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineCircle | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_circle_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_circle_get(self._position) + + +class GuidelineEllipse(Guideline[george.TVPGuidelineEllipse, george.GuidelineType]): + + TYPE = george.GuidelineType.ELLIPSE + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineEllipse | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_ellipse_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_ellipse_get(self._position) + + +class GuidelineGrid(Guideline[george.TVPGuidelineGrid, george.GuidelineType]): + + TYPE = george.GuidelineType.GRID + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineGrid | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_grid_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_grid_get(self._position) + + +class GuidelineMarks(Guideline[george.TVPGuidelineMarks, george.GuidelineType]): + + TYPE = george.GuidelineType.MARKS + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineMarks | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_marks_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_marks_get(self._position) + + +class GuidelineFieldChart(Guideline[george.TVPGuideField, george.GuidelineType]): + + TYPE = george.GuidelineType.FIELD_CHART + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuideField | None = None, + ) -> None: + super().__init__(position, project, data) + + +class GuidelineAnimatorField(Guideline[george.TVPGuideField, george.GuidelineType]): + + TYPE = george.GuidelineType.ANIMATOR_FIELD + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuideField | None = None, + ) -> None: + super().__init__(position, project, data) + + +class GuidelineSafeArea(Guideline[george.TVPGuidelineSafeArea, george.GuidelineType]): + + TYPE = george.GuidelineType.SAFE_AREA + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineSafeArea | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_safe_area_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_safe_area_get(self._position) + + +class GuidelineVanishPoint1(Guideline[george.TVPGuidelineVanishPoint1, george.GuidelineType]): + + TYPE = george.GuidelineType.VANISH_POINT_1 + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineVanishPoint1 | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_vanish_point_1_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_vanish_point_1_get(self._position) + + +class GuidelineVanishPoint2(Guideline[george.TVPGuidelineVanishPoint2, george.GuidelineType]): + + TYPE = george.GuidelineType.VANISH_POINT_2 + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineVanishPoint2 | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_vanish_point_2_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_vanish_point_2_get(self._position) + + +class GuidelineVanishPoint3(Guideline[george.TVPGuidelineVanishPoint3, george.GuidelineType]): + + TYPE = george.GuidelineType.VANISH_POINT_3 + + def __init__( + self, + position: int, + project: Project, + data: george.TVPGuidelineVanishPoint3 | None = None, + ) -> None: + super().__init__(position, project) + self._data = data or george.tv_guideline_modify_vanish_point_3_get(self._position) + + def refresh(self) -> None: + """Refreshes the camera point data.""" + super().refresh() + if not self.refresh_on_call and self._data: + return + self._data = george.tv_guideline_modify_vanish_point_3_get(self._position) diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index 6c551f2..133efd7 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -101,9 +101,7 @@ def end(self) -> int: @end.setter def end(self, value: int) -> None: if value < self.start: - raise ValueError( - f"End must be equal to or superior to instance start ({self.start})" - ) + raise ValueError(f"End must be equal to or superior to instance start ({self.start})") new_length = (value - self.start) + 1 self.length = new_length @@ -121,9 +119,7 @@ def split(self, at_frame: int) -> LayerInstance: LayerInstance: the new layer instance """ if at_frame > self.end: - raise ValueError( - f"`at_frame` must be in range of the instance's start-end ({self.start}-{self.end})" - ) + raise ValueError(f"`at_frame` must be in range of the instance's start-end ({self.start}-{self.end})") self.layer.make_current() real_frame = at_frame - self.layer.project.start_frame @@ -131,9 +127,7 @@ def split(self, at_frame: int) -> LayerInstance: return LayerInstance(self.layer, at_frame) - def duplicate( - self, direction: george.InsertDirection = george.InsertDirection.AFTER - ) -> None: + def duplicate(self, direction: george.InsertDirection = george.InsertDirection.AFTER) -> None: """Duplicate the instance and insert it in the given direction.""" self.layer.make_current() @@ -146,9 +140,7 @@ def duplicate( with utils.restore_current_frame(self.layer.clip, move_frame): self.copy() - at_frame = ( - self.end if direction == george.InsertDirection.AFTER else self.start - ) + at_frame = self.end if direction == george.InsertDirection.AFTER else self.start self.paste(at_frame=at_frame) def cut(self) -> None: @@ -271,8 +263,9 @@ def name(self) -> str: @name.setter def name(self, value: str) -> None: """Set the name of the color.""" - clip_layer_color_names = (color.name for color in self.clip.layer_colors) + clip_layer_color_names = (color.name for color in self.clip.layer_colors if color != self) value = utils.get_unique_name(clip_layer_color_names, value) + george.tv_layer_color_set_color(self.clip.id, self.index, self.color, value) @refreshed_property @@ -447,9 +440,7 @@ def folder(self) -> None: Raises: NotImplementedError: as there is currently no way to get the parent folder from a Layer. """ - raise NotImplementedError( - "There is currently no way to get the parent folder from a Layer." - ) + raise NotImplementedError("There is currently no way to get the parent folder from a Layer.") @folder.setter @george.min_version_compatible(min_version="12") @@ -472,7 +463,9 @@ def name(self, value: str) -> None: """ if value == self.name: return - value = utils.get_unique_name(self.clip.layer_names, value) + + layer_names = (layer.name for layer in self.clip.layers if layer != self) + value = utils.get_unique_name(layer_names, value) george.tv_layer_rename(self.id, value) @refreshed_property @@ -994,9 +987,7 @@ def render_frame( george.tv_save_image(export_path) if not export_path.exists(): - raise FileNotFoundError( - f"Could not find rendered image ({frame}) at : {export_path.as_posix()}" - ) + raise FileNotFoundError(f"Could not find rendered image ({frame}) at : {export_path.as_posix()}") return export_path @@ -1033,31 +1024,23 @@ def render_instances( ) if start < self.start or end > self.end: - raise ValueError( - f"Render ({start}-{end}) not in clip range ({(self.start, self.end)})" - ) + raise ValueError(f"Render ({start}-{end}) not in clip range ({(self.start, self.end)})") if not is_image: - raise ValueError( - f"Video formats ({file_sequence.extension()}) are not supported for instance rendering !" - ) + raise ValueError(f"Video formats ({file_sequence.extension()}) are not supported for instance rendering !") # render to output frames = [] for layer_instance in self.instances: cur_frame = layer_instance.start instance_output = Path(file_sequence.frame(cur_frame)) - self.render_frame( - instance_output, cur_frame, alpha_mode, background_mode, format_opts - ) + self.render_frame(instance_output, cur_frame, alpha_mode, background_mode, format_opts) frames.append(str(cur_frame)) file_sequence.setFrameSet(FrameSet(",".join(frames))) return file_sequence @set_as_current - def load_image( - self, image_path: str | Path, frame: int | None = None, stretch: bool = False - ) -> None: + def load_image(self, image_path: str | Path, frame: int | None = None, stretch: bool = False) -> None: """Load an image in the current layer at a given frame. Args: @@ -1107,9 +1090,7 @@ def add_mark(self, frame: int, color: LayerColor) -> None: TypeError: if the layer is not an animation layer """ if not self.is_anim_layer: - raise TypeError( - f"Can't add a mark because this is not an animation layer ({self})" - ) + raise TypeError(f"Can't add a mark because this is not an animation layer ({self})") frame = frame - self.project.start_frame george.tv_layer_mark_set(self.id, frame, color.index) @@ -1161,9 +1142,7 @@ def select_frames(self, start: int, end: int) -> None: end: the selected end frame """ if not self.is_anim_layer: - log.warning( - "Selection may display weird behaviour when applied to a non animation layer" - ) + log.warning("Selection may display weird behaviour when applied to a non animation layer") frame_count = (end - start) + 1 george.tv_layer_select(start - self.clip.start, frame_count) @@ -1443,9 +1422,7 @@ def new( sources = sources or [] name = utils.get_unique_name(clip.layer_names, name) - layer_id = george.tv_ctg_layer_create( - name, sources=[layer.name for layer in sources] - ) + layer_id = george.tv_ctg_layer_create(name, sources=[layer.name for layer in sources]) layer = cls(layer_id=layer_id, clip=clip) if color: @@ -1512,9 +1489,7 @@ def add_sources(self, sources: list[Layer]) -> None: def remove_sources(self, sources: list[Layer]) -> None: """Remove a list of Layers from the sources for this CTG layer.""" - george.tv_ctg_source_remove( - self.id, [layer.id for layer in sources if self in layer.sourced_ctg_layers] - ) + george.tv_ctg_source_remove(self.id, [layer.id for layer in sources if self in layer.sourced_ctg_layers]) @set_as_current def load_structure(self) -> None: diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 89d3499..41833bf 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -12,6 +12,7 @@ from pytvpaint import george, utils from pytvpaint.george.exceptions import GeorgeError from pytvpaint.sound import ProjectSound +from pytvpaint import guideline from pytvpaint.utils import ( Refreshable, Renderable, @@ -237,9 +238,7 @@ def current_frame(self) -> int: def current_frame(self, value: int) -> None: # when setting the current frame, if it is outside the current clip's range, TVP will switch to the clip but not # the required frame. So we need to set it twice, one to switch the clip, and once again to set the frame - set_twice = not ( - self.current_clip.timeline_start <= value <= self.current_clip.timeline_end - ) + set_twice = not (self.current_clip.timeline_start <= value <= self.current_clip.timeline_end) real_frame = value - self.start_frame george.tv_project_current_frame_set(real_frame) @@ -336,8 +335,9 @@ def get_project( Project | None: the searched element or None if search was unsuccessful """ - return utils.get_tvp_element(Project.open_projects(), by_id=by_id, by_name=by_name, - by_regex=by_regex, by_path=by_path) + return utils.get_tvp_element( + Project.open_projects(), by_id=by_id, by_name=by_name, by_regex=by_regex, by_path=by_path + ) @staticmethod def current_scene_ids() -> Iterator[int]: @@ -407,9 +407,7 @@ def clips(self) -> Iterator[Clip]: def clip_names(self) -> Iterator[str]: """Optimized way to get the clip names. Useful for `get_unique_name`.""" for scene_id in self.current_scene_ids(): - clip_ids = utils.position_generator( - lambda pos: george.tv_clip_enum_id(scene_id, pos) - ) + clip_ids = utils.position_generator(lambda pos: george.tv_clip_enum_id(scene_id, pos)) for clip_id in clip_ids: yield george.tv_clip_name_get(clip_id) @@ -449,9 +447,7 @@ def add_clip(self, clip_name: str, scene: Scene | None = None) -> Clip: @property def sounds(self) -> Iterator[ProjectSound]: """Iterator over the project sounds.""" - sounds_data_iter = utils.position_generator( - lambda pos: george.tv_sound_project_info(self.id, pos) - ) + sounds_data_iter = utils.position_generator(lambda pos: george.tv_sound_project_info(self.id, pos)) for track_index, _ in enumerate(sounds_data_iter): yield ProjectSound(track_index, project=self) @@ -460,6 +456,50 @@ def add_sound(self, sound_path: Path | str) -> ProjectSound: """Add a new sound clip to the project.""" return ProjectSound.new(sound_path, parent=self) + @set_as_current + def guidelines(self, guideline_type: george.GuidelineType | None = None) -> Iterator[guideline.Guideline]: + """Iterator for the `Guideline` objects of the project.""" + guideline_classes = [ + guideline.GuidelineImage, + guideline.GuidelineLine, + guideline.GuidelineSegment, + guideline.GuidelineCircle, + guideline.GuidelineEllipse, + guideline.GuidelineGrid, + guideline.GuidelineMarks, + guideline.GuidelineSafeArea, + guideline.GuidelineFieldChart, + guideline.GuidelineAnimatorField, + guideline.GuidelineVanishPoint1, + guideline.GuidelineVanishPoint2, + guideline.GuidelineVanishPoint3, + ] + guideline_classes = {c.TYPE: c for c in guideline_classes} + for g_type in george.GuidelineType: + if guideline_type and guideline_type != g_type: + continue + if not guideline_classes.get(g_type): + continue + + guideline_class = guideline_classes[g_type] + + positions = utils.position_generator(lambda pos: george.tv_guideline_enum(pos, g_type)) + for position in positions: + yield guideline_class(position, project=self) + + def add_guideline_image( + self, + img_path: Path | str | None = None, + x: float | None = None, + y: float | None = None, + rotation: float | None = None, + scale: float | None = None, + flip: george.FlipDirection | None = None, + alpha_mode: george.GuidelineAlphaMode | None = None, + ) -> guideline.GuidelineImage: + """Add a new image guideline to the project.""" + return guideline.GuidelineImage.new(self, img_path, x, y, rotation, scale, flip, alpha_mode) + def _validate_range(self, start: int, end: int) -> None: project_start_frame = self.start_frame project_end_frame = self.end_frame @@ -471,9 +511,7 @@ def _validate_range(self, start: int, end: int) -> None: max(project_mark_out or project_end_frame, project_end_frame), ) if start < proj_full_range[0] or end > proj_full_range[1]: - raise ValueError( - f"Range ({start}-{end}) outside of project bounds ({proj_full_range})" - ) + raise ValueError(f"Range ({start}-{end}) outside of project bounds ({proj_full_range})") def _get_real_range(self, start: int, end: int) -> tuple[int, int]: project_start_frame = self.start_frame @@ -583,9 +621,7 @@ def open_projects(cls) -> Iterator[Project]: @set_as_current def mark_in(self) -> int | None: """Get the project mark in or None if no mark in set.""" - frame, mark_action = george.tv_mark_in_get( - reference=george.MarkReference.PROJECT - ) + frame, mark_action = george.tv_mark_in_get(reference=george.MarkReference.PROJECT) if mark_action == george.MarkAction.CLEAR: return None return frame + self.start_frame @@ -601,17 +637,13 @@ def mark_in(self, value: int | None) -> None: value = value frame = value - self.start_frame - george.tv_mark_in_set( - reference=george.MarkReference.PROJECT, frame=frame, action=action - ) + george.tv_mark_in_set(reference=george.MarkReference.PROJECT, frame=frame, action=action) @property @set_as_current def mark_out(self) -> int | None: """Get the project mark out or None if no mark out set.""" - frame, mark_action = george.tv_mark_out_get( - reference=george.MarkReference.PROJECT - ) + frame, mark_action = george.tv_mark_out_get(reference=george.MarkReference.PROJECT) if mark_action == george.MarkAction.CLEAR: return None return frame diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index 51fcfb0..215a2b2 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -150,9 +150,7 @@ def _render( origin_start = int(start) start, end = self._get_real_range(start, end) if not is_image and start == end: - raise ValueError( - "TVPaint will not render a movie that contains a single frame" - ) + raise ValueError("TVPaint will not render a movie that contains a single frame") # get first frame, tvp doesn't understand vfx padding `#` if is_image or file_sequence.padding(): @@ -161,14 +159,10 @@ def _render( first_frame = Path(str(output_path)) first_frame.parent.mkdir(exist_ok=True, parents=True) - save_format = george.SaveFormat.from_extension( - file_sequence.extension().lower() - ) + save_format = george.SaveFormat.from_extension(file_sequence.extension().lower()) # render to output - with render_context( - alpha_mode, background_mode, save_format, format_opts, layer_selection - ): + with render_context(alpha_mode, background_mode, save_format, format_opts, layer_selection): if start == end: with restore_current_frame(self, origin_start): george.tv_save_display(first_frame) @@ -195,14 +189,11 @@ def _render( # not all frames found 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}" + f"Not all frames found, missing frames ({missing_frames}) " f"in sequence : {output_path}" ) else: if not first_frame.exists(): - raise FileNotFoundError( - f"Could not find output at : {first_frame.as_posix()}" - ) + raise FileNotFoundError(f"Could not find output at : {first_frame.as_posix()}") def get_unique_name(names: Iterable[str], stub: str) -> str: @@ -260,7 +251,7 @@ def position_generator( stop_when (Type[GeorgeError], optional): exception at which we stop. Defaults to GeorgeError. Yields: - Iterator[T]: an generator of the resulting values + Iterator[T]: a generator of the resulting values """ pos = 0 @@ -385,9 +376,7 @@ def current_frame(self, value: int) -> None: ... @contextlib.contextmanager -def restore_current_frame( - tvp_element: HasCurrentFrame, frame: int -) -> Generator[None, None, None]: +def restore_current_frame(tvp_element: HasCurrentFrame, frame: int) -> Generator[None, None, None]: """Context that temporarily changes the current frame to the one provided and restores it when done. Args: @@ -510,12 +499,8 @@ def handle_output_range( range_is_seq = start is not None and end is not None and start != end range_is_single_image = start is not None and end is not None 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) - ) + 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 if start is None: From 7053d1725f806169572489660419d064a8ce8010 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Wed, 28 Jan 2026 17:22:46 +0100 Subject: [PATCH 08/26] UPDATE tvp parsing and misc bugs * ADD/Improve Guidelines support * Clean code and documentation * ADD new feature to parse non escaped text from tvpaint --- pytvpaint/camera.py | 17 +- pytvpaint/clip.py | 25 +- pytvpaint/george/client/__init__.py | 18 +- pytvpaint/george/client/parse.py | 515 +++++++++++++++------- pytvpaint/george/grg_base.py | 6 +- pytvpaint/guideline.py | 14 +- pytvpaint/layer.py | 12 +- pytvpaint/project.py | 7 +- pytvpaint/utils.py | 4 +- tests/conftest.py | 18 +- tests/george/client/test_parse.py | 637 +++++++++++++++++++++++++++- tests/test_clip.py | 2 +- 12 files changed, 1041 insertions(+), 234 deletions(-) diff --git a/pytvpaint/camera.py b/pytvpaint/camera.py index f6695ff..9a9e6a5 100644 --- a/pytvpaint/camera.py +++ b/pytvpaint/camera.py @@ -74,17 +74,22 @@ def height(self, value: int) -> None: @refreshed_property @set_as_current @george.deprecated_warning( - msg="DEPRECATED: Property `Camera.fps` is not recommended for use in TVP 12, use Project.fps instead. " + msg="Property `Camera.fps` is not recommended for use in TVP 12, use Project.fps instead. " "For now, in TVP 12, it always returns 1.0 but will be removed in future versions." ) def fps(self) -> float: - """The framerate of the camera.""" + """The framerate of the camera. + + Warning: + DEPRECATED: Property `Camera.fps` is not recommended for use in TVP 12, use Project.fps instead. + For now, in TVP 12, it always returns 1.0 but will be removed in future versions. + """ return self._data.frame_rate @fps.setter @set_as_current @george.deprecated_warning( - msg="DEPRECATED: Property `Camera.fps` is not recommended for use in TVP 12, use Project.fps instead. " + msg="Property `Camera.fps` is not recommended for use in TVP 12, use Project.fps instead. " "For now, in TVP 12, it always sets 1.0 but will be removed in future versions." ) def fps(self, value: float) -> None: @@ -115,15 +120,15 @@ def pixel_aspect_ratio(self, value: float) -> None: @refreshed_property @set_as_current @george.deprecated_warning( - msg="DEPRECATED: Property `Camera.anti_aliasing` not longer exists in TVP 12, " + msg="Property `Camera.anti_aliasing` not longer exists in TVP 12, " "for now, in TVP 12, it always returns 1 but will be removed in future versions." ) def anti_aliasing(self) -> int: """The antialiasing value of the camera. Warning: - DEPRECATED: This property has been removed from in TVP 12 and for now always returns 1 but will be removed - in future versions. + DEPRECATED: Property `Camera.anti_aliasing` not longer exists in TVP 12. + For now, in TVP 12, it always returns 1 but will be removed in future versions. """ return self._data.anti_aliasing diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 4f1c253..9835858 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -10,9 +10,10 @@ from fileseq.filesequence import FileSequence from pytvpaint import george, utils +from pytvpaint.george.client import parse +from pytvpaint.sound import ClipSound from pytvpaint.camera import Camera from pytvpaint.layer import CameraLayer, CTGLayer, Layer, LayerColor, LayerFolder -from pytvpaint.sound import ClipSound from pytvpaint.utils import ( Removable, Renderable, @@ -247,7 +248,7 @@ def color_index(self, value: int) -> None: @property def action_text(self) -> str: """Get the action text of the clip.""" - return george.tv_clip_action_get(self.id) + return parse.unescape_everything_safely(george.tv_clip_action_get(self.id)) @action_text.setter def action_text(self, value: str) -> None: @@ -257,7 +258,7 @@ def action_text(self, value: str) -> None: @property def dialog_text(self) -> str: """Get the dialog text of the clip.""" - return george.tv_clip_dialog_get(self.id) + return parse.unescape_everything_safely(george.tv_clip_dialog_get(self.id)) @dialog_text.setter def dialog_text(self, value: str) -> None: @@ -267,7 +268,7 @@ def dialog_text(self, value: str) -> None: @property def note_text(self) -> str: """Get the note text of the clip.""" - return george.tv_clip_note_get(self.id) + return parse.unescape_everything_safely(george.tv_clip_note_get(self.id)) @note_text.setter def note_text(self, value: str) -> None: @@ -345,7 +346,7 @@ def get_layers( ignore_types: list of layer types to ignore, default is None, meaning all. Note: - This function is mainly here for TVPaint 12 and above, when using previous versions, prefer Clip.layers. + Since TVP12 have introduced multiple layer types, this function is will replace Clip.layers. """ for layer_id in self.layer_ids: layer_data = george.tv_layer_info(layer_id) @@ -367,7 +368,17 @@ def get_layers( yield layer_class(layer_id, clip=self, data=layer_data) @property + @george.deprecated_warning(msg="use `Clip.get_layers()` instead.") def layers(self) -> Iterator[Layer]: + """Iterator over the clip's animation layers, ignores all Folder, Camera and CTG layers. + + Warning: + DEPRECATED: use `Clip.get_layers()` instead. + """ + yield from self.get_layers(ignore_types=(LayerFolder, CameraLayer, CTGLayer)) + + @property + def anim_layers(self) -> Iterator[Layer]: """Iterator over the clip's animation layers, ignores all Folder, Camera and CTG layers.""" yield from self.get_layers(ignore_types=(LayerFolder, CameraLayer, CTGLayer)) @@ -479,12 +490,12 @@ def add_layer_folder(self, folder_name: str) -> LayerFolder: @property def selected_layers(self) -> Iterator[Layer]: """Iterator over the selected layers.""" - yield from (layer for layer in self.layers if layer.is_selected) + yield from (layer for layer in self.get_layers() if layer.is_selected) @property def visible_layers(self) -> Iterator[Layer]: """Iterator over the visible layers.""" - yield from (layer for layer in self.layers if layer.is_visible) + yield from (layer for layer in self.get_layers() if layer.is_visible) @set_as_current @george.undoable diff --git a/pytvpaint/george/client/__init__.py b/pytvpaint/george/client/__init__.py index a18f5ca..2ca822c 100644 --- a/pytvpaint/george/client/__init__.py +++ b/pytvpaint/george/client/__init__.py @@ -5,10 +5,10 @@ from __future__ import annotations -import contextlib -import functools import os import re +import functools +import contextlib from pathlib import Path from time import sleep, time from typing import Any, Callable, TypeVar, cast @@ -19,9 +19,7 @@ from pytvpaint.george.exceptions import GeorgeError -def _connect_client( - host: str = "ws://localhost", port: int = 3000, timeout: int = 60 -) -> JSONRPCClient: +def _connect_client(host: str = "ws://localhost", port: int = 3000, timeout: int = 60) -> JSONRPCClient: host = os.getenv("PYTVPAINT_WS_HOST", host) port = int(os.getenv("PYTVPAINT_WS_PORT", port)) startup_connect = bool(int(os.getenv("PYTVPAINT_WS_STARTUP_CONNECT", 1))) @@ -30,6 +28,7 @@ def _connect_client( rpc_client = JSONRPCClient(f"{host}:{port}", timeout) if not startup_connect: + log.debug(f"Auto Connect Disabled, RPC client is not connected") return rpc_client start_time = time() @@ -52,9 +51,7 @@ def _connect_client( if rpc_client.is_connected: rpc_client.disconnect() - raise ConnectionRefusedError( - "Could not establish connection with a tvpaint instance before timeout !" - ) + raise ConnectionRefusedError("Could not establish connection with a tvpaint instance before timeout !") if connection_successful: log.info(f"Connected to TVPaint on port {port}") @@ -120,10 +117,7 @@ def send_cmd( Returns: the George return string """ - tv_args = [ - tv_handle_string(arg) if handle_string and isinstance(arg, str) else arg - for arg in args - ] + tv_args = [tv_handle_string(arg) if handle_string and isinstance(arg, str) else arg for arg in args] cmd_str = " ".join([str(arg) for arg in [command, *tv_args]]) is_undo_stack = command in [ diff --git a/pytvpaint/george/client/parse.py b/pytvpaint/george/client/parse.py index e0f2480..a0d6e7f 100644 --- a/pytvpaint/george/client/parse.py +++ b/pytvpaint/george/client/parse.py @@ -7,6 +7,11 @@ from __future__ import annotations + +import re +import shlex +from contextlib import suppress +from pathlib import Path from collections.abc import Sequence from dataclasses import Field, fields, is_dataclass from enum import Enum @@ -62,227 +67,296 @@ def camel_to_pascal(s: str) -> str: return "".join([c.capitalize() for c in s.split("_")]) -T = TypeVar("T", bound=Any) - - -def tv_cast_to_type(value: str, cast_type: type[T]) -> T: - """Cast a value to the provided type using George's convention for values. - - Note: - "1" and "on"/"ON" values are considered True when parsing a boolean - - Args: - value: the input value - cast_type: the type to cast to - - Raises: - ValueError: if given an enum, and it can't find the value or the enum index is invalid - - Returns: - the value cast to the provided type +def _strip_safe(text: str) -> str: + """ + Removes surrounding quotes ONLY if they form a single matching pair + wrapping the entire string. + + Examples: + '"a b c"' -> 'a b c' (Stripped: wrapper) + '"a" "b"' -> '"a" "b"' (Kept: multiple items) + '"file" 10' -> '"file" 10' (Kept: doesn't end in quote) + 'normal text' -> 'normal text' (Kept) """ - if issubclass(cast_type, Enum): - value = value.strip().strip('"') + if len(text) < 2: + return text - # Find all enum members that matche the value (lower case) - matches = [m for m in cast_type if value.lower() == m.value.lower()] - try: - # If the unmodified value is in the enum, return that first - return cast(T, next(m for m in matches if value == m.value)) - except StopIteration: - # Otherwise return the first match - if matches: - return cast(T, matches[0]) - - # if the above fails, then maybe the value is the index in the enum - try: - index = int(value) - except ValueError: - raise ValueError(f"{value} is not a valid Enum index since it can't be parsed as int") + first = text[0] + # 1. Must start and end with the same quote style + if first not in ('"', "'") or text[-1] != first: + return text - # get the enum member at the index - enum_members = list(cast_type) - if index < len(enum_members): - return cast(T, enum_members[index]) + # 2. Verify the closing quote matches the opening quote syntactically. + # We use regex to ensure the FIRST quoted segment covers the WHOLE string. + # Regex: ^(["']) ( [^"\] OR escaped char )* \1 $ + # This ensures we don't match '"a" "b"' as a single block. + escaped_body = r"(?:[^\\" + first + r"]|\\.)*" + pattern = re.compile(rf"^{first}({escaped_body}){first}$") - raise ValueError(f"Enum index {index} is out of bounds (max {len(enum_members) - 1})") + match = pattern.match(text) + if match: + # Return the inner content (preserving internal escapes like \") + return str(match.group(1)) - if get_origin(cast_type) in (tuple, list): - # Split by space and convert each member to the right type - values_types = [] - sub_value_types = get_args(cast_type) - for index, sub_value in enumerate(value.split()): - sub_value_type = sub_value_types[index] - cast_sub_value = tv_cast_to_type(sub_value, sub_value_type) - values_types.append(cast_sub_value) + return text - origin_type = get_origin(cast_type) - return cast(T, origin_type(values_types)) # type: ignore[misc] - if cast_type == bool: - return cast(T, value.lower() in ["1", "on", "true"]) +def _infer_type(token: str) -> Any: + """ + Guess the type of a string token (Int, Float, Bool, String). + Used when no specific type hint is provided. + """ + # 1. Try Integer (Strict, so 10.5 doesn't become 10) + try: + return int(token) + except ValueError: + pass + + # 2. Try Float + try: + return float(token) + except ValueError: + pass + + # 3. Try Boolean + # Note: '1' and '0' are already caught by int check above, which is usually fine. + lower_val = token.lower() + if lower_val in ("true", "yes", "on"): + return True + if lower_val in ("false", "no", "off"): + return False + + # 4. Fallback to String (removing internal quotes if shlex didn't) + return token.strip("\"'") - if cast_type == str: - return cast(T, value.strip().strip('"')) - return cast(T, cast_type(value)) +T = TypeVar("T", bound=Any) -FieldTypes: TypeAlias = list[tuple[tuple[str, str], Any]] +def tv_cast_to_type(value_str: str, type_cls: type[T]) -> T: + """ + Casts a string value to a specific Python type, supporting: + - Primitives (int, float, bool, str) + - Path objects + - Enums (by name, value, index) + - Collections (List[T], Tuple[T, ...], Tuple[A, B]) + """ + # We strip outer quotes to expose list content (e.g. "a b") + # BUT we leave them alone if it's a complex string (e.g. "a" "b") + clean_val = _strip_safe(value_str.strip()) + origin_type = get_origin(type_cls) + # --- 1. Collections (List[T], Tuple[...]) --- + if type_cls in (list, tuple) or origin_type in (list, tuple): + # Determine the base container type (list or tuple) + container_cls = origin_type or type_cls -def get_dataclass_fields( - datacls: DataclassInstance | type[DataclassInstance], -) -> FieldTypes: + # Tokenize the string (handling quotes: "1 '2 3' 4") + try: + tokens = shlex.split(clean_val, posix=False) + except ValueError: + tokens = clean_val.split() # Fallback + tokens = [t.strip("\"'") for t in tokens] + + # Get inner type arguments (e.g. [int] from List[int]) + type_args = get_args(type_cls) + casted_items = [] + + # Case A: Homogeneous (List[int] or Tuple[int, ...]) + # If args exist and (it's a list OR it's a tuple with Ellipsis) + if type_args and (container_cls is list or (len(type_args) == 2 and type_args[1] is ...)): + item_type = type_args[0] + for token in tokens: + casted_items.append(tv_cast_to_type(token, item_type)) + + # Case B: Fixed-Size Tuple (Tuple[str, int]) + elif container_cls is tuple and type_args: + if len(tokens) != len(type_args): + raise ValueError( + f"Count mismatch for {type_cls}: expected {len(type_args)}, got {len(tokens)} ('{clean_val}')" + ) + for token, sub_type in zip(tokens, type_args): + casted_items.append(tv_cast_to_type(token, sub_type)) + + # Case C: Raw Collection (list, tuple) - Infer or keep strings + else: + for token in tokens: + # Recursive call with 'str' keeps it simple, or add auto-inference logic here + casted_items.append(_infer_type(token)) + + return container_cls(casted_items) + + clean_val = clean_val.strip("\"'") + # --- 2. Basic Primitives --- + if type_cls is str: + return clean_val + if type_cls is int: + return int(float(clean_val)) + if type_cls is float: + return float(clean_val) + if type_cls is Path: + return Path(clean_val) + if type_cls is bool: + return clean_val.lower() in ("true", "1", "yes", "on") + + # --- 3. Enums --- + if isinstance(type_cls, type) and issubclass(type_cls, Enum): + # A. By Name + with suppress(KeyError): + return type_cls[clean_val] + with suppress(KeyError): + return type_cls[clean_val.lower()] + # B. By Value + with suppress(ValueError): + return type_cls(clean_val) + with suppress(ValueError): + return type_cls(clean_val.lower()) + # C. By Int Value (for "1" -> 1) + with suppress(ValueError): + return type_cls(int(float(clean_val))) + # D. By Index (Position in definition) + if clean_val.isdigit(): + with suppress(IndexError): + return list(type_cls)[int(clean_val)] + + raise ValueError(f"'{clean_val}' is not a valid {type_cls.__name__}") + + # Fallback + return type_cls(clean_val) + + +FieldTypes: TypeAlias = list[tuple[tuple[str, str] | str, Any]] + + +def get_dataclass_fields(datacls: DataclassInstance | type[DataclassInstance], alt_names: bool = False) -> FieldTypes: """Get the dataclass key/type pairs and filter those with the "parsed" metadata. Args: datacls: input dataclass + alt_names: get alt names, if no alt_name field name will be used Returns: the list of key/type tuple """ + with_fields = [] type_hints = get_type_hints(datacls) - return [ - ((f.name, f.metadata.get("alt_name", f.name)), type_hints[f.name]) - for f in fields(datacls) - if f.metadata.get("parsed", True) - ] + for f in fields(datacls): + if not f.metadata.get("parsed", True): + continue + + if not alt_names: + with_fields.append((f.name, type_hints[f.name])) + continue -def tv_parse_dict( - input_text: str, + alt_name = f.metadata.get("alt_name", f.name) + with_fields.append(((f.name, alt_name), type_hints[f.name])) + + return with_fields + + +def tv_parse_list( + text: str, with_fields: FieldTypes | type[DataclassInstance], + unused_indices: list[int] | None = None, ) -> dict[str, Any]: - """Parse a list of values as key value pairs returned from TVPaint commands. - - Cast the values to a provided dataclass type or list of key/types pairs. + """ + Parses a positional string into typed arguments. Args: - input_text: the George string result - with_fields: the field types (can be a dataclass) + text: The raw input string. + with_fields: A list of tuples [('name', Type), ...]. + unused_indices: Some George functions return positional arguments that are unused. Defaults to None. Returns: - a dict with the values cast to the given types + a dict with the provided fields and the values cast to the given types """ # For dataclasses get the type hints and filter those with metadata if is_dataclass(with_fields): - with_fields = get_dataclass_fields(with_fields) + with_fields = get_dataclass_fields(with_fields, alt_names=False) else: # Explicitly cast because we are sure now with_fields = cast(FieldTypes, with_fields) - output_dict: dict[str, Any] = {} - search_start = 0 + if len(with_fields) == 1: + # Single Argument, pass the whole raw text to smart_cast. + # It handles splitting internally if type_cls is list/tuple. + name, type_cls = with_fields[0] + return {name: tv_cast_to_type(text, type_cls)} - for i, (field_name, field_type) in enumerate(with_fields): - # handle different naming schemes - alt_field_name = field_name - if isinstance(field_name, tuple): - field_name, alt_field_name = field_name - current_key_pascal = camel_to_pascal(alt_field_name) + # Tokenize (respecting quotes), posix=False preserves Windows backslashes + try: + tokens = shlex.split(text, posix=False) + except ValueError as e: + raise ValueError(f"Parsing error (unbalanced quotes?): {e}") - # Search for the key from the end - search_text = input_text.lower() - try: - start = search_text.index(current_key_pascal.lower(), search_start) - except ValueError: - continue - - if i < (len(with_fields) - 1): - # Search for the next key also from the end - next_key = with_fields[i + 1][0] - if isinstance(next_key, tuple): - next_key = next_key[1] - next_key_pascal = camel_to_pascal(next_key) - end = search_text.rfind(" " + next_key_pascal.lower(), search_start) - else: - end = len(input_text) - - # Get the "key value" substring - key_value = input_text[start:end] - - # Extract the value - cut_at = len(current_key_pascal) + 1 - value = key_value[cut_at:].strip() - - # Cast it to the corresponding type - value = tv_cast_to_type(value, field_type) + # Remove any unused values + if unused_indices: + tokens = [t for i, t in enumerate(tokens) if i not in unused_indices] - output_dict[field_name] = value - search_start = end + parsed_args: dict[str, Any] = {} + for (field_name, type_cls), token in zip(with_fields, tokens): + if isinstance(field_name, tuple): + field_name, _ = field_name + parsed_args[field_name] = tv_cast_to_type(token, type_cls) - return output_dict + return parsed_args -def tv_parse_list( - output: str, - with_fields: FieldTypes | type[DataclassInstance], - unused_indices: list[int] | None = None, -) -> dict[str, Any]: - """Parse a list of values returned from TVPaint commands. +def tv_parse_dict(text: str, with_fields: FieldTypes | type[DataclassInstance]) -> dict[str, Any]: + """Parse a list of values as key value pairs returned from TVPaint commands. Cast the values to a provided dataclass type or list of key/types pairs. - You can specify unused indices to exclude positional values from being parsed. - This is useful because some George commands have unused return values. - Args: - output: the input string - with_fields: the field types (can be a dataclass) - unused_indices: Some George functions return positional arguments that are unused. Defaults to None. + text: The input string (e.g. 'X 10 y 20 TAGS "a b"') + with_fields: A list of tuples [('name', Type), ...]. Returns: - a dict with the values cast to the given types + a dict with the provided fields and the values cast to the given types """ - start = 0 - current = 0 - string_open = False - tokens: list[str] = [] - - while current < len(output): - char = output[current] - is_quote = char == '"' - is_space = char == " " - last_char = current == len(output) - 1 - - if (is_space and not string_open) or last_char: - last_cut = current if not last_char else current + 1 - token = output[start:last_cut] - if start != last_cut and token != " ": - tokens.append(token) - start = current = current + 1 - elif is_quote: - if string_open: - tokens.append(output[start:current]) - - start = current = current + 1 - string_open = not string_open - else: - current += 1 - - # Get type annotations from the dataclass fields + # For dataclasses get the type hints and filter those with metadata if is_dataclass(with_fields): - with_fields = get_dataclass_fields(with_fields) + with_fields = get_dataclass_fields(with_fields, alt_names=True) else: # Explicitly cast because we are sure now with_fields = cast(FieldTypes, with_fields) - # Remove any unused values - if unused_indices: - tokens = [t for i, t in enumerate(tokens) if i not in unused_indices] - - # Cast each token to a type and construct the dict - tokens_dict: dict[str, Any] = {} - for token, (field_name, field_type) in zip(tokens, with_fields): + # 1. Create a Normalized Map for alt names and case sensitivity + # e.g. {'opacity': ('Transparency', int), 'x': ('X', float)} + normalized_map: dict[str, tuple[str, type[T]]] = {} + for field_name, type_cls in with_fields: # handle different naming schemes + alt_field_name = str(field_name) if isinstance(field_name, tuple): - field_name, _ = field_name + field_name, alt_field_name = field_name + + pascal_name = camel_to_pascal(alt_field_name).lower() + normalized_map[pascal_name] = (field_name, type_cls) + + # 2. Sort keys by length (descending) to prevent partial matching + # (e.g. preventing 'x' from matching inside 'x_pos') + sorted_keys = sorted(normalized_map.keys(), key=len, reverse=True) - token = tv_cast_to_type(token, field_type) - tokens_dict[field_name] = token + # 3. Build the Regex, use re.escape for keys with symbols + joined_keys = "|".join(map(re.escape, sorted_keys)) + pattern = re.compile(rf"({joined_keys})\s+(.*?)(?=\s+(?:{joined_keys})|$)", re.IGNORECASE) + parsed_data = {} - return tokens_dict + # 4. match and Cast + for match_key, match_val in pattern.findall(text): + clean_val = match_val.strip() + + # Normalize match to lowercase to look up the type definition + lookup = normalized_map.get(match_key.lower()) + + if lookup: + original_key, target_type = lookup + # Use the original key name for the output dict (e.g. 'x' not 'X') + # and cast the value using smart_cast + parsed_data[original_key] = tv_cast_to_type(clean_val, target_type) + + return parsed_data def args_dict_to_list(args: dict[str, Any]) -> list[Any]: @@ -338,3 +412,120 @@ def validate_args_list(optional_args: Sequence[Value | tuple[Value, ...]]) -> li args.append(arg) return args + + +def normalize_windows_paths(text: str) -> str: + """ + Identifies Windows-style file paths and converts backslashes to forward slashes. + + Supports: + 1. Drive Roots: C:\\Folder + 2. Relative: .\\Folder or ..\\Folder + 3. Root Relative: \\.\\Folder or \\..\\Folder <-- NEW + 4. UNC Paths: \\\\Server\\Share\\File + """ + # --- 1. Building Blocks --- + + # Valid characters for a filename (excludes forbidden chars and newlines) + # Note: We include '.' in valid chars + valid_chars = r'(?:(?!\s[a-zA-Z]:)[^\\/:*?"<>|\r\n])+' + # A Path Segment is a backslash followed by valid chars: "\Folder" + segment = rf"\\{valid_chars}" + + # --- 2. Define Start Patterns --- + + # A. Standard Starts + # Matches: "C:" OR "\.." OR ".." OR "\." OR "." + # Logic: + # [a-zA-Z]: -> Drive Letter + # | + # \\\.\.? -> Root Relative (\. or \..) - SAFE (Dot is not an escape) + # | + # \.\.? -> Relative (. or ..) + # + # use \.\.? (greedy) to match ".." before "." + start_standard = r"(?:[a-zA-Z]:|\\\.\.?|\.\.?)" + + # B. UNC Start + # Matches: "\\Server" (Backslash-Backslash-Hostname) + start_unc = rf"\\\\{valid_chars}" + + # --- 3. Assemble Patterns --- + + # Pattern 1: Standard Path + # Start (C: or `..`) + At least one Segment (\Folder) + pattern_standard = rf"(?:{start_standard})(?:{segment})+" + + # Pattern 2: UNC Path + # Start (\\Host) + At least one Segment (\Share) + pattern_unc = rf"(?:{start_unc})(?:{segment})+" + + # Combine them + full_pattern = rf"({pattern_standard}|{pattern_unc})" + + def _to_posix(match: re.Match) -> str: + return str(match.group(1)).replace("\\", "/") + + return re.sub(full_pattern, _to_posix, text) + + +def unescape_everything_safely(text: str) -> str: + """ + Unescapes a "mangled" string while preserving Windows file paths and filenames. + + This function uses a two-step process: + 1. Normalizes Windows paths to POSIX (forward slashes) to prevent `\\n` in paths + from being misinterpreted as newlines. + 2. Unescapes standard characters (\\n, \\t) and safe Hex/Octal codes. + + Args: + text: The "dirty" raw string (e.g. from a log or subprocess output). + + Returns: + str: The clean, unescaped string with normalized newlines. + """ + # 1. Normalize Paths + # "C:\\new_folder" -> "C:/new_folder" + text = normalize_windows_paths(text) + + # 2. Unescape Logic + # New Regex: Matches greedily (no lookaheads here). + # We catch the full [0-7]{1,3} or x[...]{2} regardless of what follows. + pattern = r"\\([rntbfva]|x[0-9a-fA-F]{2}|[0-7]{1,3})" + + simple_escapes = {"n": "\n", "r": "\r", "t": "\t", "b": "\b", "f": "\f", "v": "\v", "a": "\a"} + + def _replace_match(match: re.Match) -> str: + code = match.group(1) + + # A. Standard Escapes (Always safe: \n, \t, etc) + if code in simple_escapes: + return simple_escapes[code] + + # B. Hex/Octal (Require Safety Check) + # We manually peek at the character AFTER the match. + full_match = match.group(0) # e.g. "\100" + end_idx = match.end() # Index immediately after match + + # Check if we are at the end of the string + if end_idx < len(match.string): + next_char = match.string[end_idx] + # THE SAFETY CHECK: + # If followed by . or _, treat it as a filename, NOT an escape. + if next_char in (".", "_"): + return full_match + + # Perform the conversion + if code.startswith("x"): + return chr(int(code[1:], 16)) + else: + return chr(int(code, 8)) + + # Apply unescape + text = re.sub(pattern, _replace_match, text) + + # 3. Final Pass: Normalize Newlines + # Consolidate Windows (\r\n) and Mac Classic (\r) to Unix (\n) + text = text.replace("\r\n", "\n").replace("\r", "\n") + + return text diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index 2c48f7e..926f9e4 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -699,7 +699,7 @@ def deprecated_warning(msg: str) -> Callable[[T], T]: def decorate(func: T) -> T: @functools.wraps(func) def applicator(*args: Any, **kwargs: Any) -> Any: - log.warning(msg) + log.warning(f"DEPRECTED: {msg}") return func(*args, **kwargs) return cast(T, applicator) @@ -1102,8 +1102,8 @@ def tv_set_b_pen_hsl(color: HSLColor) -> HSLColor: @deprecated_warning( - msg="DEPRECATED: Function `tv_pen` is most likely deprecated it is undocumented in the George reference but still " - "works, We advise using `tv_penbrush` instead." + msg="Function `tv_pen` is most likely deprecated, it is undocumented in the George reference but still " + "works. We advise using `tv_penbrush` instead." ) def tv_pen(size: float) -> float: """Change current pen tool size. diff --git a/pytvpaint/guideline.py b/pytvpaint/guideline.py index 5cecc24..adfe434 100644 --- a/pytvpaint/guideline.py +++ b/pytvpaint/guideline.py @@ -148,7 +148,7 @@ def refresh(self) -> None: @refreshed_property def path(self) -> Path: - """The x coordinate of the point.""" + """The image path.""" return self._data.path @path.setter @@ -157,7 +157,7 @@ def path(self, value: Path | str) -> None: @refreshed_property def x(self) -> float: - """The x coordinate of the point.""" + """The x coordinate of the image.""" return self._data.x @x.setter @@ -166,7 +166,7 @@ def x(self, value: float) -> None: @refreshed_property def y(self) -> float: - """The y coordinate of the point.""" + """The y coordinate of the image.""" return self._data.y @y.setter @@ -175,7 +175,7 @@ def y(self, value: float) -> None: @refreshed_property def rotation(self) -> float: - """The angle of the camera at that point.""" + """The rotation of the image.""" return self._data.rotation @rotation.setter @@ -184,7 +184,7 @@ def rotation(self, value: float) -> None: @refreshed_property def scale(self) -> float: - """The scale of the camera at that point.""" + """The scale of the image.""" return self._data.scale @scale.setter @@ -193,7 +193,7 @@ def scale(self, value: float) -> None: @refreshed_property def flip(self) -> george.FlipDirection: - """The scale of the camera at that point.""" + """The orientation of the image.""" return self._data.flip @flip.setter @@ -212,7 +212,7 @@ def new( flip: george.FlipDirection | None = None, alpha_mode: george.GuidelineAlphaMode | None = None, ) -> GuidelineImage: - """Create a new point and add it to the camera path at that index.""" + """Create a new guideline in the project.""" project.make_current() position = george.tv_guideline_add_image(img_path, x, y, rotation, scale, flip, alpha_mode) diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index 133efd7..08e1f92 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -366,7 +366,7 @@ def id(self) -> int: """The layers unique identifier. Warning: - layer ids are not persistent across project load/close + layer ids are not persistent and are reset every time the project is opened """ return self._id @@ -464,7 +464,7 @@ def name(self, value: str) -> None: if value == self.name: return - layer_names = (layer.name for layer in self.clip.layers if layer != self) + layer_names = (layer.name for layer in self.clip.get_layers() if layer != self) value = utils.get_unique_name(layer_names, value) george.tv_layer_rename(self.id, value) @@ -1465,14 +1465,6 @@ def sources(self) -> list[Layer]: """Get this CTG layer's source layers.""" sources = [] - # TODO commenting this since it was used when tv_CTGGetSources "wasn't" working - # for layer in self.clip.layers: - # if not layer.is_ctg_source: - # continue # noqa: ERA001 - # if self.id not in [ctg_layer.id for ctg_layer in layer.sourced_ctg_layers]: - # continue # noqa: ERA001 - # sources.append(layer) # noqa: ERA001 - for layer_id in george.tv_ctg_get_source(self.id): layer = self.clip.get_layer(by_id=layer_id) if not layer: diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 41833bf..eda62e5 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -10,6 +10,7 @@ from fileseq.filesequence import FileSequence from pytvpaint import george, utils +from pytvpaint.george.client import parse from pytvpaint.george.exceptions import GeorgeError from pytvpaint.sound import ProjectSound from pytvpaint import guideline @@ -288,7 +289,7 @@ def clear_background(self) -> None: @property def header_info(self) -> str: """The project's header info.""" - return george.tv_project_header_info_get(self.id) + return parse.unescape_everything_safely(george.tv_project_header_info_get(self.id)) @header_info.setter def header_info(self, value: str) -> None: @@ -297,7 +298,7 @@ def header_info(self, value: str) -> None: @property def author(self) -> str: """The project's author info.""" - return george.tv_project_header_author_get(self.id) + return parse.unescape_everything_safely(george.tv_project_header_author_get(self.id)) @author.setter def author(self, value: str) -> None: @@ -306,7 +307,7 @@ def author(self, value: str) -> None: @property def notes(self) -> str: """The project's notes text.""" - return george.tv_project_header_notes_get(self.id) + return parse.unescape_everything_safely(george.tv_project_header_notes_get(self.id)) @notes.setter def notes(self, value: str) -> None: diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index 215a2b2..aaa590f 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -340,7 +340,7 @@ def render_context( layers_visibility = [] if layer_selection: clip = layer_selection[0].clip - layers_visibility = [(layer, layer.is_visible) for layer in clip.layers] + layers_visibility = [(layer, layer.is_visible) for layer in clip.get_layers()] # Show and hide the clip layers to render for layer, _ in layers_visibility: should_be_visible = not layer_selection or layer in layer_selection @@ -443,7 +443,7 @@ def get_tvp_element( continue if by_name is not None and element.name.lower() != by_name.lower(): continue - if by_regex is not None and element.name != by_regex.search(element.name): + if by_regex is not None and not by_regex.search(element.name): continue if by_path is not None and getattr(element, "path") != Path(by_path): continue diff --git a/tests/conftest.py b/tests/conftest.py index 4161555..c9b3c50 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,7 +67,7 @@ def _fix_tvp_12_selection() -> None: if current_layer.layer_type == george.LayerType.CAMERA: # switch to first layer in real/non-camera layers first_layer = None - for layer in current_clip.layers: + for layer in current_clip.get_layers(): if layer.layer_type == george.LayerType.CAMERA: continue @@ -191,9 +191,7 @@ def count_up_generate(test_clip_obj: Clip) -> None: george.tv_set_a_pen_rgba(george.RGBColor(0, 0, 0), 255) # set the pen color send_cmd("tv_TextTool2", "size", 200) # set text size george.tv_text_brush(str(i)) # set the brush text - george.tv_set_active_shape( - george.TVPShape.FREE_HAND_LINE, size=200 - ) # set the shape and it's size + george.tv_set_active_shape(george.TVPShape.FREE_HAND_LINE, size=200) # set the shape and it's size # write a line with the text brush, having the start-end pos being the same will fake a single click george.tv_line(text_pos, text_pos) # update undo stack otherwise edits to last image are not saved (-_-)" @@ -354,9 +352,7 @@ def create_some_layer_folders( @pytest.fixture -def test_project_sound( - test_project_obj: Project, wav_file: Path -) -> FixtureYield[ProjectSound]: +def test_project_sound(test_project_obj: Project, wav_file: Path) -> FixtureYield[ProjectSound]: tv_sound_project_new(wav_file) yield ProjectSound(0, test_project_obj) @@ -368,9 +364,7 @@ def test_clip_sound(test_clip_obj: Clip, wav_file: Path) -> FixtureYield[ClipSou @pytest.fixture -def create_some_project_sounds( - test_project_obj: Project, wav_file: Path -) -> FixtureYield[list[ProjectSound]]: +def create_some_project_sounds(test_project_obj: Project, wav_file: Path) -> FixtureYield[list[ProjectSound]]: sounds: list[ProjectSound] = [] for i in range(5): @@ -381,9 +375,7 @@ def create_some_project_sounds( @pytest.fixture -def with_loaded_sequence( - test_clip_obj: Clip, ppm_sequence: list[Path] -) -> FixtureYield[Layer]: +def with_loaded_sequence(test_clip_obj: Clip, ppm_sequence: list[Path]) -> FixtureYield[Layer]: yield test_clip_obj.load_media( ppm_sequence[0], with_name="images", diff --git a/tests/george/client/test_parse.py b/tests/george/client/test_parse.py index 8616a23..8fc3da4 100644 --- a/tests/george/client/test_parse.py +++ b/tests/george/client/test_parse.py @@ -1,9 +1,9 @@ from __future__ import annotations -from dataclasses import dataclass from enum import Enum from pathlib import Path -from typing import Any +from dataclasses import dataclass +from typing import Any, Tuple, List import pytest @@ -14,6 +14,8 @@ tv_handle_string, tv_parse_dict, tv_parse_list, + normalize_windows_paths, + unescape_everything_safely, ) @@ -47,6 +49,16 @@ class EnumTest(Enum): C = "Cc" +class Color(Enum): + RED = "red" + BLUE = "blue" + + +class LayerType(Enum): + IMG = 1 + SEQ = 2 + + @pytest.mark.parametrize( "value, cast, result", [ @@ -73,14 +85,148 @@ def test_tv_cast_to_type(value: str, cast: Any, result: str) -> None: assert tv_cast_to_type(value, cast) == result -def test_tv_cast_to_enum_index_parse_error() -> None: - with pytest.raises(ValueError, match="can't be parsed as int"): - tv_cast_to_type("valfsg", EnumTest) +def test_primitives(): + assert tv_cast_to_type("123", int) == 123 + assert tv_cast_to_type("123.45", float) == 123.45 + assert tv_cast_to_type("true", bool) is True + assert tv_cast_to_type("FALSE", bool) is False + assert tv_cast_to_type("hello", str) == "hello" + + +@pytest.mark.parametrize( + "input_text, error_match", + [ + ("valfsg", "is not a valid"), + ("67", "is not a valid"), + ], +) +def test_tv_cast_to_enum_index_out_of_bounds(input_text, error_match) -> None: + with pytest.raises(ValueError, match=error_match): + tv_cast_to_type(input_text, EnumTest) + + +def test_homogeneous_list(): + # List[int]: applies int() to all items + input_str = "10 20 30" + assert tv_cast_to_type(input_str, List[int]) == [10, 20, 30] + + +def test_variable_tuple(): + # Tuple[float, ...]: applies float() to all items + input_str = "1.1 2.2 3.3" + assert tv_cast_to_type(input_str, Tuple[float, ...]) == (1.1, 2.2, 3.3) + + +def test_fixed_tuple(): + # Tuple[str, int]: First item str, second item int + input_str = '"my file.png" 50' + expected = ("my file.png", 50) + assert tv_cast_to_type(input_str, Tuple[str, int]) == expected + + +def test_fixed_tuple_mismatch_error(): + # Tuple expects 2 items, got 3 + with pytest.raises(ValueError) as exc: + tv_cast_to_type("1 2 3", Tuple[int, int]) + assert "Count mismatch" in str(exc.value) + + +def test_raw_list_quoting(): + # Raw list (no types), but handles quoted strings correctly + input_str = 'item1 "item 2" item3' + # Should result in 3 items, not 4 + assert tv_cast_to_type(input_str, list) == ["item1", "item 2", "item3"] + + +def test_enum_strategies(): + # 1. Name + assert tv_cast_to_type("RED", Color) == Color.RED + # 2. Value + assert tv_cast_to_type("blue", Color) == Color.BLUE + # 3. Int Value (String "1" -> Value 1) + assert tv_cast_to_type("1", LayerType) == LayerType.IMG + # 4. Index (Position 1 -> Second item -> BLUE) + # "1" is ambiguous for Color, but since Color values are strings, "1" is treated as index + assert tv_cast_to_type("1", Color) == Color.BLUE + + +def test_nested_path_list(): + # List[Path] + input_str = r'"C:\file1.txt" "D:\file2.txt"' + result = tv_cast_to_type(input_str, List[Path]) + assert result[0] == Path(r"C:\file1.txt") + assert isinstance(result[1], Path) + + +def test_strip_logic_list(): + # Case 1: Wrapped List "item1 item2" + # Expected: Strip quotes -> split -> ['item1', 'item2'] + assert tv_cast_to_type('"item1 item2"', List[str]) == ["item1", "item2"] + + # Case 2: Separate Quoted Items "item1" "item2" + # Expected: Don't strip (middle quote) -> split -> ['item1', 'item2'] + assert tv_cast_to_type('"item1" "item2"', List[str]) == ["item1", "item2"] + + # Case 3: Mixed content "file.ext" 10 + # Expected: Don't strip (doesn't end in quote) -> split -> ['file.ext', '10'] + assert tv_cast_to_type('"file.ext" 10', List[str]) == ["file.ext", "10"] + + # Case 4: Single quoted item '"item1"' + # Expected: Strip -> 'item1' -> list -> ['item1'] + assert tv_cast_to_type('"item1"', List[str]) == ["item1"] + + +def test_strip_logic_primitives(): + # Ensure stripping still works for single values + assert tv_cast_to_type('"123"', int) == 123 + assert tv_cast_to_type("'true'", bool) is True + + # Nested quotes handling: '"text"' + assert tv_cast_to_type('"text"', str) == "text" + + +def test_auto_inference_mixed_list(): + # Input: String "word" Integer 10 Float 15.5 Bool true + input_str = "word 10 15.5 true" + + # We ask for a raw 'list', triggering auto-inference + result = tv_cast_to_type(input_str, list) + + assert result == ["word", 10, 15.5, True] + assert isinstance(result[1], int) + assert isinstance(result[2], float) + assert isinstance(result[3], bool) + + +def test_auto_inference_quoted_strings(): + # Quotes should be stripped during inference if they aren't syntactical + input_str = '"my string" "10"' + + result = tv_cast_to_type(input_str, list) + + # "my string" -> my string (str) + # "10" -> 10 (int) - because _infer_type tries int() on the token "10" + assert result == ["my string", 10] + + +def test_auto_inference_nested_structure(): + # Raw tuple + input_str = '100 false "file.txt"' + result = tv_cast_to_type(input_str, tuple) + assert result == (100, False, "file.txt") -def test_tv_cast_to_enum_index_out_of_bounds() -> None: - with pytest.raises(ValueError, match="out of bounds"): - tv_cast_to_type("67", EnumTest) + +def test_auto_inference_edge_cases(): + # Negative numbers and zero + input_str = "-5 0 0.0" + result = tv_cast_to_type(input_str, list) + assert result == [-5, 0, 0.0] + + # Boolean variants + input_str = "yes no" + result = tv_cast_to_type(input_str, list) + assert result == [True, False] @dataclass @@ -183,6 +329,91 @@ def test_tv_parse_dict( assert result_dict == check_keys +class LayerMode(Enum): + NORMAL = "normal" + MULTIPLY = "multiply" + + +TEST_DEFS = [ + ("path", Path), + ("x", float), + ("y", float), + ("opacity", int), + ("tags", List[str]), + ("mode", LayerMode), + ("visible", bool), + ("dims", Tuple[int, int]), +] + + +def test_happy_path_typed(): + """Verifies standard usage with mixed types.""" + input_str = r'path "\\server\ref.png" x 10.5 y 20.2 opacity 255 visible true' + + result = tv_parse_dict(input_str, TEST_DEFS) + + assert result["path"] == Path(r"\\server\ref.png") + assert result["x"] == 10.5 + assert isinstance(result["x"], float) + assert result["opacity"] == 255 + assert result["visible"] is True + + +def test_case_insensitivity_normalization(): + """ + Verifies that input keys like 'OPACITY' are matched case-insensitively + but stored using the normalized key 'opacity' from definitions. + """ + input_str = "OPACITY 50 X 100 Visible FALSE" + + result = tv_parse_dict(input_str, TEST_DEFS) + + assert result["opacity"] == 50 + assert result["x"] == 100.0 + assert result["visible"] is False + + # Ensure keys are normalized in the output dict + assert "OPACITY" not in result + assert "opacity" in result + + +def test_list_and_enum(): + """Verifies generic List[str] and Enum casting.""" + input_str = 'tags "tree sky" mode multiply' + result = tv_parse_dict(input_str, TEST_DEFS) + + assert result["tags"] == ["tree", "sky"] + assert result["mode"] == LayerMode.MULTIPLY + + +def test_tuple_casting(): + """Verifies Tuple[int, int] casting from a space-separated string.""" + input_str = 'dims "1920 1080"' + result = tv_parse_dict(input_str, TEST_DEFS) + + assert result["dims"] == (1920, 1080) + + +def test_duplicate_keys_last_wins_case_insensitive(): + """Verifies that the last occurrence of a key overwrites previous ones.""" + input_str = "opacity 10 OPACITY 100" + result = tv_parse_dict(input_str, TEST_DEFS) + + assert result["opacity"] == 100 + + +def test_unknown_keys_error(): + """Verifies that parts of the string not matching known keys are ignored.""" + input_str = "x 10 unknown_param 999" + with pytest.raises(ValueError): + tv_parse_dict(input_str, TEST_DEFS) + + +def test_empty_input(): + """Verifies empty input handling.""" + assert tv_parse_dict("", TEST_DEFS) == {} + + @dataclass class Project: name: str @@ -228,3 +459,393 @@ def test_tv_parse_list( ) -> None: result_dict = tv_parse_list(list_str, with_fields=with_type) assert result_dict == check_keys + + +class Orientation(Enum): + HORIZONTAL = "horiz" + VERTICAL = "vert" + + +class Layer(Enum): + BACKGROUND = 1 + FOREGROUND = 2 + + +ARGS_DEF = [ + ("path", Path), + ("x", float), + ("y", float), + ("visible", bool), + ("tags", list), + ("orient", Orientation), + ("layer", Layer), +] + + +# --- Tests --- + + +def test_happy_path_complex(): + input_str = r'"C:\my files\img.png" 10.5 20.0 true "tree sky water" horiz 2' + + result = tv_parse_list(input_str, ARGS_DEF) + + assert result["path"] == Path(r"C:\my files\img.png") + assert isinstance(result["x"], float) + assert result["x"] == 10.5 + assert result["tags"] == ["tree", "sky", "water"] + assert result["orient"] == Orientation.HORIZONTAL + assert result["layer"] == Layer.FOREGROUND + + +def test_enum_lookup_strategies(): + """Verifies the suppress waterfall logic works.""" + # 1. By Name (VERTICAL) + res_name = tv_parse_list(r'"p" 0 0 true [] VERTICAL 1', ARGS_DEF) + assert res_name["orient"] == Orientation.VERTICAL + + # 2. By Value ("vert") + res_val = tv_parse_list(r'"p" 0 0 true [] vert 1', ARGS_DEF) + assert res_val["orient"] == Orientation.VERTICAL + + # 3. By Int String ("2") -> int(2) -> Layer(2) + res_int = tv_parse_list(r'"p" 0 0 true [] 1 2', ARGS_DEF) + assert res_int["orient"] == Orientation.VERTICAL + assert res_int["layer"] == Layer.FOREGROUND + + +@pytest.mark.parametrize( + "input_text, expected", + [ + ('"10 20.5 word"', [10, 20.5, "word"]), + ("10 20.5 word", [10, 20.5, "word"]), + ], +) +def test_list_numeric_conversion(input_text: str, expected: list): + """Verifies list items are smart-cast to numbers using suppress.""" + defs = [("vals", list)] + # "10 20.5 word" -> [10, 20.5, "word"] + result = tv_parse_list(input_text, defs) + assert result["vals"] == expected + + +def test_single_arg_optimization_list(): + """ + Verifies that a single list argument consumes the entire string + without needing quotes. + """ + # Definition: One argument named 'ids', type is List[int] + defs = [("ids", List[int])] + + # Input: Space-separated numbers (without outer quotes) + # OLD behavior: shlex.split -> ['10', '20', '30'] -> Length 3 vs 1 -> Error + # NEW behavior: smart_cast("10 20 30", List[int]) -> [10, 20, 30] -> Success + input_str = "10 20 30" + + result = tv_parse_list(input_str, defs) + assert result["ids"] == [10, 20, 30] + + +def test_single_arg_complex_quotes(): + """ + Verifies that quotes are handled correctly inside the single argument. + """ + defs = [("tags", List[str])] + # Input: mixed quoting + input_str = 'tag1 "tag 2" tag3' + + result = tv_parse_list(input_str, defs) + assert result["tags"] == ["tag1", "tag 2", "tag3"] + + +def test_multi_arg_still_validates(): + """ + Verifies that we didn't break validation for multiple arguments. + """ + defs = [("x", int), ("y", int)] + + # Input has 3 tokens, but we expect 2. Should still fail. + input_str = "10 20 30" + + assert tv_parse_list(input_str, defs) == {"x": 10, "y": 20} + + +def test_single_arg_primitive(): + """ + Sanity check that simple single primitives still work. + """ + defs = [("name", str)] + assert tv_parse_list("my_layer", defs) == {"name": "my_layer"} + + # Even if it looks like a list, if type is str, it remains a string + # (smart_cast(..., str) doesn't split) + assert tv_parse_list("a b c", defs) == {"name": "a b c"} + + +@pytest.mark.parametrize( + "input_text, expected", + [ + (r"Line1\nLine2", "Line1\nLine2"), + (r"Col1\tCol2", "Col1\tCol2"), + (r"Carriage\rReturn", "Carriage\nReturn"), # Normalized to \n + (r"Mixed\r\nNewline", "Mixed\nNewline"), # Normalized to \n + (r"Bell\bChar", "Bell\bChar"), + ], +) +def test_standard_escapes(input_text, expected): + """Verifies that standard escaped characters are correctly converted.""" + assert unescape_everything_safely(input_text) == expected + + +@pytest.mark.parametrize( + "input_text, expected", + [ + (r"Hello\x20World", "Hello World"), # Hex space + (r"Value\040Octal", "Value Octal"), # Octal space + (r"Complex\x4A", "ComplexJ"), # Hex 'J' + (r"Octal\101", "OctalA"), # Octal 'A' + ], +) +def test_hex_octal_conversion(input_text, expected): + """Verifies that valid hex and octal codes are converted.""" + assert unescape_everything_safely(input_text) == expected + + +@pytest.mark.parametrize( + "input_text, expected", + [ + # Hex conflicts + (r"folder\x_file.txt", r"folder\x_file.txt"), # Should NOT unescape \x (invalid hex anyway, but safe) + (r"data\x86_temp.dat", r"data\x86_temp.dat"), # Valid hex structure, but followed by _, so PROTECTED + # Octal conflicts (Filenames starting with numbers) + (r"path\2.2.0\lib", r"path\2.2.0\lib"), # Protected because of following dot + (r"log\100_backup", r"log\100_backup"), # Protected because of following underscore + # False Positives (Should unescape) + (r"value\x41text", "valueAtext"), # Unescaped: 't' is not a separator + (r"calc\101value", "calcAvalue"), # Unescaped: 'v' is not a separator + ], +) +def test_filename_collision_safety(input_text, expected): + """Verifies that filenames looking like hex/octal are PRESERVED.""" + assert unescape_everything_safely(input_text) == expected + + +@pytest.mark.parametrize( + "input_text, expected", + [ + (r"C:\Users\Name", r"C:/Users/Name"), # \U is not an escape, but logic holds + (r"C:\new_folder", r"C:/new_folder"), # \n protected by (? C:/Users/Admin) + # Result: "Path: C:/Users/Admin\nStatus: OK" + # Step 2: Unescape (\n -> Newline) + expected = "Path: C:/Users/Admin\nStatus: OK" + assert unescape_everything_safely(raw) != expected + + +@pytest.mark.parametrize( + "input_text, expected", + [ + (r"C:\Users\Admin", "C:/Users/Admin"), + (r"D:\Data\new_folder\table.csv", "D:/Data/new_folder/table.csv"), + (r"\\Server\Share\Report.pdf", "//Server/Share/Report.pdf"), + (r"Z:\Work\real_v1.0", "Z:/Work/real_v1.0"), + ], +) +def test_path_normalization(input_text: str, expected: str): + """Verifies that Windows paths are correctly converted to POSIX style.""" + assert normalize_windows_paths(input_text) == expected + assert unescape_everything_safely(input_text) == expected + + +@pytest.mark.parametrize( + "input_text, expected", + [ + # 1. Standard Relative (..\) + (r"..\images\bg.png", "../images/bg.png"), + (r".\logs\current.log", "./logs/current.log"), + # 2. Multi-Level Relative (..\..\) + (r"..\..\assets\char.png", "../../assets/char.png"), + (r".\subdir\..\file.txt", "./subdir/../file.txt"), + # 3. Root Relative (\..\) <-- NEW CASE + # Starts with literal backslash, then dot(s). + (r"\..\..\dir1\file.txt", "/../../dir1/file.txt"), + (r"\.\config.ini", "/./config.ini"), + # 4. Mixed with Text + (r"Check path \..\..\logs now", "Check path /../../logs now"), + ], +) +def test_relative_paths(input_text, expected): + # Verify the regex catches the relative starts + assert normalize_windows_paths(input_text) == expected + assert unescape_everything_safely(input_text) == expected + + +@pytest.mark.parametrize( + "input_text, expected", + [ + # 1. Standard UNC + (r"\\Server\Share\File.txt", "//Server/Share/File.txt"), + # 2. IP Address Host + (r"\\192.168.1.10\Public\Doc.pdf", "//192.168.1.10/Public/Doc.pdf"), + # 3. Dashes in Hostname + (r"\\My-NAS-01\Backup\v1", "//My-NAS-01/Backup/v1"), + ], +) +def test_unc_paths(input_text, expected): + assert normalize_windows_paths(input_text) == expected + + +def test_safety_check(): + """Ensure generic backslashes don't accidentally become paths.""" + # \n should NOT match because 'n' is not '.' or '..' + assert normalize_windows_paths(r"Line1\nLine2") == r"Line1\nLine2" + + # \Users should NOT match (Ambiguous start rule: must be Drive, UNC, or Dot-Relative) + # If we allowed \Users, we would break \Unknown escapes. + assert normalize_windows_paths(r"\Users\Admin") == r"\Users\Admin" + + +@pytest.mark.parametrize( + "input_text, expected", + [ + # Path inside Quotes + (r'"C:\Program Files\App"', '"C:/Program Files/App"'), + # Path with Spaces + (r"C:\My Documents\file.txt", "C:/My Documents/file.txt"), + # Multiple paths + (r"Copy C:\Source\A to D:\Dest\B", "Copy C:/Source/A to D:/Dest/B"), + # Ambiguous: Path structure priority + (r"C:\Notes\nLine2", "C:/Notes/nLine2"), + ], +) +def test_edge_cases(input_text: str, expected: str): + """Verifies robustness against quotes, spaces, and ambiguous inputs.""" + assert unescape_everything_safely(input_text) == expected + + +def test_hex_integrity(): + """Verifies that standard hex unescaping still works when no path is involved.""" + assert unescape_everything_safely(r"Value\x41") == "ValueA" + assert unescape_everything_safely(r"folder\x_file") == r"folder\x_file" diff --git a/tests/test_clip.py b/tests/test_clip.py index 6a84c04..bc49a00 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -207,7 +207,7 @@ # # # def test_clip_layers(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: -# assert list(test_clip_obj.layers) == create_some_layers +# assert list(test_clip_obj.get_layers()) == create_some_layers # # # @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") From d5d96503ed47bc67be95997f49ec052a8c50b775 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 24 Feb 2026 13:11:02 +0100 Subject: [PATCH 09/26] ADD more support for guidelines --- pytvpaint/george/grg_guideline.py | 579 +++++++++++++++++++++++++++++- pytvpaint/guideline.py | 442 ++++++++++++++++++++++- pytvpaint/layer.py | 1 + pytvpaint/utils.py | 7 +- 4 files changed, 1016 insertions(+), 13 deletions(-) diff --git a/pytvpaint/george/grg_guideline.py b/pytvpaint/george/grg_guideline.py index 478cebb..643e2ad 100644 --- a/pytvpaint/george/grg_guideline.py +++ b/pytvpaint/george/grg_guideline.py @@ -557,8 +557,32 @@ def tv_guideline_modify_image_set( return TVPGuidelineImage(**guideline) +def tv_guideline_add_line( + x: float | None = None, + y: float | None = None, + angle: float | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "x": x, + "y": y, + "angle": angle, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "line", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_line_get(position: int) -> TVPGuidelineLine: - """Get info for the image guideline at the given position.""" + """Get info for the line guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -571,8 +595,62 @@ def tv_guideline_modify_line_get(position: int) -> TVPGuidelineLine: return TVPGuidelineLine(**guideline) +def tv_guideline_modify_line_set( + position: int, + x: float | None = None, + y: float | None = None, + angle: float | None = None, +) -> TVPGuidelineLine: + """Set info for the line guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "x": x, + "y": y, + "angle": angle, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + fields = get_dataclass_fields(cast(DataclassInstance, TVPGuidelineLine)) + guideline = tv_parse_dict(result, with_fields=fields) + guideline["position"] = position + return TVPGuidelineLine(**guideline) + + +def tv_guideline_add_segment( + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "x1": x1, + "y1": y1, + "x2": x2, + "y2": y2, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "segment", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_segment_get(position: int) -> TVPGuidelineSegment: - """Get info for the image guideline at the given position.""" + """Get info for the segment guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -585,8 +663,61 @@ def tv_guideline_modify_segment_get(position: int) -> TVPGuidelineSegment: return TVPGuidelineSegment(**guideline) +def tv_guideline_modify_segment_set( + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, +) -> TVPGuidelineSegment: + """Set info for the segment guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "x1": x1, + "y1": y1, + "x2": x2, + "y2": y2, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + fields = get_dataclass_fields(cast(DataclassInstance, TVPGuidelineSegment)) + guideline = tv_parse_dict(result, with_fields=fields) + guideline["position"] = position + return TVPGuidelineSegment(**guideline) + + +def tv_guideline_add_circle( + x: float | None = None, + y: float | None = None, + radius: float | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "x": x, + "y": y, + "radius": radius, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "circle", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_circle_get(position: int) -> TVPGuidelineCircle: - """Get info for the image guideline at the given position.""" + """Get info for the circle guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -599,8 +730,61 @@ def tv_guideline_modify_circle_get(position: int) -> TVPGuidelineCircle: return TVPGuidelineCircle(**guideline) +def tv_guideline_modify_circle_set( + position: int, + x: float | None = None, + y: float | None = None, + radius: float | None = None, +) -> TVPGuidelineCircle: + """Set info for the circle guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "x": x, + "y": y, + "radius": radius, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineCircle) + guideline["position"] = position + return TVPGuidelineCircle(**guideline) + + +def tv_guideline_add_ellipse( + x: float | None = None, + y: float | None = None, + radius_a: float | None = None, + radius_b: float | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "x": x, + "y": y, + "radiusa": radius_a, + "radiusb": radius_b, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "ellipse", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_ellipse_get(position: int) -> TVPGuidelineEllipse: - """Get info for the image guideline at the given position.""" + """Get info for the ellipse guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -613,8 +797,63 @@ def tv_guideline_modify_ellipse_get(position: int) -> TVPGuidelineEllipse: return TVPGuidelineEllipse(**guideline) +def tv_guideline_modify_ellipse_set( + position: int, + x: float | None = None, + y: float | None = None, + radius_a: float | None = None, + radius_b: float | None = None, +) -> TVPGuidelineEllipse: + """Set info for the ellipse guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "x": x, + "y": y, + "radiusa": radius_a, + "radiusb": radius_b, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineEllipse) + guideline["position"] = position + return TVPGuidelineEllipse(**guideline) + + +def tv_guideline_add_grid( + x: float | None = None, + y: float | None = None, + width: float | None = None, + height: float | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "x": x, + "y": y, + "w": width, + "h": height, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "grid", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_grid_get(position: int) -> TVPGuidelineGrid: - """Get info for the image guideline at the given position.""" + """Get info for the grid guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -627,8 +866,59 @@ def tv_guideline_modify_grid_get(position: int) -> TVPGuidelineGrid: return TVPGuidelineGrid(**guideline) +def tv_guideline_modify_grid_set( + position: int, + x: float | None = None, + y: float | None = None, + width: float | None = None, + height: float | None = None, +) -> TVPGuidelineGrid: + """Set info for the grid guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "x": x, + "y": y, + "w": width, + "h": height, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineGrid) + guideline["position"] = position + return TVPGuidelineGrid(**guideline) + + +def tv_guideline_add_marks( + count_x: int | None = None, + count_y: int | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "count_x": count_x, + "count_y": count_y, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "marks", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_marks_get(position: int) -> TVPGuidelineMarks: - """Get info for the image guideline at the given position.""" + """Get info for the marks guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -641,8 +931,55 @@ def tv_guideline_modify_marks_get(position: int) -> TVPGuidelineMarks: return TVPGuidelineMarks(**guideline) +def tv_guideline_modify_marks_set( + position: int, + count_x: int | None = None, + count_y: int | None = None, +) -> TVPGuidelineMarks: + """Set info for the marks guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "count_x": count_x, + "count_y": count_y, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineMarks) + guideline["position"] = position + return TVPGuidelineMarks(**guideline) + + +def tv_guideline_add_safe_area( + sf_out: int | None = None, + sf_in: int | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "out": sf_out, + "in": sf_in, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "safearea", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_safe_area_get(position: int) -> TVPGuidelineSafeArea: - """Get info for the image guideline at the given position.""" + """Get info for the safe area guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -655,8 +992,57 @@ def tv_guideline_modify_safe_area_get(position: int) -> TVPGuidelineSafeArea: return TVPGuidelineSafeArea(**guideline) +def tv_guideline_modify_safe_area_set( + position: int, + sf_out: int | None = None, + sf_in: int | None = None, +) -> TVPGuidelineSafeArea: + """Set info for the safe area guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "out": sf_out, + "in": sf_in, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineSafeArea) + guideline["position"] = position + return TVPGuidelineSafeArea(**guideline) + + +def tv_guideline_add_vanish_point_1( + x: float | None = None, + y: float | None = None, + grid: bool | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "x": x, + "y": y, + "grid": grid, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "vanishpoint1", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_vanish_point_1_get(position: int) -> TVPGuidelineVanishPoint1: - """Get info for the image guideline at the given position.""" + """Get info for the vanish point 1 guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -669,8 +1055,63 @@ def tv_guideline_modify_vanish_point_1_get(position: int) -> TVPGuidelineVanishP return TVPGuidelineVanishPoint1(**guideline) +def tv_guideline_modify_vanish_point_1_set( + position: int, + x: float | None = None, + y: float | None = None, + ray: int | None = None, + grid: bool | None = None, +) -> TVPGuidelineVanishPoint1: + """Set info for the vanish point 1 guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "x": x, + "y": y, + "ray": ray, + "grid": grid, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint1) + guideline["position"] = position + return TVPGuidelineVanishPoint1(**guideline) + + +def tv_guideline_add_vanish_point_2( + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "x1": x1, + "y1": y1, + "x2": x2, + "y2": y2, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "vanishpoint2", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_vanish_point_2_get(position: int) -> TVPGuidelineVanishPoint2: - """Get info for the image guideline at the given position.""" + """Get info for the vanish point 2 guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -683,8 +1124,69 @@ def tv_guideline_modify_vanish_point_2_get(position: int) -> TVPGuidelineVanishP return TVPGuidelineVanishPoint2(**guideline) +def tv_guideline_modify_vanish_point_2_set( + position: int, + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + ray: int | None = None, +) -> TVPGuidelineVanishPoint2: + """Set info for the vanish point 2 guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "x1": x1, + "y1": y1, + "x2": x2, + "y2": y2, + "ray": ray, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint2) + guideline["position"] = position + return TVPGuidelineVanishPoint2(**guideline) + + +def tv_guideline_add_vanish_point_3( + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + x3: float | None = None, + y3: float | None = None, +) -> int: + """Set info for the image guideline at the given position.""" + args = args_dict_to_list( + { + "x1": x1, + "y1": y1, + "x2": x2, + "y2": y2, + "x3": x3, + "y3": y3, + } + ) + + return int( + send_cmd( + "tv_GuidelineAdd", + "vanishpoint3", + *args, + error_values=[-1, -2], + ) + ) + + def tv_guideline_modify_vanish_point_3_get(position: int) -> TVPGuidelineVanishPoint3: - """Get info for the image guideline at the given position.""" + """Get info for the vanish point 3 guideline at the given position.""" result = send_cmd( "tv_GuidelineModify", @@ -695,3 +1197,60 @@ def tv_guideline_modify_vanish_point_3_get(position: int) -> TVPGuidelineVanishP guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint3) guideline["position"] = position return TVPGuidelineVanishPoint3(**guideline) + + +def tv_guideline_modify_vanish_point_3_set( + position: int, + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + x3: float | None = None, + y3: float | None = None, + ray: int | None = None, +) -> TVPGuidelineVanishPoint3: + """Set info for the vanish point 3 guideline at the given position.""" + args = args_dict_to_list( + { + "position": position, + "x1": x1, + "y1": y1, + "x2": x2, + "y2": y2, + "x3": x3, + "y3": y3, + "ray": ray, + } + ) + + result = send_cmd( + "tv_GuidelineModify", + *args, + error_values=[-1, -2], + ) + + guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint3) + guideline["position"] = position + return TVPGuidelineVanishPoint3(**guideline) + + +def tv_guideline_add_field_chart() -> int: + """Set info for the image guideline at the given position.""" + return int( + send_cmd( + "tv_GuidelineAdd", + "fieldchart", + error_values=[-1, -2], + ) + ) + + +def tv_guideline_add_animator_chart() -> int: + """Set info for the image guideline at the given position.""" + return int( + send_cmd( + "tv_GuidelineAdd", + "animatorfield", + error_values=[-1, -2], + ) + ) diff --git a/pytvpaint/guideline.py b/pytvpaint/guideline.py index adfe434..60005c9 100644 --- a/pytvpaint/guideline.py +++ b/pytvpaint/guideline.py @@ -239,6 +239,47 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_line_get(self._position) + @refreshed_property + def x(self) -> float: + """The x coordinate of the image.""" + return self._data.x + + @x.setter + def x(self, value: float) -> None: + george.tv_guideline_modify_line_set(self.position, x=value) + + @refreshed_property + def y(self) -> float: + """The y coordinate of the image.""" + return self._data.y + + @y.setter + def y(self, value: float) -> None: + george.tv_guideline_modify_line_set(self.position, y=value) + + @refreshed_property + def angle(self) -> float: + """The rotation of the image.""" + return self._data.angle + + @angle.setter + def angle(self, value: float) -> None: + george.tv_guideline_modify_line_set(self.position, angle=value) + + @classmethod + def new( + cls, + project: Project, + x: float | None = None, + y: float | None = None, + angle: float | None = None, + ) -> GuidelineLine: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_line(x, y, angle) + return cls(position, project) + class GuidelineSegment(Guideline[george.TVPGuidelineSegment, george.GuidelineType]): @@ -260,6 +301,57 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_segment_get(self._position) + @refreshed_property + def x1(self) -> float: + """The x coordinate of the image.""" + return self._data.x1 + + @x1.setter + def x1(self, value: float) -> None: + george.tv_guideline_modify_segment_set(self.position, x1=value) + + @refreshed_property + def y1(self) -> float: + """The y coordinate of the image.""" + return self._data.y1 + + @y1.setter + def y1(self, value: float) -> None: + george.tv_guideline_modify_segment_set(self.position, y1=value) + + @refreshed_property + def x2(self) -> float: + """The x coordinate of the image.""" + return self._data.x2 + + @x2.setter + def x2(self, value: float) -> None: + george.tv_guideline_modify_segment_set(self.position, x2=value) + + @refreshed_property + def y2(self) -> float: + """The y coordinate of the image.""" + return self._data.y2 + + @y2.setter + def y2(self, value: float) -> None: + george.tv_guideline_modify_segment_set(self.position, y2=value) + + @classmethod + def new( + cls, + project: Project, + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + ) -> GuidelineSegment: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_segment(x1, y1, x2, y2) + return cls(position, project) + class GuidelineCircle(Guideline[george.TVPGuidelineCircle, george.GuidelineType]): @@ -281,6 +373,47 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_circle_get(self._position) + @refreshed_property + def x(self) -> float: + """The x coordinate of the image.""" + return self._data.x + + @x.setter + def x(self, value: float) -> None: + george.tv_guideline_modify_circle_set(self.position, x=value) + + @refreshed_property + def y(self) -> float: + """The y coordinate of the image.""" + return self._data.y + + @y.setter + def y(self, value: float) -> None: + george.tv_guideline_modify_circle_set(self.position, y=value) + + @refreshed_property + def radius(self) -> float: + """The rotation of the image.""" + return self._data.radius + + @radius.setter + def radius(self, value: float) -> None: + george.tv_guideline_modify_circle_set(self.position, radius=value) + + @classmethod + def new( + cls, + project: Project, + x: float | None = None, + y: float | None = None, + radius: float | None = None, + ) -> GuidelineCircle: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_circle(x, y, radius) + return cls(position, project) + class GuidelineEllipse(Guideline[george.TVPGuidelineEllipse, george.GuidelineType]): @@ -302,6 +435,57 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_ellipse_get(self._position) + @refreshed_property + def x(self) -> float: + """The x coordinate of the image.""" + return self._data.x + + @x.setter + def x(self, value: float) -> None: + george.tv_guideline_modify_ellipse_set(self.position, x=value) + + @refreshed_property + def y(self) -> float: + """The y coordinate of the image.""" + return self._data.y + + @y.setter + def y(self, value: float) -> None: + george.tv_guideline_modify_ellipse_set(self.position, y=value) + + @refreshed_property + def radius_a(self) -> float: + """The rotation of the image.""" + return self._data.radius_a + + @radius_a.setter + def radius_a(self, value: float) -> None: + george.tv_guideline_modify_ellipse_set(self.position, radius_a=value) + + @refreshed_property + def radius_b(self) -> float: + """The rotation of the image.""" + return self._data.radius_b + + @radius_b.setter + def radius_b(self, value: float) -> None: + george.tv_guideline_modify_ellipse_set(self.position, radius_b=value) + + @classmethod + def new( + cls, + project: Project, + x: float | None = None, + y: float | None = None, + radius_a: float | None = None, + radius_b: float | None = None, + ) -> GuidelineEllipse: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_ellipse(x, y, radius_a, radius_b) + return cls(position, project) + class GuidelineGrid(Guideline[george.TVPGuidelineGrid, george.GuidelineType]): @@ -323,6 +507,57 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_grid_get(self._position) + @refreshed_property + def x(self) -> float: + """The x coordinate of the image.""" + return self._data.x + + @x.setter + def x(self, value: float) -> None: + george.tv_guideline_modify_grid_set(self.position, x=value) + + @refreshed_property + def y(self) -> float: + """The y coordinate of the image.""" + return self._data.y + + @y.setter + def y(self, value: float) -> None: + george.tv_guideline_modify_grid_set(self.position, y=value) + + @refreshed_property + def width(self) -> float: + """The rotation of the image.""" + return self._data.w + + @width.setter + def width(self, value: float) -> None: + george.tv_guideline_modify_grid_set(self.position, width=value) + + @refreshed_property + def height(self) -> float: + """The rotation of the image.""" + return self._data.height + + @height.setter + def height(self, value: float) -> None: + george.tv_guideline_modify_grid_set(self.position, height=value) + + @classmethod + def new( + cls, + project: Project, + x: float | None = None, + y: float | None = None, + width: float | None = None, + height: float | None = None, + ) -> GuidelineGrid: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_grid(x, y, width, height) + return cls(position, project) + class GuidelineMarks(Guideline[george.TVPGuidelineMarks, george.GuidelineType]): @@ -344,6 +579,37 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_marks_get(self._position) + @refreshed_property + def count_x(self) -> float: + """The x coordinate of the image.""" + return self._data.count_x + + @count_x.setter + def count_x(self, value: float) -> None: + george.tv_guideline_modify_marks_set(self.position, count_x=value) + + @refreshed_property + def count_y(self) -> float: + """The y coordinate of the image.""" + return self._data.count_y + + @count_y.setter + def count_y(self, value: float) -> None: + george.tv_guideline_modify_marks_set(self.position, count_y=value) + + @classmethod + def new( + cls, + project: Project, + count_x: int | None = None, + count_y: int | None = None, + ) -> GuidelineMarks: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_marks(count_x, count_y) + return cls(position, project) + class GuidelineFieldChart(Guideline[george.TVPGuideField, george.GuidelineType]): @@ -357,6 +623,14 @@ def __init__( ) -> None: super().__init__(position, project, data) + @classmethod + def new(cls, project: Project) -> GuidelineFieldChart: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_field_chart() + return cls(position, project) + class GuidelineAnimatorField(Guideline[george.TVPGuideField, george.GuidelineType]): @@ -370,6 +644,14 @@ def __init__( ) -> None: super().__init__(position, project, data) + @classmethod + def new(cls, project: Project) -> GuidelineAnimatorField: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_animator_chart() + return cls(position, project) + class GuidelineSafeArea(Guideline[george.TVPGuidelineSafeArea, george.GuidelineType]): @@ -391,6 +673,37 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_safe_area_get(self._position) + @refreshed_property + def sf_out(self) -> float: + """The x coordinate of the image.""" + return self._data.sf_out + + @sf_out.setter + def sf_out(self, value: float) -> None: + george.tv_guideline_modify_safe_area_set(self.position, sf_out=value) + + @refreshed_property + def sf_in(self) -> float: + """The y coordinate of the image.""" + return self._data.sf_in + + @sf_in.setter + def sf_in(self, value: float) -> None: + george.tv_guideline_modify_safe_area_set(self.position, sf_in=value) + + @classmethod + def new( + cls, + project: Project, + sf_out: int | None = None, + sf_in: int | None = None, + ) -> GuidelineSafeArea: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_safe_area(sf_out, sf_in) + return cls(position, project) + class GuidelineVanishPoint1(Guideline[george.TVPGuidelineVanishPoint1, george.GuidelineType]): @@ -412,6 +725,56 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_vanish_point_1_get(self._position) + @refreshed_property + def x(self) -> float: + """The x coordinate of the image.""" + return self._data.x + + @x.setter + def x(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_1_set(self.position, x=value) + + @refreshed_property + def y(self) -> float: + """The y coordinate of the image.""" + return self._data.y + + @y.setter + def y(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_1_set(self.position, y=value) + + @refreshed_property + def ray(self) -> int: + """The rotation of the image.""" + return self._data.grid + + @ray.setter + def ray(self, value: int) -> None: + george.tv_guideline_modify_vanish_point_1_set(self.position, ray=value) + + @refreshed_property + def grid(self) -> bool: + """The rotation of the image.""" + return self._data.grid + + @grid.setter + def grid(self, value: bool) -> None: + george.tv_guideline_modify_vanish_point_1_set(self.position, grid=value) + + @classmethod + def new( + cls, + project: Project, + x: float | None = None, + y: float | None = None, + grid: bool | None = None, + ) -> GuidelineVanishPoint1: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_vanish_point_1(x, y, grid) + return cls(position, project) + class GuidelineVanishPoint2(Guideline[george.TVPGuidelineVanishPoint2, george.GuidelineType]): @@ -433,8 +796,59 @@ def refresh(self) -> None: return self._data = george.tv_guideline_modify_vanish_point_2_get(self._position) + @refreshed_property + def x1(self) -> float: + """The x coordinate of the image.""" + return self._data.x1 -class GuidelineVanishPoint3(Guideline[george.TVPGuidelineVanishPoint3, george.GuidelineType]): + @x1.setter + def x1(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_2_set(self.position, x1=value) + + @refreshed_property + def y1(self) -> float: + """The y coordinate of the image.""" + return self._data.y1 + + @y1.setter + def y1(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_2_set(self.position, y1=value) + + @refreshed_property + def x2(self) -> float: + """The x coordinate of the image.""" + return self._data.x2 + + @x2.setter + def x2(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_2_set(self.position, x2=value) + + @refreshed_property + def y2(self) -> float: + """The y coordinate of the image.""" + return self._data.y2 + + @y2.setter + def y2(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_2_set(self.position, y2=value) + + @classmethod + def new( + cls, + project: Project, + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + ) -> GuidelineVanishPoint2: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_vanish_point_2(x1, y1, x2, y2) + return cls(position, project) + + +class GuidelineVanishPoint3(GuidelineVanishPoint2[george.TVPGuidelineVanishPoint3, george.GuidelineType]): TYPE = george.GuidelineType.VANISH_POINT_3 @@ -453,3 +867,29 @@ def refresh(self) -> None: if not self.refresh_on_call and self._data: return self._data = george.tv_guideline_modify_vanish_point_3_get(self._position) + + @refreshed_property + def y3(self) -> float: + """The y coordinate of the image.""" + return self._data.y3 + + @y3.setter + def y3(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_3_set(self.position, y3=value) + + @classmethod + def new( + cls, + project: Project, + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + x3: float | None = None, + y3: float | None = None, + ) -> GuidelineVanishPoint3: + """Create a new guideline in the project.""" + project.make_current() + + position = george.tv_guideline_add_vanish_point_3(x1, y1, x2, y2, x3, y3) + return cls(position, project) diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index 08e1f92..590961a 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -343,6 +343,7 @@ def refresh(self) -> None: super().refresh() if not self.refresh_on_call and self._data: return + try: self._data = george.tv_layer_info(self._id) except GeorgeError: diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index aaa590f..6bd23a7 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -67,6 +67,8 @@ def __init__(self) -> None: def __getattribute__(self, name: str) -> Any: """For each attribute access, we check if the object was marked removed.""" if not name.startswith("_") and self._is_removed: + if name in ["_is_removed", "is_removed"]: + return self._is_removed raise ValueError(f"{self.__class__.__name__} has been removed!") return super().__getattribute__(name) @@ -87,10 +89,11 @@ def is_removed(self) -> bool: Returns: bool: whether if it was removed or not """ - self._is_removed = False + is_removed = True with contextlib.suppress(Exception): self.refresh() - self._is_removed = True + is_removed = False + self._is_removed = is_removed return self._is_removed @abstractmethod From de0d2945f755e020d243997803c1584292954370 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 24 Feb 2026 13:20:36 +0100 Subject: [PATCH 10/26] HOTFIX GuidelineVanishPoint3 inheritance --- pytvpaint/guideline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytvpaint/guideline.py b/pytvpaint/guideline.py index 60005c9..b1b238c 100644 --- a/pytvpaint/guideline.py +++ b/pytvpaint/guideline.py @@ -848,7 +848,7 @@ def new( return cls(position, project) -class GuidelineVanishPoint3(GuidelineVanishPoint2[george.TVPGuidelineVanishPoint3, george.GuidelineType]): +class GuidelineVanishPoint3(GuidelineVanishPoint2): TYPE = george.GuidelineType.VANISH_POINT_3 From 2369b3fd4a890ff1d8d20a027946f53260d20294 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 10 Mar 2026 18:42:47 +0100 Subject: [PATCH 11/26] UPDATE layer.render_instances sub render function * Also updated documentation and fixed some typos --- pytvpaint/clip.py | 24 ++++++++++++------------ pytvpaint/george/grg_base.py | 2 ++ pytvpaint/layer.py | 22 ++++++++++------------ pytvpaint/project.py | 4 ++-- pytvpaint/scene.py | 10 +++------- pytvpaint/sound.py | 6 ++---- pytvpaint/utils.py | 1 + 7 files changed, 32 insertions(+), 37 deletions(-) diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 9835858..3ad5566 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -331,7 +331,7 @@ def remove(self) -> None: @property @set_as_current def layer_ids(self) -> Iterator[int]: - """Iterator over the layer ids.""" + """Returns an iterator over the layer ids.""" return utils.position_generator(lambda pos: george.tv_layer_get_id(pos)) def get_layers( @@ -339,7 +339,7 @@ def get_layers( filter_types: tuple[type, ...] | None = None, ignore_types: tuple[type, ...] | None = None, ) -> Iterator[Layer]: - """Iterator over the clip's layers. + """Returns an iterator over the clip's layers. Args: filter_types: list of layer types to return, default is None, meaning all. @@ -370,7 +370,7 @@ def get_layers( @property @george.deprecated_warning(msg="use `Clip.get_layers()` instead.") def layers(self) -> Iterator[Layer]: - """Iterator over the clip's animation layers, ignores all Folder, Camera and CTG layers. + """Returns an iterator over the clip's animation layers, excluding all Folder, Camera and CTG layers. Warning: DEPRECATED: use `Clip.get_layers()` instead. @@ -379,13 +379,13 @@ def layers(self) -> Iterator[Layer]: @property def anim_layers(self) -> Iterator[Layer]: - """Iterator over the clip's animation layers, ignores all Folder, Camera and CTG layers.""" + """Returns an iterator over the clip's animation layers, excluding all Folder, Camera and CTG layers.""" yield from self.get_layers(ignore_types=(LayerFolder, CameraLayer, CTGLayer)) @property @george.min_version_compatible(min_version="12") def ctg_layers(self) -> Iterator[CTGLayer]: - """Iterator over the clip's CTG layers. + """Returns an iterator over the clip's CTG layers. Note: This function is only available in TVPaint version 12 and above. @@ -399,7 +399,7 @@ def ctg_layers(self) -> Iterator[CTGLayer]: @property @george.min_version_compatible(min_version="12") def folders(self) -> Iterator[LayerFolder]: - """Iterator over the clip's Folder layers. + """Returns an iterator over the clip's Folder layers. Note: This function is only available in TVPaint version 12 and above. @@ -413,7 +413,7 @@ def folders(self) -> Iterator[LayerFolder]: @property @george.min_version_compatible(min_version="12") def camera_layer(self) -> CameraLayer | None: - """Iterator over the clip's Folder layers. + """Returns the clip's Camera layer. Note: This function is only available in TVPaint version 12 and above. @@ -432,7 +432,7 @@ def camera_layer(self) -> CameraLayer | None: @property @set_as_current def layer_names(self) -> Iterator[str]: - """Iterator over the clip's layer names.""" + """Returns an iterator over the clip's layer names.""" for layer in self.get_layers(): yield layer.name @@ -489,12 +489,12 @@ def add_layer_folder(self, folder_name: str) -> LayerFolder: @property def selected_layers(self) -> Iterator[Layer]: - """Iterator over the selected layers.""" + """Returns an iterator over the selected layers.""" yield from (layer for layer in self.get_layers() if layer.is_selected) @property def visible_layers(self) -> Iterator[Layer]: - """Iterator over the visible layers.""" + """Returns an iterator over the visible layers.""" yield from (layer for layer in self.get_layers() if layer.is_visible) @set_as_current @@ -931,7 +931,7 @@ def mark_out(self, value: int | None) -> None: @property def layer_colors(self) -> Iterator[LayerColor]: - """Iterator over the layer colors.""" + """Returns an iterator over the layer colors.""" for color_index in range(26): yield LayerColor(color_index=color_index, clip=self) @@ -982,7 +982,7 @@ def get_layer_color( @property def bookmarks(self) -> Iterator[int]: - """Iterator over the clip bookmarks.""" + """Returns an iterator over the clip bookmarks.""" bookmarks_iter = utils.position_generator(lambda pos: george.tv_bookmarks_enum(pos)) project_start_frame = self.project.start_frame return (frame + project_start_frame for frame in bookmarks_iter) diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index 926f9e4..727090a 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -9,6 +9,7 @@ from enum import Enum from pathlib import Path from typing import Any, Callable, TypeVar, cast, overload +import warnings from packaging import version from typing_extensions import Literal, TypeAlias @@ -699,6 +700,7 @@ def deprecated_warning(msg: str) -> Callable[[T], T]: def decorate(func: T) -> T: @functools.wraps(func) def applicator(*args: Any, **kwargs: Any) -> Any: + warnings.warn(msg, DeprecationWarning) log.warning(f"DEPRECTED: {msg}") return func(*args, **kwargs) diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index 590961a..f6c0cf0 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -978,18 +978,16 @@ def render_frame( frame = frame or self.clip.current_frame self.clip.current_frame = frame - with utils.render_context( - alpha_mode, - background_mode, - save_format, - format_opts, + self.clip.render( + output_path=output_path, + start=frame, + end=frame, + use_camera=use_camera, layer_selection=[self], - ): - george.tv_save_image(export_path) - - if not export_path.exists(): - raise FileNotFoundError(f"Could not find rendered image ({frame}) at : {export_path.as_posix()}") - + alpha_mode=alpha_mode, + background_mode=background_mode, + format_opts=format_opts, + ) return export_path @set_as_current @@ -1106,7 +1104,7 @@ def remove_mark(self, frame: int) -> None: @property def marks(self) -> Iterator[tuple[int, LayerColor]]: - """Iterator over the layer marks including the frame and the color. + """Returns an iterator over the layer marks including the frame and the color. Yields: frame (int): the mark frame diff --git a/pytvpaint/project.py b/pytvpaint/project.py index eda62e5..d8a9ad1 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -447,7 +447,7 @@ def add_clip(self, clip_name: str, scene: Scene | None = None) -> Clip: @property def sounds(self) -> Iterator[ProjectSound]: - """Iterator over the project sounds.""" + """Returns an iterator over the project sounds.""" sounds_data_iter = utils.position_generator(lambda pos: george.tv_sound_project_info(self.id, pos)) for track_index, _ in enumerate(sounds_data_iter): @@ -614,7 +614,7 @@ def open_projects_ids() -> Iterator[str]: @classmethod def open_projects(cls) -> Iterator[Project]: - """Iterator over the currently open projects.""" + """Returns an iterator over the currently open projects.""" for project_id in Project.open_projects_ids(): yield Project(project_id) diff --git a/pytvpaint/scene.py b/pytvpaint/scene.py index 72b368f..574ca3c 100644 --- a/pytvpaint/scene.py +++ b/pytvpaint/scene.py @@ -46,9 +46,7 @@ def current_scene() -> Scene: ) @classmethod - def new( - cls, project: Project | None = None, clips: list[str] | None = None - ) -> Scene: + def new(cls, project: Project | None = None, clips: list[str] | None = None) -> Scene: """Creates a new scene in the provided project. Args: @@ -129,10 +127,8 @@ def position(self, value: int) -> None: @property @set_as_current def clip_ids(self) -> Iterator[int]: - """Returns an iterator over the clip ids.""" - return utils.position_generator( - lambda pos: george.tv_clip_enum_id(self.id, pos) - ) + """Returns an Returns an iterator over the clip ids.""" + return utils.position_generator(lambda pos: george.tv_clip_enum_id(self.id, pos)) @property def clips(self) -> Iterator[Clip]: diff --git a/pytvpaint/sound.py b/pytvpaint/sound.py index 677572c..3f5c2a8 100644 --- a/pytvpaint/sound.py +++ b/pytvpaint/sound.py @@ -65,10 +65,8 @@ def reload(self) -> None: @classmethod def iter_sounds_data(cls, parent_id: str | int) -> Iterator[george.TVPSound]: - """Iterator over the sound's data.""" - return utils.position_generator( - lambda track_index: cls._info(parent_id, track_index) - ) + """Returns an iterator over the sound's data.""" + return utils.position_generator(lambda track_index: cls._info(parent_id, track_index)) def make_current(self) -> None: """Make the parent object current (there's no way to make a sound current).""" diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index 6bd23a7..e0544b6 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -139,6 +139,7 @@ def _render( default_end: int, start: int | None = None, end: int | None = None, + frameset: FrameSet | None = None, use_camera: bool = False, layer_selection: list[Layer] | None = None, alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, From a45639d385f39c0bfd3fb8e1ac009c6c73cec830 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 24 Mar 2026 10:06:46 +0100 Subject: [PATCH 12/26] UPDATE unit tests and clean code --- .github/workflows/check.yml | 19 +- .github/workflows/docs-deploy.yml | 29 +- .github/workflows/pypi.yml | 18 +- .gitignore | 5 +- docs/api/george/guideline.md | 3 + docs/api/objects/guideline.md | 3 + docs/api/objects/guideline_animatorfield.md | 3 + docs/api/objects/guideline_circle.md | 3 + docs/api/objects/guideline_ellipse.md | 3 + docs/api/objects/guideline_fieldchart.md | 3 + docs/api/objects/guideline_grid.md | 3 + docs/api/objects/guideline_image.md | 3 + docs/api/objects/guideline_marks.md | 3 + docs/api/objects/guideline_safearea.md | 3 + docs/api/objects/guideline_segment.md | 3 + docs/api/objects/guideline_vanishpoint1.md | 3 + docs/api/objects/guideline_vanishpoint2.md | 3 + docs/api/objects/guideline_vanishpoint3.md | 3 + docs/api/objects/layer.md | 2 +- docs/api/objects/layer_camera.md | 3 + docs/api/objects/layer_ctg.md | 3 + docs/api/objects/layer_folder.md | 3 + docs/contributing/developer_setup.md | 27 +- docs/cpp/building.md | 4 +- docs/limitations.md | 30 +- mkdocs.yml | 22 +- poetry.lock | 1182 ------------ pyproject.toml | 106 +- pytvpaint/camera.py | 18 +- pytvpaint/clip.py | 61 +- pytvpaint/george/client/__init__.py | 6 +- pytvpaint/george/client/parse.py | 263 +-- pytvpaint/george/client/rpc.py | 12 +- pytvpaint/george/exceptions.py | 4 +- pytvpaint/george/grg_base.py | 27 +- pytvpaint/george/grg_camera.py | 6 +- pytvpaint/george/grg_clip.py | 47 +- pytvpaint/george/grg_guideline.py | 222 +-- pytvpaint/george/grg_layer.py | 104 +- pytvpaint/george/grg_project.py | 4 +- pytvpaint/george/grg_scene.py | 2 +- pytvpaint/guideline.py | 261 ++- pytvpaint/layer.py | 58 +- pytvpaint/project.py | 208 ++- pytvpaint/utils.py | 51 +- tests/conftest.py | 227 +-- tests/george/client/test_parse.py | 227 +-- tests/george/client/test_rpc.py | 8 +- tests/george/test_grg_base.py | 233 +-- tests/george/test_grg_camera.py | 100 +- tests/george/test_grg_clip.py | 890 ++++----- tests/george/test_grg_guideline.py | 281 +++ tests/george/test_grg_layer.py | 1870 +++++++++---------- tests/george/test_grg_project.py | 1082 +++++------ tests/george/test_grg_scene.py | 203 +- tests/george/test_utils.py | 22 +- tests/test_camera.py | 178 +- tests/test_clip.py | 1060 ++++++----- tests/test_clip_sound.py | 42 +- tests/test_guideline.py | 190 ++ tests/test_layer.py | 762 ++++---- tests/test_layer_instance.py | 68 +- tests/test_project.py | 955 +++++----- tests/test_project_sound.py | 42 +- tests/test_scene.py | 176 +- tests/test_utils.py | 50 +- 66 files changed, 5475 insertions(+), 6040 deletions(-) create mode 100644 docs/api/george/guideline.md create mode 100644 docs/api/objects/guideline.md create mode 100644 docs/api/objects/guideline_animatorfield.md create mode 100644 docs/api/objects/guideline_circle.md create mode 100644 docs/api/objects/guideline_ellipse.md create mode 100644 docs/api/objects/guideline_fieldchart.md create mode 100644 docs/api/objects/guideline_grid.md create mode 100644 docs/api/objects/guideline_image.md create mode 100644 docs/api/objects/guideline_marks.md create mode 100644 docs/api/objects/guideline_safearea.md create mode 100644 docs/api/objects/guideline_segment.md create mode 100644 docs/api/objects/guideline_vanishpoint1.md create mode 100644 docs/api/objects/guideline_vanishpoint2.md create mode 100644 docs/api/objects/guideline_vanishpoint3.md create mode 100644 docs/api/objects/layer_camera.md create mode 100644 docs/api/objects/layer_ctg.md create mode 100644 docs/api/objects/layer_folder.md delete mode 100644 poetry.lock create mode 100644 tests/george/test_grg_guideline.py create mode 100644 tests/test_guideline.py diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e4cf4df..d75ca65 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -7,27 +7,24 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 with: - python-version: 3.9 + fetch-depth: 0 - - name: Install Poetry Action - uses: snok/install-poetry@v1.3.4 + - uses: actions/setup-python@v5 with: - version: 1.7.0 + python-version: "3.9" - - name: Installing dependencies... - run: poetry install --no-interaction --with dev,test + - name: Install Hatch + uses: pypa/hatch@install - name: Formatting check if: always() - run: poetry run black --check . + run: hatch run black --check . - name: Linting if: always() - run: poetry run ruff . + run: hatch run ruff check . - name: Type checking if: always() - run: poetry run mypy . + run: hatch run mypy . \ No newline at end of file diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 2d7ec71..2229f29 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -6,6 +6,9 @@ on: - main tags: - "*" + pull_request: + branches: + - main permissions: contents: write @@ -15,6 +18,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Configure Git Credentials run: | @@ -23,12 +28,10 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.9" - - name: Install Poetry Action - uses: snok/install-poetry@v1.3.4 - with: - version: 1.7.0 + - name: Install Hatch + uses: pypa/hatch@install - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV @@ -39,5 +42,17 @@ jobs: restore-keys: | mkdocs-material- - - run: poetry install --no-interaction --with docs - - run: poetry run mkdocs gh-deploy --force + # Build the docs on PRs + - name: Verify Docs Build (on Pull Requests) + if: github.event_name == 'pull_request' + run: hatch run docs:mkdocs build --strict + + # Deploy Dev Documentation (on main) + - name: Deploy Dev Documentation (on main) + if: github.ref == 'refs/heads/main' + run: hatch run docs:mike deploy dev --push + + # Deploy Release Documentation (on tags) + - name: Deploy Release Documentation (on tags) + if: startsWith(github.ref, 'refs/tags/') + run: hatch run docs:mike deploy ${{ github.ref_name }} latest --update-aliases --push \ No newline at end of file diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 2e0ccc9..9d83777 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -9,15 +9,21 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.9" - - name: Install Poetry Action - uses: snok/install-poetry@v1.3.4 - with: - version: 1.7.0 + - name: Install Hatch + uses: pypa/hatch@install + + - name: Build package + run: hatch build - name: Publish to PyPi - run: poetry publish --build --username __token__ --password ${{ secrets.PYPI_TOKEN }} + env: + HATCH_INDEX_USER: __token__ + HATCH_INDEX_AUTH: ${{ secrets.PYPI_TOKEN }} + run: hatch publish \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0105e9e..c934d60 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,7 @@ venv.bak/ .idea/ .pytest_cache/ -.vscode \ No newline at end of file +.vscode + +# hatch version +pytvpaint/_version.py \ No newline at end of file diff --git a/docs/api/george/guideline.md b/docs/api/george/guideline.md new file mode 100644 index 0000000..a1c1208 --- /dev/null +++ b/docs/api/george/guideline.md @@ -0,0 +1,3 @@ +# Guideline related George functions + +::: pytvpaint.george.grg_guideline \ No newline at end of file diff --git a/docs/api/objects/guideline.md b/docs/api/objects/guideline.md new file mode 100644 index 0000000..32d5e96 --- /dev/null +++ b/docs/api/objects/guideline.md @@ -0,0 +1,3 @@ +# Guideline class + +::: pytvpaint.guideline.Guideline \ No newline at end of file diff --git a/docs/api/objects/guideline_animatorfield.md b/docs/api/objects/guideline_animatorfield.md new file mode 100644 index 0000000..be6a856 --- /dev/null +++ b/docs/api/objects/guideline_animatorfield.md @@ -0,0 +1,3 @@ +# GuidelineAnimatorField class + +::: pytvpaint.guideline.GuidelineAnimatorField \ No newline at end of file diff --git a/docs/api/objects/guideline_circle.md b/docs/api/objects/guideline_circle.md new file mode 100644 index 0000000..e58a18b --- /dev/null +++ b/docs/api/objects/guideline_circle.md @@ -0,0 +1,3 @@ +# GuidelineCircle class + +::: pytvpaint.guideline.GuidelineCircle \ No newline at end of file diff --git a/docs/api/objects/guideline_ellipse.md b/docs/api/objects/guideline_ellipse.md new file mode 100644 index 0000000..15a81a0 --- /dev/null +++ b/docs/api/objects/guideline_ellipse.md @@ -0,0 +1,3 @@ +# GuidelineEllipse class + +::: pytvpaint.guideline.GuidelineEllipse \ No newline at end of file diff --git a/docs/api/objects/guideline_fieldchart.md b/docs/api/objects/guideline_fieldchart.md new file mode 100644 index 0000000..0186d21 --- /dev/null +++ b/docs/api/objects/guideline_fieldchart.md @@ -0,0 +1,3 @@ +# GuidelineFieldChart class + +::: pytvpaint.guideline.GuidelineFieldChart \ No newline at end of file diff --git a/docs/api/objects/guideline_grid.md b/docs/api/objects/guideline_grid.md new file mode 100644 index 0000000..f3022d6 --- /dev/null +++ b/docs/api/objects/guideline_grid.md @@ -0,0 +1,3 @@ +# GuidelineGrid class + +::: pytvpaint.guideline.GuidelineGrid \ No newline at end of file diff --git a/docs/api/objects/guideline_image.md b/docs/api/objects/guideline_image.md new file mode 100644 index 0000000..fc929da --- /dev/null +++ b/docs/api/objects/guideline_image.md @@ -0,0 +1,3 @@ +# GuidelineImage class + +::: pytvpaint.guideline.GuidelineImage \ No newline at end of file diff --git a/docs/api/objects/guideline_marks.md b/docs/api/objects/guideline_marks.md new file mode 100644 index 0000000..1798f2b --- /dev/null +++ b/docs/api/objects/guideline_marks.md @@ -0,0 +1,3 @@ +# GuidelineMarks class + +::: pytvpaint.guideline.GuidelineMarks \ No newline at end of file diff --git a/docs/api/objects/guideline_safearea.md b/docs/api/objects/guideline_safearea.md new file mode 100644 index 0000000..bcc7edc --- /dev/null +++ b/docs/api/objects/guideline_safearea.md @@ -0,0 +1,3 @@ +# GuidelineSafeArea class + +::: pytvpaint.guideline.GuidelineSafeArea \ No newline at end of file diff --git a/docs/api/objects/guideline_segment.md b/docs/api/objects/guideline_segment.md new file mode 100644 index 0000000..6d27605 --- /dev/null +++ b/docs/api/objects/guideline_segment.md @@ -0,0 +1,3 @@ +# GuidelineSegment class + +::: pytvpaint.guideline.GuidelineSegment \ No newline at end of file diff --git a/docs/api/objects/guideline_vanishpoint1.md b/docs/api/objects/guideline_vanishpoint1.md new file mode 100644 index 0000000..04b9f22 --- /dev/null +++ b/docs/api/objects/guideline_vanishpoint1.md @@ -0,0 +1,3 @@ +# GuidelineVanishPoint1 class + +::: pytvpaint.guideline.GuidelineVanishPoint1 \ No newline at end of file diff --git a/docs/api/objects/guideline_vanishpoint2.md b/docs/api/objects/guideline_vanishpoint2.md new file mode 100644 index 0000000..6d9b819 --- /dev/null +++ b/docs/api/objects/guideline_vanishpoint2.md @@ -0,0 +1,3 @@ +# GuidelineVanishPoint2 class + +::: pytvpaint.guideline.GuidelineVanishPoint2 \ No newline at end of file diff --git a/docs/api/objects/guideline_vanishpoint3.md b/docs/api/objects/guideline_vanishpoint3.md new file mode 100644 index 0000000..2415bd5 --- /dev/null +++ b/docs/api/objects/guideline_vanishpoint3.md @@ -0,0 +1,3 @@ +# GuidelineVanishPoint3 class + +::: pytvpaint.guideline.GuidelineVanishPoint3 \ No newline at end of file diff --git a/docs/api/objects/layer.md b/docs/api/objects/layer.md index 24be782..5080958 100644 --- a/docs/api/objects/layer.md +++ b/docs/api/objects/layer.md @@ -1,3 +1,3 @@ # Layer class -::: pytvpaint.layer.Layer +::: pytvpaint.layer.Layer \ No newline at end of file diff --git a/docs/api/objects/layer_camera.md b/docs/api/objects/layer_camera.md new file mode 100644 index 0000000..30b1817 --- /dev/null +++ b/docs/api/objects/layer_camera.md @@ -0,0 +1,3 @@ +# Camera Layer class + +::: pytvpaint.layer.CameraLayer \ No newline at end of file diff --git a/docs/api/objects/layer_ctg.md b/docs/api/objects/layer_ctg.md new file mode 100644 index 0000000..765df62 --- /dev/null +++ b/docs/api/objects/layer_ctg.md @@ -0,0 +1,3 @@ +# CTG Layer class + +::: pytvpaint.layer.CTGLayer \ No newline at end of file diff --git a/docs/api/objects/layer_folder.md b/docs/api/objects/layer_folder.md new file mode 100644 index 0000000..9ef0902 --- /dev/null +++ b/docs/api/objects/layer_folder.md @@ -0,0 +1,3 @@ +# Layer Folder class + +::: pytvpaint.layer.LayerFolder \ No newline at end of file diff --git a/docs/contributing/developer_setup.md b/docs/contributing/developer_setup.md index b106cb4..ffd5561 100644 --- a/docs/contributing/developer_setup.md +++ b/docs/contributing/developer_setup.md @@ -6,29 +6,34 @@ This guide will explain how to set up your environment in order to contribute to - [Python](https://www.python.org/) 3.9 or greater are the supported versions 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. +- We use [Hatch](https://hatch.pypa.io/) 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 +❯ git clone [https://github.com/brunchstudio/pytvpaint.git](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: +Install Hatch if needed: +```shell +❯ pip install hatch +``` + +Then install the dependencies in a virtualenv with Hatch: ```shell -❯ poetry install +❯ hatch env create ``` -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: +Note that this will install the library and default development dependencies. To install optional [environments](https://hatch.pypa.io/latest/config/environment/overview/) (to build the documentation, etc...) you can specify the environment name: ```shell -❯ poetry install --with dev,docs,test +❯ hatch env create docs ``` ### Code formatting @@ -45,7 +50,7 @@ We use [Black](https://black.readthedocs.io/en/stable/) to ensure that the code !!! 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 ` + Use `hatch shell` to enter a new shell in the virtualenv. In this page commands marked `(venv) ❯` can also be run with `hatch run ` ### Linting @@ -68,7 +73,7 @@ Mypy is the go-to static type checker for Python. It ensures that variables and !!! info - We currently exclude untyped calls for [Fileseq](https://github.com/justinfx/fileseq) and [websocket-client](https://github.com/websocket-client/websocket-client) 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) + We currently exclude untyped calls for [Fileseq](https://github.com/justinfx/fileseq) and [websocket-client](https://github.com/websocket-client/websocket-client)  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 @@ -79,7 +84,7 @@ On top of that we use [Material for MkDocs](https://squidfunk.github.io/mkdocs-m 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 +# Will serve the doc on [http://127.0.0.1:8000](http://127.0.0.1:8000) with hot reload (venv) ❯ mkdocs serve # Build the doc as static files @@ -114,7 +119,7 @@ To run the tests you'll need an open TVPaint instance with the [tvpaint-rpc plug To run them, use the following commands: ```shell -# Will run all the tests +# run all the tests (venv) ❯ pytest # Run with verbosity enabled (use PYTVPAINT_LOG_LEVEL to DEBUG) to see George commands @@ -125,4 +130,4 @@ To run them, use the following commands: # See the coverage statistics with pytest-cov (venv) ❯ pytest --cov=pytvpaint -``` +``` \ No newline at end of file diff --git a/docs/cpp/building.md b/docs/cpp/building.md index ec0fb95..e606bf0 100644 --- a/docs/cpp/building.md +++ b/docs/cpp/building.md @@ -11,8 +11,8 @@ To install the build dependencies, we use [Conan](https://conan.io/) which is a To install it, use the virtualenv provided by [Poetry](https://python-poetry.org/): ```shell -❯ poetry install # Installs Conan -❯ poetry shell # Enter a new venv shell +❯ hatch env create # create the venv +❯ hatch shell # Enter the new venv shell ``` Then configure your Conan compilation profile: diff --git a/docs/limitations.md b/docs/limitations.md index 7f3a673..d980b63 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -6,18 +6,22 @@ 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 being a Windows Studio we have not needed nor had +We are interested in making it available for Linux and MacOS, but being a Windows first Studio we have not needed nor had time to do so yet. If you want to contribute on this, please open an issue or a pull request on the [plugin repository](https://github.com/brunchstudio/tvpaint-rpc/issues). ## TVPaint 11.5+ TVPaint 11.5 is minimum supported version simply because we currently do not have an older version of TVPaint. The TVPaint -SDK is what really limits the compatibility, but we suspect PyTVPaint is also compatible with TVPaint 11 since the SDK +SDK is what really limits the compatibility, but we suspect PyTVPaint is also compatible with TVPaint 11 (and perhaps even 10) since the SDK hasn't changed in a while. If you have a version of TVPaint prior to 11.5 and want to test the API, please let us know if it works and we will update the minimum requirements for the plugin and API. ## Control characters in George results +!!! Info + + The latest version of PyTVPaint tries to fix this via our API, it works 90% of the time but there are still some edge cases that cannot be resolved. + This issue has been reported to TVPaint and is related to how the C++ SDK function `TVSendCmd` returns and encodes [control character](https://en.wikipedia.org/wiki/Control_character) values. If we use the George command [`tv_projectheadernotes`](https://www.tvpaint.com/doc/tvpaint-animation-11/george-commands#tv_projectheadernotes) for instance and the project notes text has some line breaks (`\n`), then the characters in the result buffer are not encoded properly. @@ -97,15 +101,15 @@ issues in the table below: ## TVPaint 12 Bugs and Breaking Changes TVPaint 12 introduces a lot of welcome changes (especially for the artists) and some new needed function for -developers. However it also introduces a lot of breaking changes and some new bugs. +developers. However, it also introduces a lot of breaking changes and some new bugs. -To deal with these changes as well as the new functions exclusive to TVP 12, we added a couple decorators and functions +To deal with these changes as well as the new functions exclusive to TVP 12, we added a couple decorators and helper functions : | Method | Description | |:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| | [`min_version_compatible`](api/george/misc.md#pytvpaint.george.grg_base.min_version_compatible) | When used as decorator, checks if the tvp instance making the call is above the miniumum version needed. | | [`deprecated_warning`](api/george/misc.md#pytvpaint.george.grg_base.deprecated_warning) | When used as decorator, will log a warning message anytime the decorated function is called. | -| [`is_tvp_version_below_12`](api/george/misc.md#pytvpaint.george.grg_base.is_tvp_version_below_12) | Returns a True if the tvp instances version is below 12. | +| [`is_tvp_version_below_12`](api/george/misc.md#pytvpaint.george.grg_base.is_tvp_version_below_12) | Returns True if the tvp instances version is below 12, False otherwise. | Below is also a list of the current breaking changes and bugs we noticed during development @@ -120,28 +124,28 @@ Below is also a list of the current breaking changes and bugs we noticed during * [BUG] [`tv_SceneCreate`](api/george/scene.md#pytvpaint.george.grg_scene.tv_scene_create) doesn't seem to work. ### Layers : -* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually tv_CTGGetSource without the `s` at the end. +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually `tv_CTGGetSource` without the `s` at the end. * [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) actually requires and returns layer Ids not names. * [FEATURE] Some function now return a clear error message which is much appreciated (Ex: [`tv_CTGSource`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_source_add)) * child layers have no way of knowing if they are in a folder layer or which one. -* Folder layer has no way of knowing which layers are its children. +* Folder layer has no way of knowing which layers are inside the folder. * [BUG] Creating a CTG layer from a folder crashes TVPaint. * [BUG] Moving a layer that is already in a folder in the same folder crashes TVPaint. * [`tv_LayerMove`](api/george/layer.md#pytvpaint.george.grg_layer.tv_layer_move) can now move layers into folders but the position is relative to the root and not the folder, - this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers in and out of folders. + this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers. * Not providing a `FolderID` to [`tv_LayerMove`](api/george/layer.md#pytvpaint.george.grg_layer.tv_layer_move) doesn't move the layer to the root, you just need to move outside the folder range for it to work, which again is pretty tough to do since we can't get the child layers of a folder and therefore their positions. * Moving a layer inside a folder can be done without providing a `FolderID`, just by moving the layer in the folder's range. * [BUG] UI doesn't always update when values are set/updated via code, you either have to wait a few seconds for a refresh, or click somewhere else, this might lead to errors when users are using the UI at the same time. * [BUG] CTG layer sometimes takes a while to update in the UI (a few seconds), which means they can be unintentionally edited or reset before the update. * CameraLayer is not really a layer and most layer functions will ignore it, prefer use of PyTVPaint [`Camera`](api/objects/camera.md#pytvpaint.camera.Camera) object instead. -* [BUG] Selecting the Camera Layer in the UI now disables/greys out most layer related tools (this is a new behaviour), this causes a lot of errors when using pipeline tools or - TVPaint panels as TVPaint now raises an error messages anytime you try to use a tool that is not camera related when the camera layer is selected. - The problem is that the Camera layer is now also the default layer and selected by default when creating/opening any project. +* [BUG] Selecting the Camera Layer in the UI now disables/grays out most layer related tools (this is a new behaviour), this causes a lot of errors when using pipeline tools or + TVPaint panels as TVPaint now raises an error prompt/message anytime you try to use a tool that is not camera related when the camera layer is selected. ### Camera : -* [DEPRECATED/BREAKING] Camera.anti_aliasing always returns 1, [`Camera.fps`](api/objects/camera.md#pytvpaint.camera.Camera.fps) no longer returns/sets fps, now only fps is Project fps. -* [BUG] Most Camera values can still be edited/queried using [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) but they are not reflected in +* [DEPRECATED/BREAKING] Camera.anti_aliasing always returns 1 +* [DEPRECATED/BREAKING] [`Camera.fps`](api/objects/camera.md#pytvpaint.camera.Camera.fps) no longer returns/sets fps, now only fps is Project fps. +* [BUG] Most Camera values can still be edited/queried using [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) but they are not immediately reflected in the UI, and so they can be unintentionally edited or reset. * [DEPRECATED/BREAKING] [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) values of pixel aspect ratio and fps have been "swapped" (since camera fps is no longer provided). * [BUG] [`tv_CameraInterpolation`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_interpolation) doesn't work properly in TVP12 and always returns an "empty" point. diff --git a/mkdocs.yml b/mkdocs.yml index 0d1abfa..1392524 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,8 @@ validation: extra: generator: false + version: + provider: mike nav: - Get Started: @@ -61,13 +63,31 @@ nav: - Layer: api/objects/layer.md - LayerInstance: api/objects/layer_instance.md - LayerColor: api/objects/layer_color.md + - LayerFolder: api/objects/layer_folder.md + - CameraLayer: api/objects/layer_camera.md + - CTGLayer: api/objects/layer_ctg.md - Camera: api/objects/camera.md + - Guideline: api/objects/guideline.md + - GuidelineImage: api/objects/guideline_image.md + - GuidelineLine: api/objects/guideline_line.md + - GuidelineSegment: api/objects/guideline_segment.md + - GuidelineCircle: api/objects/guideline_circle.md + - GuidelineEllipse: api/objects/guideline_ellipse.md + - GuidelineGrid: api/objects/guideline_grid.md + - GuidelineMarks: api/objects/guideline_marks.md + - GuidelineSafeArea: api/objects/guideline_safearea.md + - GuidelineFieldChart: api/objects/guideline_fieldchart.md + - GuidelineAnimatorField: api/objects/guideline_animatorfield.md + - GuidelineVanishPoint1: api/objects/guideline_vanishpoint1.md + - GuidelineVanishPoint2: api/objects/guideline_vanishpoint2.md + - GuidelineVanishPoint3: api/objects/guideline_vanishpoint3.md - George: - Project: api/george/project.md - Scene: api/george/scene.md - Clip: api/george/clip.md - Layer: api/george/layer.md - Camera: api/george/camera.md + - Guideline: api/george/guideline.md - Misc: api/george/misc.md - Exceptions: api/george/exceptions.md - Client: @@ -114,4 +134,4 @@ markdown_extensions: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - toc: - permalink: true + permalink: true \ No newline at end of file diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index c897057..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1182 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. - -[[package]] -name = "babel" -version = "2.17.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] - -[[package]] -name = "backrefs" -version = "5.8" -description = "A wrapper around re and regex that adds additional back references." -optional = false -python-versions = ">=3.9" -files = [ - {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, - {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, - {file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"}, - {file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"}, - {file = "backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc"}, - {file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"}, -] - -[package.extras] -extras = ["regex"] - -[[package]] -name = "black" -version = "24.10.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -files = [ - {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, - {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, - {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, - {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, - {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, - {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, - {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, - {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, - {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, - {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, - {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, - {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, - {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, - {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, - {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, - {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, - {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, - {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, - {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, - {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, - {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, - {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2025.1.31" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, -] - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.7.0" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "coverage-7.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a538a23119d1e2e2ce077e902d02ea3d8e0641786ef6e0faf11ce82324743944"}, - {file = "coverage-7.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1586ad158523f4133499a4f322b230e2cfef9cc724820dbd58595a5a236186f4"}, - {file = "coverage-7.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b6c96d69928a3a6767fab8dc1ce8a02cf0156836ccb1e820c7f45a423570d98"}, - {file = "coverage-7.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f18d47641282664276977c604b5a261e51fefc2980f5271d547d706b06a837f"}, - {file = "coverage-7.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a1e18a85bd066c7c556d85277a7adf4651f259b2579113844835ba1a74aafd"}, - {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:70f0925c4e2bfc965369f417e7cc72538fd1ba91639cf1e4ef4b1a6b50439b3b"}, - {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b0fac2088ec4aaeb5468b814bd3ff5e5978364bfbce5e567c44c9e2854469f6c"}, - {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3e212a894d8ae07fde2ca8b43d666a6d49bbbddb10da0f6a74ca7bd31f20054"}, - {file = "coverage-7.7.0-cp310-cp310-win32.whl", hash = "sha256:f32b165bf6dfea0846a9c9c38b7e1d68f313956d60a15cde5d1709fddcaf3bee"}, - {file = "coverage-7.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2454b12a3f12cc4698f3508912e6225ec63682e2ca5a96f80a2b93cef9e63f3"}, - {file = "coverage-7.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0a207c87a9f743c8072d059b4711f8d13c456eb42dac778a7d2e5d4f3c253a7"}, - {file = "coverage-7.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d673e3add00048215c2cc507f1228a7523fd8bf34f279ac98334c9b07bd2656"}, - {file = "coverage-7.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f81fe93dc1b8e5673f33443c0786c14b77e36f1025973b85e07c70353e46882b"}, - {file = "coverage-7.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8c7524779003d59948c51b4fcbf1ca4e27c26a7d75984f63488f3625c328b9b"}, - {file = "coverage-7.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c124025430249118d018dcedc8b7426f39373527c845093132196f2a483b6dd"}, - {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f559c36d5cdc448ee13e7e56ed7b6b5d44a40a511d584d388a0f5d940977ba"}, - {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:37cbc7b0d93dfd133e33c7ec01123fbb90401dce174c3b6661d8d36fb1e30608"}, - {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7d2a65876274acf544703e943c010b60bd79404e3623a1e5d52b64a6e2728de5"}, - {file = "coverage-7.7.0-cp311-cp311-win32.whl", hash = "sha256:f5a2f71d6a91238e7628f23538c26aa464d390cbdedf12ee2a7a0fb92a24482a"}, - {file = "coverage-7.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae8006772c6b0fa53c33747913473e064985dac4d65f77fd2fdc6474e7cd54e4"}, - {file = "coverage-7.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:056d3017ed67e7ddf266e6f57378ece543755a4c9231e997789ab3bd11392c94"}, - {file = "coverage-7.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33c1394d8407e2771547583b66a85d07ed441ff8fae5a4adb4237ad39ece60db"}, - {file = "coverage-7.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fbb7a0c3c21908520149d7751cf5b74eb9b38b54d62997b1e9b3ac19a8ee2fe"}, - {file = "coverage-7.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb356e7ae7c2da13f404bf8f75be90f743c6df8d4607022e759f5d7d89fe83f8"}, - {file = "coverage-7.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce730d484038e97f27ea2dbe5d392ec5c2261f28c319a3bb266f6b213650135"}, - {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa4dff57fc21a575672176d5ab0ef15a927199e775c5e8a3d75162ab2b0c7705"}, - {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b667b91f4f714b17af2a18e220015c941d1cf8b07c17f2160033dbe1e64149f0"}, - {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:693d921621a0c8043bfdc61f7d4df5ea6d22165fe8b807cac21eb80dd94e4bbd"}, - {file = "coverage-7.7.0-cp312-cp312-win32.whl", hash = "sha256:52fc89602cde411a4196c8c6894afb384f2125f34c031774f82a4f2608c59d7d"}, - {file = "coverage-7.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ce8cf59e09d31a4915ff4c3b94c6514af4c84b22c4cc8ad7c3c546a86150a92"}, - {file = "coverage-7.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4545485fef7a8a2d8f30e6f79ce719eb154aab7e44217eb444c1d38239af2072"}, - {file = "coverage-7.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1393e5aa9441dafb0162c36c8506c648b89aea9565b31f6bfa351e66c11bcd82"}, - {file = "coverage-7.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:316f29cc3392fa3912493ee4c83afa4a0e2db04ff69600711f8c03997c39baaa"}, - {file = "coverage-7.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ffde1d6bc2a92f9c9207d1ad808550873748ac2d4d923c815b866baa343b3f"}, - {file = "coverage-7.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:416e2a8845eaff288f97eaf76ab40367deafb9073ffc47bf2a583f26b05e5265"}, - {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5efdeff5f353ed3352c04e6b318ab05c6ce9249c25ed3c2090c6e9cadda1e3b2"}, - {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:57f3bd0d29bf2bd9325c0ff9cc532a175110c4bf8f412c05b2405fd35745266d"}, - {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ab7090f04b12dc6469882ce81244572779d3a4b67eea1c96fb9ecc8c607ef39"}, - {file = "coverage-7.7.0-cp313-cp313-win32.whl", hash = "sha256:180e3fc68ee4dc5af8b33b6ca4e3bb8aa1abe25eedcb958ba5cff7123071af68"}, - {file = "coverage-7.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:55143aa13c49491f5606f05b49ed88663446dce3a4d3c5d77baa4e36a16d3573"}, - {file = "coverage-7.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc41374d2f27d81d6558f8a24e5c114580ffefc197fd43eabd7058182f743322"}, - {file = "coverage-7.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:89078312f06237417adda7c021c33f80f7a6d2db8572a5f6c330d89b080061ce"}, - {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b2f144444879363ea8834cd7b6869d79ac796cb8f864b0cfdde50296cd95816"}, - {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60e6347d1ed882b1159ffea172cb8466ee46c665af4ca397edbf10ff53e9ffaf"}, - {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb203c0afffaf1a8f5b9659a013f8f16a1b2cad3a80a8733ceedc968c0cf4c57"}, - {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ad0edaa97cb983d9f2ff48cadddc3e1fb09f24aa558abeb4dc9a0dbacd12cbb4"}, - {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c5f8a5364fc37b2f172c26a038bc7ec4885f429de4a05fc10fdcb53fb5834c5c"}, - {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4e09534037933bf6eb31d804e72c52ec23219b32c1730f9152feabbd7499463"}, - {file = "coverage-7.7.0-cp313-cp313t-win32.whl", hash = "sha256:1b336d06af14f8da5b1f391e8dec03634daf54dfcb4d1c4fb6d04c09d83cef90"}, - {file = "coverage-7.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b54a1ee4c6f1905a436cbaa04b26626d27925a41cbc3a337e2d3ff7038187f07"}, - {file = "coverage-7.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c8fbce80b2b8bf135d105aa8f5b36eae0c57d702a1cc3ebdea2a6f03f6cdde5"}, - {file = "coverage-7.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9710521f07f526de30ccdead67e6b236fe996d214e1a7fba8b36e2ba2cd8261"}, - {file = "coverage-7.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7789e700f33f2b133adae582c9f437523cd5db8de845774988a58c360fc88253"}, - {file = "coverage-7.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c36093aca722db73633cf2359026ed7782a239eb1c6db2abcff876012dc4cf"}, - {file = "coverage-7.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c075d167a6ec99b798c1fdf6e391a1d5a2d054caffe9593ba0f97e3df2c04f0e"}, - {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d013c07061751ae81861cae6ec3a4fe04e84781b11fd4b6b4201590234b25c7b"}, - {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:104bf640f408f4e115b85110047c7f27377e1a8b7ba86f7db4fa47aa49dc9a8e"}, - {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:39abcacd1ed54e2c33c54bdc488b310e8ef6705833f7148b6eb9a547199d375d"}, - {file = "coverage-7.7.0-cp39-cp39-win32.whl", hash = "sha256:8e336b56301774ace6be0017ff85c3566c556d938359b61b840796a0202f805c"}, - {file = "coverage-7.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:8c938c6ae59be67ac19a7204e079efc94b38222cd7d0269f96e45e18cddeaa59"}, - {file = "coverage-7.7.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:3b0e6e54591ae0d7427def8a4d40fca99df6b899d10354bab73cd5609807261c"}, - {file = "coverage-7.7.0-py3-none-any.whl", hash = "sha256:708f0a1105ef2b11c79ed54ed31f17e6325ac936501fc373f24be3e6a578146a"}, - {file = "coverage-7.7.0.tar.gz", hash = "sha256:cd879d4646055a573775a1cec863d00c9ff8c55860f8b17f6d8eee9140c06166"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fileseq" -version = "2.1.2" -description = "A Python library for parsing frame ranges and file sequences commonly used in VFX and Animation applications." -optional = false -python-versions = "*" -files = [ - {file = "Fileseq-2.1.2-py3-none-any.whl", hash = "sha256:b5d599b0bff72f0b82d31a465b3f6b28ef4d20fa097bb7ff4d1a047efe913932"}, - {file = "fileseq-2.1.2.tar.gz", hash = "sha256:f1d844d62ad017821d0188110d36f3b93ae9996782f0e890cb64419644e469f2"}, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -optional = false -python-versions = "*" -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "griffe" -version = "1.6.0" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.9" -files = [ - {file = "griffe-1.6.0-py3-none-any.whl", hash = "sha256:9f1dfe035d4715a244ed2050dfbceb05b1f470809ed4f6bb10ece5a7302f8dd1"}, - {file = "griffe-1.6.0.tar.gz", hash = "sha256:eb5758088b9c73ad61c7ac014f3cdfb4c57b5c2fcbfca69996584b702aefa354"}, -] - -[package.dependencies] -colorama = ">=0.4" - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.6.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -files = [ - {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, - {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markdown" -version = "3.7" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, - {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -description = "Project documentation with Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, - {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} -jinja2 = ">=2.11.1" -markdown = ">=3.3.6" -markupsafe = ">=2.0.1" -mergedeep = ">=1.3.4" -mkdocs-get-deps = ">=0.2.0" -packaging = ">=20.5" -pathspec = ">=0.11.1" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.1" -description = "Automatically link across pages in MkDocs." -optional = false -python-versions = ">=3.9" -files = [ - {file = "mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f"}, - {file = "mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079"}, -] - -[package.dependencies] -Markdown = ">=3.3" -markupsafe = ">=2.0.1" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, - {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} -mergedeep = ">=1.3.4" -platformdirs = ">=2.2.0" -pyyaml = ">=5.1" - -[[package]] -name = "mkdocs-material" -version = "9.6.8" -description = "Documentation that simply works" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_material-9.6.8-py3-none-any.whl", hash = "sha256:0a51532dd8aa80b232546c073fe3ef60dfaef1b1b12196ac7191ee01702d1cf8"}, - {file = "mkdocs_material-9.6.8.tar.gz", hash = "sha256:8de31bb7566379802532b248bd56d9c4bc834afc4625884bf5769f9412c6a354"}, -] - -[package.dependencies] -babel = ">=2.10,<3.0" -backrefs = ">=5.7.post1,<6.0" -colorama = ">=0.4,<1.0" -jinja2 = ">=3.0,<4.0" -markdown = ">=3.2,<4.0" -mkdocs = ">=1.6,<2.0" -mkdocs-material-extensions = ">=1.3,<2.0" -paginate = ">=0.5,<1.0" -pygments = ">=2.16,<3.0" -pymdown-extensions = ">=10.2,<11.0" -requests = ">=2.26,<3.0" - -[package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -description = "Extension pack for Python Markdown and MkDocs Material." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, - {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, -] - -[[package]] -name = "mkdocstrings" -version = "0.24.3" -description = "Automatic documentation from sources, for MkDocs." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocstrings-0.24.3-py3-none-any.whl", hash = "sha256:5c9cf2a32958cd161d5428699b79c8b0988856b0d4a8c5baf8395fc1bf4087c3"}, - {file = "mkdocstrings-0.24.3.tar.gz", hash = "sha256:f327b234eb8d2551a306735436e157d0a22d45f79963c60a8b585d5f7a94c1d2"}, -] - -[package.dependencies] -click = ">=7.0" -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} -Jinja2 = ">=2.11.1" -Markdown = ">=3.3" -MarkupSafe = ">=1.1" -mkdocs = ">=1.4" -mkdocs-autorefs = ">=0.3.1" -mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} -platformdirs = ">=2.2.0" -pymdown-extensions = ">=6.3" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} - -[package.extras] -crystal = ["mkdocstrings-crystal (>=0.3.4)"] -python = ["mkdocstrings-python (>=0.5.2)"] -python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] - -[[package]] -name = "mkdocstrings-python" -version = "1.10.0" -description = "A Python handler for mkdocstrings." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocstrings_python-1.10.0-py3-none-any.whl", hash = "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc"}, - {file = "mkdocstrings_python-1.10.0.tar.gz", hash = "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828"}, -] - -[package.dependencies] -griffe = ">=0.44" -mkdocstrings = ">=0.24.2" - -[[package]] -name = "mypy" -version = "1.15.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, - {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, - {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, - {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, - {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, - {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, - {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, - {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, - {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, - {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, - {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, - {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, - {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, - {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, - {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, - {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, - {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, - {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "paginate" -version = "0.5.7" -description = "Divides large result sets into pages for easier browsing" -optional = false -python-versions = "*" -files = [ - {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, - {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, -] - -[package.extras] -dev = ["pytest", "tox"] -lint = ["black"] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pymdown-extensions" -version = "10.14.3" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, - {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, -] - -[package.dependencies] -markdown = ">=3.6" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.19.1)"] - -[[package]] -name = "pytest" -version = "8.3.5" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-mock" -version = "3.14.0" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, - {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, -] - -[package.dependencies] -pytest = ">=6.2.5" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "ruff" -version = "0.2.2" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, -] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "watchdog" -version = "6.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.9" -files = [ - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, - {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, - {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, - {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, - {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "zipp" -version = "3.21.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.9" -content-hash = "5d02070af240a1bb5608ea9675687e8fb735b8caaa89faccf55b2a6ecff4ce43" diff --git a/pyproject.toml b/pyproject.toml index 3010d5b..2e0e28e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,57 +1,89 @@ -[tool.poetry] +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] name = "pytvpaint" -version = "1.0.2" +dynamic = ["version"] description = "Python scripting for TVPaint" +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.9" authors = [ - "Brunch Studio Developers ", - "Radouane Lahmidi ", - "Joseph Henry ", + { name = "Brunch Studio Developers", email = "dev@brunchstudio.tv" }, + { name = "Radouane Lahmidi", email = "rlahmidi@brunchstudio.tv" }, + { name = "Joseph Henry", email = "jhenry@brunchstudio.tv" }, ] -license = "MIT" -readme = "README.md" -repository = "https://github.com/brunchstudio/pytvpaint" -documentation = "https://brunchstudio.github.io/pytvpaint/" keywords = ["tvpaint", "brunch", "tvp", "george"] -packages = [{ include = "pytvpaint" }] +dependencies = [ + "websocket-client>=1.7.0", + "typing-extensions>=4.9.0", + "fileseq>=2.0.0", + "packaging>=22.0", +] -[tool.poetry.dependencies] -python = ">=3.9" -websocket-client = "^1.7.0" -typing-extensions = "^4.9.0" -fileseq = "^2.0.0" -packaging = ">=22.0" +[project.urls] +Repository = "https://github.com/brunchstudio/pytvpaint" +Documentation = "https://brunchstudio.github.io/pytvpaint/" -[tool.poetry.group.dev] -optional = true +[project.optional-dependencies] +dev = [ + "black>=24.2.0", + "ruff>=0.2.2", + "mypy>=1.9.0", +] +test = [ + "pytest>=7.0.0", + "pytest-mock>=3.0.0", + "pytest-cov>=4.0.0", + "pytest-html>=4.0.0", +] +docs = [ + "mkdocs-material>=9.5.10", + "mkdocstrings[python]>=0.24.0", + "mike", +] -[tool.poetry.group.dev.dependencies] -black = "^24.2.0" -ruff = "^0.2.2" -mypy = "^1.9.0" +[tool.hatch.version] +source = "vcs" -[tool.poetry.group.test] -optional = true +[tool.hatch.build.hooks.vcs] +version-file = "pytvpaint/_version.py" -[tool.poetry.group.test.dependencies] -pytest = "^8.0.1" -pytest-mock = "^3.12.0" -pytest-cov = "^4.1.0" +[tool.hatch.build.targets.sdist] +exclude = ["/.github"] -[tool.poetry.group.docs] -optional = true +[tool.hatch.envs.default] +features = ["dev", "test"] -[tool.poetry.group.docs.dependencies] -mkdocs-material = "^9.5.10" -mkdocstrings = { extras = ["python"], version = "^0.24.0" } +[tool.hatch.envs.docs] +features = ["docs"] -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.hatch.envs.docs.scripts] +deploy-dev = "mike deploy dev --push" +deploy-release = "mike deploy {args} latest --update-aliases --push" + +[tool.pytest.ini_options] +addopts = [ + "--cov=pytvpaint", + "--durations=5", + "--failed-first", + "--new-first", + "--html=tests/reports/test_report.html", + "--self-contained-html", + "-vv", + "--maxfail=1" +] +cache_dir = "tests/.pytest_cache" +log_cli = true +required_plugins = ["pytest-cov", "pytest-html"] [tool.black] +line-length = 120 target-version = ["py39", "py310", "py311", "py312"] [tool.ruff] +line-length = 120 target-version = "py39" [tool.ruff.lint] @@ -103,4 +135,4 @@ warn_return_any = true warn_unused_configs = true warn_unused_ignores = true extra_checks = true -untyped_calls_exclude = "fileseq,websocket" +untyped_calls_exclude = "fileseq,websocket" \ No newline at end of file diff --git a/pytvpaint/camera.py b/pytvpaint/camera.py index 9a9e6a5..c2ffd44 100644 --- a/pytvpaint/camera.py +++ b/pytvpaint/camera.py @@ -190,7 +190,7 @@ def points(self) -> Iterator[CameraPoint]: @set_as_current def get_point_data_at(self, position: float) -> InterpolationCameraPoint: - """Get the points data interpolated at that position (between 0 and 1).""" + """Get the points data interpolated at the provided position (between 0 and 1).""" return InterpolationCameraPoint(position, self, InterpolationCameraPoint.get_point_data_at(position)) @george.min_version_compatible(min_version="12") @@ -201,7 +201,7 @@ def get_point_data_at_frame(self, frame: int) -> FrameCameraPoint: @set_as_current def remove_point(self, index: int) -> None: - """Remove a point at that index.""" + """Remove a point at the provided index.""" try: point = next(p for i, p in enumerate(self.points) if i == index) point.remove() @@ -294,7 +294,7 @@ def y(self, value: float) -> None: @refreshed_property def angle(self) -> float: - """The angle of the camera at that point.""" + """The angle of the camera at the point.""" return self._data.angle @angle.setter @@ -310,7 +310,7 @@ def angle(self, value: float) -> None: @refreshed_property def scale(self) -> float: - """The scale of the camera at that point.""" + """The scale of the camera at the point.""" return self._data.scale @scale.setter @@ -326,12 +326,12 @@ def scale(self, value: float) -> None: @property def width(self) -> float: - """The scale of the camera at that point.""" + """The scale of the camera at the point.""" return self.camera.clip.project.width * (self.scale * 0.01) @property def height(self) -> float: - """The scale of the camera at that point.""" + """The scale of the camera at the point.""" return self.camera.clip.project.height * (self.scale * 0.01) @classmethod @@ -344,7 +344,7 @@ def new( angle: int, scale: float, ) -> CameraPoint: - """Create a new point and add it to the camera path at that index.""" + """Create a new point and add it to the camera path at the provided index.""" george.tv_camera_insert_point(index, x, y, angle, scale) return cls(index, camera) @@ -352,7 +352,7 @@ def remove(self) -> None: """Remove the camera point. Warning: - the point instance won't be usable after that call + the point instance won't be usable after this call """ george.tv_camera_remove_point(self.index) self.mark_removed() @@ -384,7 +384,7 @@ def interpolation_point(self) -> float: @classmethod def get_point_data_at(cls, position: float) -> george.TVPCameraPoint: - """Get the points data interpolated at that position (between 0 and 1).""" + """Get the points data interpolated at the provided position (between 0 and 1).""" position = max(0.0, min(position, 1.0)) return george.tv_camera_interpolation(position) diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 3ad5566..2d31793 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -8,12 +8,13 @@ from typing import TYPE_CHECKING, cast from fileseq.filesequence import FileSequence +from fileseq.frameset import FrameSet -from pytvpaint import george, utils -from pytvpaint.george.client import parse -from pytvpaint.sound import ClipSound +from pytvpaint import george, log, utils from pytvpaint.camera import Camera +from pytvpaint.george.client import parse from pytvpaint.layer import CameraLayer, CTGLayer, Layer, LayerColor, LayerFolder +from pytvpaint.sound import ClipSound from pytvpaint.utils import ( Removable, Renderable, @@ -311,11 +312,13 @@ def duplicate(self) -> Clip: Note: a new unique name is choosen for the duplicated clip with `get_unique_name`. """ + current_clips = list(self.project.clips) george.tv_clip_duplicate(self.id) - new_clip = self.project.current_clip + new_clip = [clip for clip in self.project.clips if clip not in current_clips][0] # noqa: RUF015 - clip_names = [clip.name for clip in self.project.clips if clip != new_clip] - new_clip.name = utils.get_unique_name(clip_names, new_clip.name) + clip_names = [clip.name for clip in current_clips] + new_name = utils.get_unique_name(clip_names, self.name) + new_clip.name = new_name return new_clip @@ -346,7 +349,7 @@ def get_layers( ignore_types: list of layer types to ignore, default is None, meaning all. Note: - Since TVP12 have introduced multiple layer types, this function is will replace Clip.layers. + Since TVP12 have introduced multiple layer types, this function will replace Clip.layers. """ for layer_id in self.layer_ids: layer_data = george.tv_layer_info(layer_id) @@ -552,7 +555,7 @@ def _validate_range(self, start: int, end: int) -> None: if start < clip_full_range[0] or end > clip_full_range[1]: raise ValueError(f"Render ({start}-{end}) not in clip range ({clip_full_range})") - def _get_real_range(self, start: int, end: int) -> tuple[int, int]: + def _get_real_range(self, start: int, end: int, frame_set: FrameSet | None = None) -> tuple[int, int, FrameSet]: # get project start to get real values project_start_frame = self.project.start_frame # get clip real start in project timeline @@ -560,13 +563,28 @@ def _get_real_range(self, start: int, end: int) -> tuple[int, int]: # get real mark_in since we'll also need to subtract it from the range real_mark_in = (self.mark_in - project_start_frame) if self.mark_in else 0 - start = (start - project_start_frame - real_mark_in) + clip_real_start - end = (end - project_start_frame - real_mark_in) + clip_real_start + def convert_range(x: int) -> int: + return (x - project_start_frame - real_mark_in) + clip_real_start + + start = convert_range(start) + end = convert_range(end) # clamp values to clip start start = max(clip_real_start, start) end = max(clip_real_start, end) - return start, end + + if frame_set is not None: + start = max(start, convert_range(frame_set.start())) + end = min(end, convert_range(frame_set.end())) + if frame_set.isConsecutive(): + frame_set = FrameSet(f"{start}-{end}") + else: + frames = [convert_range(f) for f in frame_set.items] + [start, end] + frame_set = FrameSet(frames) + else: + frame_set = FrameSet(f"{start}-{end}") + + return start, end, frame_set @set_as_current def render( @@ -574,6 +592,7 @@ def render( output_path: Path | str | FileSequence, start: int | None = None, end: int | None = None, + frame_set: FrameSet | None = None, use_camera: bool = False, layer_selection: list[Layer] | None = None, alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, @@ -586,6 +605,7 @@ def render( output_path: a single file or file sequence pattern start: the start frame to render or the mark in or the clip's start if None. Defaults to None. end: the end frame to render or the mark out or the clip's end if None. Defaults to None. + frame_set: a FrameSet with the frames/range to render. Defaults to None. use_camera: use the camera for rendering, otherwise render the whole canvas. Defaults to False. 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. @@ -610,12 +630,13 @@ def render( default_end = self.mark_out or self.end self._render( - output_path, - default_start, - default_end, - start, - end, - use_camera, + output_path=output_path, + default_start=default_start, + default_end=default_end, + start=start, + end=end, + frame_set=frame_set, + use_camera=use_camera, layer_selection=layer_selection, alpha_mode=alpha_mode, background_mode=background_mode, @@ -941,6 +962,8 @@ def set_layer_color(self, layer_color: LayerColor) -> None: Args: layer_color: the layer color instance. """ + if not george.is_tvp_version_below_12(): + log.warning("this function currently does not work properly in tvpaint 12") george.tv_layer_color_set_color(self.id, layer_color.index, layer_color.color, layer_color.name) def get_layer_color( @@ -961,7 +984,7 @@ def get_layer_color( """ values = (by_index, by_name, by_regex) if not any(v is not None for v in values): - raise ValueError(f"At least one value ({' or '.join(values)} must be provided") + raise ValueError(f"At least one value ({' or '.join(str(v) for v in values)} must be provided") if by_index is not None: return next(c for i, c in enumerate(self.layer_colors) if i == by_index) @@ -1038,7 +1061,7 @@ def get_sound( """ values = (by_track_index, by_path) if not any(v is not None for v in values): - raise ValueError(f"At least one value ({' or '.join(values)} must be provided") + raise ValueError(f"At least one value ({' or '.join(str(v) for v in values)} must be provided") for sound in self.sounds: if by_track_index is not None and sound.by_track_index != by_track_index: diff --git a/pytvpaint/george/client/__init__.py b/pytvpaint/george/client/__init__.py index 2ca822c..3c48401 100644 --- a/pytvpaint/george/client/__init__.py +++ b/pytvpaint/george/client/__init__.py @@ -5,10 +5,10 @@ from __future__ import annotations +import contextlib +import functools import os import re -import functools -import contextlib from pathlib import Path from time import sleep, time from typing import Any, Callable, TypeVar, cast @@ -28,7 +28,7 @@ def _connect_client(host: str = "ws://localhost", port: int = 3000, timeout: int rpc_client = JSONRPCClient(f"{host}:{port}", timeout) if not startup_connect: - log.debug(f"Auto Connect Disabled, RPC client is not connected") + log.debug("Auto Connect Disabled, RPC client is not connected") return rpc_client start_time = time() diff --git a/pytvpaint/george/client/parse.py b/pytvpaint/george/client/parse.py index a0d6e7f..9c86795 100644 --- a/pytvpaint/george/client/parse.py +++ b/pytvpaint/george/client/parse.py @@ -7,14 +7,13 @@ from __future__ import annotations - import re import shlex +from collections.abc import MutableSequence, Sequence from contextlib import suppress -from pathlib import Path -from collections.abc import Sequence from dataclasses import Field, fields, is_dataclass from enum import Enum +from pathlib import Path from typing import ( Any, ClassVar, @@ -28,6 +27,12 @@ from typing_extensions import Protocol, TypeAlias +T = TypeVar("T", bound=Any) +Value = Union[int, float, str, bool, None] +FieldTypes: TypeAlias = MutableSequence[tuple[Union[tuple[str, str], str], Any]] + +TVP_DOUBLE_QUOTES_RE = re.compile(r'""(.*?)""', re.I) + class DataclassInstance(Protocol): """Protocol that describes a Dataclass instance.""" @@ -68,28 +73,24 @@ def camel_to_pascal(s: str) -> str: def _strip_safe(text: str) -> str: - """ - Removes surrounding quotes ONLY if they form a single matching pair - wrapping the entire string. + """Removes surrounding quotes ONLY if they form a single matching pair wrapping the entire string. Examples: '"a b c"' -> 'a b c' (Stripped: wrapper) - '"a" "b"' -> '"a" "b"' (Kept: multiple items) - '"file" 10' -> '"file" 10' (Kept: doesn't end in quote) - 'normal text' -> 'normal text' (Kept) + '"a" "b"' -> '"a" "b"' (Kept: multiple items) + '"file" 10' -> '"file" 10' (Kept: doesn't end in quote) + 'normal text' -> 'normal text' (Kept) """ if len(text) < 2: return text first = text[0] - # 1. Must start and end with the same quote style + # Must start and end with the same quote style if first not in ('"', "'") or text[-1] != first: return text - # 2. Verify the closing quote matches the opening quote syntactically. - # We use regex to ensure the FIRST quoted segment covers the WHOLE string. - # Regex: ^(["']) ( [^"\] OR escaped char )* \1 $ - # This ensures we don't match '"a" "b"' as a single block. + # use regex to ensure the FIRST quoted segment covers the WHOLE string. + # This ensures we don't match '"a" "b"' as a single block. escaped_body = r"(?:[^\\" + first + r"]|\\.)*" pattern = re.compile(rf"^{first}({escaped_body}){first}$") @@ -101,55 +102,46 @@ def _strip_safe(text: str) -> str: return text -def _infer_type(token: str) -> Any: - """ - Guess the type of a string token (Int, Float, Bool, String). - Used when no specific type hint is provided. - """ - # 1. Try Integer (Strict, so 10.5 doesn't become 10) +def _guess_type(token: str) -> Any: + """Guess the type of the string token (int, float, bool, string). Used when no specific type hint is provided.""" + # Try Integer (Strict, so 10.5 doesn't become 10) try: return int(token) except ValueError: pass - # 2. Try Float + # Try float try: return float(token) except ValueError: pass - # 3. Try Boolean - # Note: '1' and '0' are already caught by int check above, which is usually fine. + # Try Boolean lower_val = token.lower() if lower_val in ("true", "yes", "on"): return True if lower_val in ("false", "no", "off"): return False - # 4. Fallback to String (removing internal quotes if shlex didn't) - return token.strip("\"'") + return token.strip("\"'") # remove internal quotes if shlex didn't -T = TypeVar("T", bound=Any) - - -def tv_cast_to_type(value_str: str, type_cls: type[T]) -> T: - """ - Casts a string value to a specific Python type, supporting: +def tv_cast_to_type(value: str, cast_type: type[T]) -> T: # noqa: C901 + """Casts a string value to a specific Python type, supporting: - Primitives (int, float, bool, str) - Path objects - Enums (by name, value, index) - Collections (List[T], Tuple[T, ...], Tuple[A, B]) - """ - # We strip outer quotes to expose list content (e.g. "a b") - # BUT we leave them alone if it's a complex string (e.g. "a" "b") - clean_val = _strip_safe(value_str.strip()) - origin_type = get_origin(type_cls) - - # --- 1. Collections (List[T], Tuple[...]) --- - if type_cls in (list, tuple) or origin_type in (list, tuple): + """ # noqa: D205, D415 + # strip outer quotes to expose list content (e.g. "a b") + # BUT leave as is if it's a complex string (e.g. "a" "b") + clean_val = _strip_safe(value.strip()) + origin_type = get_origin(cast_type) + + # check if Collections (List[T], Tuple[...]) + if cast_type in (list, tuple) or origin_type in (list, tuple): # Determine the base container type (list or tuple) - container_cls = origin_type or type_cls + container_cls = origin_type or cast_type # Tokenize the string (handling quotes: "1 '2 3' 4") try: @@ -159,7 +151,7 @@ def tv_cast_to_type(value_str: str, type_cls: type[T]) -> T: tokens = [t.strip("\"'") for t in tokens] # Get inner type arguments (e.g. [int] from List[int]) - type_args = get_args(type_cls) + type_args = get_args(cast_type) casted_items = [] # Case A: Homogeneous (List[int] or Tuple[int, ...]) @@ -167,13 +159,13 @@ def tv_cast_to_type(value_str: str, type_cls: type[T]) -> T: if type_args and (container_cls is list or (len(type_args) == 2 and type_args[1] is ...)): item_type = type_args[0] for token in tokens: - casted_items.append(tv_cast_to_type(token, item_type)) + casted_items.append(tv_cast_to_type(token, item_type)) # noqa: PERF401 # Case B: Fixed-Size Tuple (Tuple[str, int]) elif container_cls is tuple and type_args: if len(tokens) != len(type_args): raise ValueError( - f"Count mismatch for {type_cls}: expected {len(type_args)}, got {len(tokens)} ('{clean_val}')" + f"Count mismatch for {cast_type}: expected {len(type_args)}, got {len(tokens)} ('{clean_val}')" ) for token, sub_type in zip(tokens, type_args): casted_items.append(tv_cast_to_type(token, sub_type)) @@ -182,50 +174,55 @@ def tv_cast_to_type(value_str: str, type_cls: type[T]) -> T: else: for token in tokens: # Recursive call with 'str' keeps it simple, or add auto-inference logic here - casted_items.append(_infer_type(token)) + casted_items.append(_guess_type(token)) # noqa: PERF401 - return container_cls(casted_items) + return cast(T, container_cls(casted_items)) clean_val = clean_val.strip("\"'") - # --- 2. Basic Primitives --- - if type_cls is str: - return clean_val - if type_cls is int: - return int(float(clean_val)) - if type_cls is float: - return float(clean_val) - if type_cls is Path: - return Path(clean_val) - if type_cls is bool: - return clean_val.lower() in ("true", "1", "yes", "on") - - # --- 3. Enums --- - if isinstance(type_cls, type) and issubclass(type_cls, Enum): - # A. By Name + # Basic Primitives + if cast_type is str: + return cast(T, clean_val) + if cast_type is int: + return cast(T, int(float(clean_val))) + if cast_type is float: + return cast(T, float(clean_val)) + if cast_type is Path: + return cast(T, Path(clean_val)) + if cast_type is bool: + return cast(T, clean_val.lower() in ("true", "1", "yes", "on")) + + # Enums + if isinstance(cast_type, type) and issubclass(cast_type, Enum): + # By Name with suppress(KeyError): - return type_cls[clean_val] + return cast(T, cast_type[clean_val]) with suppress(KeyError): - return type_cls[clean_val.lower()] - # B. By Value + return cast(T, cast_type[clean_val.lower()]) + # By value with suppress(ValueError): - return type_cls(clean_val) + return cast(T, cast_type(clean_val)) with suppress(ValueError): - return type_cls(clean_val.lower()) - # C. By Int Value (for "1" -> 1) + return cast(T, cast_type(clean_val.lower())) + + # Exhaustive case-insensitive search for names and string values + clean_lower = clean_val.lower() + for member in cast_type: + if member.name.lower() == clean_lower: + return cast(T, member) + if isinstance(member.value, str) and member.value.lower() == clean_lower: + return cast(T, member) + + # By int value (for "1" -> 1) with suppress(ValueError): - return type_cls(int(float(clean_val))) - # D. By Index (Position in definition) + return cast(T, cast_type(int(float(clean_val)))) + # By Index (Position in definition) if clean_val.isdigit(): with suppress(IndexError): - return list(type_cls)[int(clean_val)] - - raise ValueError(f"'{clean_val}' is not a valid {type_cls.__name__}") + return cast(T, list(cast_type)[int(clean_val)]) - # Fallback - return type_cls(clean_val) + raise ValueError(f"'{clean_val}' is not a valid {cast_type.__name__}") - -FieldTypes: TypeAlias = list[tuple[tuple[str, str] | str, Any]] + return cast(T, cast_type(clean_val)) def get_dataclass_fields(datacls: DataclassInstance | type[DataclassInstance], alt_names: bool = False) -> FieldTypes: @@ -238,7 +235,7 @@ def get_dataclass_fields(datacls: DataclassInstance | type[DataclassInstance], a Returns: the list of key/type tuple """ - with_fields = [] + with_fields: FieldTypes = [] type_hints = get_type_hints(datacls) for f in fields(datacls): @@ -260,8 +257,7 @@ def tv_parse_list( with_fields: FieldTypes | type[DataclassInstance], unused_indices: list[int] | None = None, ) -> dict[str, Any]: - """ - Parses a positional string into typed arguments. + """Parses a positional string into typed arguments. Args: text: The raw input string. @@ -271,26 +267,20 @@ def tv_parse_list( Returns: a dict with the provided fields and the values cast to the given types """ - # For dataclasses get the type hints and filter those with metadata if is_dataclass(with_fields): with_fields = get_dataclass_fields(with_fields, alt_names=False) - else: - # Explicitly cast because we are sure now - with_fields = cast(FieldTypes, with_fields) if len(with_fields) == 1: - # Single Argument, pass the whole raw text to smart_cast. - # It handles splitting internally if type_cls is list/tuple. - name, type_cls = with_fields[0] + name_entry, type_cls = with_fields[0] + name = name_entry[0] if isinstance(name_entry, tuple) else name_entry return {name: tv_cast_to_type(text, type_cls)} - # Tokenize (respecting quotes), posix=False preserves Windows backslashes + text = TVP_DOUBLE_QUOTES_RE.sub(r'"\1"', text) try: - tokens = shlex.split(text, posix=False) + tokens = shlex.split(text, posix=False) # posix=False preserves Windows backslashes except ValueError as e: raise ValueError(f"Parsing error (unbalanced quotes?): {e}") - # Remove any unused values if unused_indices: tokens = [t for i, t in enumerate(tokens) if i not in unused_indices] @@ -303,7 +293,7 @@ def tv_parse_list( return parsed_args -def tv_parse_dict(text: str, with_fields: FieldTypes | type[DataclassInstance]) -> dict[str, Any]: +def tv_parse_dict(text: str, with_fields: FieldTypes | type[DataclassInstance]) -> dict[str, T]: """Parse a list of values as key value pairs returned from TVPaint commands. Cast the values to a provided dataclass type or list of key/types pairs. @@ -315,15 +305,9 @@ def tv_parse_dict(text: str, with_fields: FieldTypes | type[DataclassInstance]) Returns: a dict with the provided fields and the values cast to the given types """ - # For dataclasses get the type hints and filter those with metadata if is_dataclass(with_fields): with_fields = get_dataclass_fields(with_fields, alt_names=True) - else: - # Explicitly cast because we are sure now - with_fields = cast(FieldTypes, with_fields) - # 1. Create a Normalized Map for alt names and case sensitivity - # e.g. {'opacity': ('Transparency', int), 'x': ('X', float)} normalized_map: dict[str, tuple[str, type[T]]] = {} for field_name, type_cls in with_fields: # handle different naming schemes @@ -334,26 +318,20 @@ def tv_parse_dict(text: str, with_fields: FieldTypes | type[DataclassInstance]) pascal_name = camel_to_pascal(alt_field_name).lower() normalized_map[pascal_name] = (field_name, type_cls) - # 2. Sort keys by length (descending) to prevent partial matching - # (e.g. preventing 'x' from matching inside 'x_pos') + # Sort keys by length (descending) to prevent partial matching (ex: prevent 'x' from matching inside 'x_pos') sorted_keys = sorted(normalized_map.keys(), key=len, reverse=True) - # 3. Build the Regex, use re.escape for keys with symbols - joined_keys = "|".join(map(re.escape, sorted_keys)) + joined_keys = "|".join(map(re.escape, sorted_keys)) # use re.escape for keys with symbols pattern = re.compile(rf"({joined_keys})\s+(.*?)(?=\s+(?:{joined_keys})|$)", re.IGNORECASE) parsed_data = {} - # 4. match and Cast + text = TVP_DOUBLE_QUOTES_RE.sub(r'"\1"', text) for match_key, match_val in pattern.findall(text): clean_val = match_val.strip() - - # Normalize match to lowercase to look up the type definition lookup = normalized_map.get(match_key.lower()) if lookup: original_key, target_type = lookup - # Use the original key name for the output dict (e.g. 'x' not 'X') - # and cast the value using smart_cast parsed_data[original_key] = tv_cast_to_type(clean_val, target_type) return parsed_data @@ -371,13 +349,9 @@ def args_dict_to_list(args: dict[str, Any]) -> list[Any]: key/values list """ args_filter = {k: v for k, v in args.items() if v is not None} - # Flatten dictionnary key value pairs with zip return [item for kv in args_filter.items() for item in kv] -Value = Union[int, float, str, bool, None] - - def validate_args_list(optional_args: Sequence[Value | tuple[Value, ...]]) -> list[Any]: """Validates *args equivalent for tvpaint. @@ -403,7 +377,6 @@ def validate_args_list(optional_args: Sequence[Value | tuple[Value, ...]]) -> li if arg is None or (isinstance(arg, tuple) and all(a is None for a in arg)): break - # If it's a tuple they need to be defined if isinstance(arg, tuple): if any(a is None for a in arg): raise ValueError(f"You must pass all the parameters: {arg}") @@ -415,26 +388,20 @@ def validate_args_list(optional_args: Sequence[Value | tuple[Value, ...]]) -> li def normalize_windows_paths(text: str) -> str: - """ - Identifies Windows-style file paths and converts backslashes to forward slashes. + r"""Identifies Windows-style file paths and converts backslashes to forward slashes. Supports: 1. Drive Roots: C:\\Folder 2. Relative: .\\Folder or ..\\Folder 3. Root Relative: \\.\\Folder or \\..\\Folder <-- NEW - 4. UNC Paths: \\\\Server\\Share\\File + 4. UNC Paths: \\\\Server\\Share\\File """ - # --- 1. Building Blocks --- - # Valid characters for a filename (excludes forbidden chars and newlines) - # Note: We include '.' in valid chars valid_chars = r'(?:(?!\s[a-zA-Z]:)[^\\/:*?"<>|\r\n])+' # A Path Segment is a backslash followed by valid chars: "\Folder" segment = rf"\\{valid_chars}" - # --- 2. Define Start Patterns --- - - # A. Standard Starts + # Standard Starts # Matches: "C:" OR "\.." OR ".." OR "\." OR "." # Logic: # [a-zA-Z]: -> Drive Letter @@ -445,87 +412,53 @@ def normalize_windows_paths(text: str) -> str: # # use \.\.? (greedy) to match ".." before "." start_standard = r"(?:[a-zA-Z]:|\\\.\.?|\.\.?)" - - # B. UNC Start - # Matches: "\\Server" (Backslash-Backslash-Hostname) - start_unc = rf"\\\\{valid_chars}" - - # --- 3. Assemble Patterns --- - - # Pattern 1: Standard Path - # Start (C: or `..`) + At least one Segment (\Folder) pattern_standard = rf"(?:{start_standard})(?:{segment})+" - # Pattern 2: UNC Path - # Start (\\Host) + At least one Segment (\Share) + # UNC Start. Matches: "\\Server" (Backslash-Backslash-Hostname) + start_unc = rf"\\\\{valid_chars}" pattern_unc = rf"(?:{start_unc})(?:{segment})+" - # Combine them full_pattern = rf"({pattern_standard}|{pattern_unc})" - def _to_posix(match: re.Match) -> str: + def _to_posix(match: re.Match[str]) -> str: return str(match.group(1)).replace("\\", "/") return re.sub(full_pattern, _to_posix, text) def unescape_everything_safely(text: str) -> str: - """ - Unescapes a "mangled" string while preserving Windows file paths and filenames. - - This function uses a two-step process: - 1. Normalizes Windows paths to POSIX (forward slashes) to prevent `\\n` in paths - from being misinterpreted as newlines. - 2. Unescapes standard characters (\\n, \\t) and safe Hex/Octal codes. + """Unescapes a raw/non-formatted tvpaint string while trying to preserve Windows file paths and filenames. Args: - text: The "dirty" raw string (e.g. from a log or subprocess output). + text: raw string. Returns: - str: The clean, unescaped string with normalized newlines. + str: unescaped string. """ - # 1. Normalize Paths - # "C:\\new_folder" -> "C:/new_folder" text = normalize_windows_paths(text) - - # 2. Unescape Logic - # New Regex: Matches greedily (no lookaheads here). - # We catch the full [0-7]{1,3} or x[...]{2} regardless of what follows. pattern = r"\\([rntbfva]|x[0-9a-fA-F]{2}|[0-7]{1,3})" - simple_escapes = {"n": "\n", "r": "\r", "t": "\t", "b": "\b", "f": "\f", "v": "\v", "a": "\a"} - def _replace_match(match: re.Match) -> str: + def _replace_match(match: re.Match[str]) -> str: code = match.group(1) - # A. Standard Escapes (Always safe: \n, \t, etc) if code in simple_escapes: return simple_escapes[code] - # B. Hex/Octal (Require Safety Check) - # We manually peek at the character AFTER the match. - full_match = match.group(0) # e.g. "\100" - end_idx = match.end() # Index immediately after match + # Hex/Octal check + full_match = match.group(0) + end_idx = match.end() # Check if we are at the end of the string if end_idx < len(match.string): next_char = match.string[end_idx] - # THE SAFETY CHECK: # If followed by . or _, treat it as a filename, NOT an escape. if next_char in (".", "_"): - return full_match + return str(full_match) - # Perform the conversion if code.startswith("x"): return chr(int(code[1:], 16)) - else: - return chr(int(code, 8)) + return chr(int(code, 8)) - # Apply unescape text = re.sub(pattern, _replace_match, text) - - # 3. Final Pass: Normalize Newlines - # Consolidate Windows (\r\n) and Mac Classic (\r) to Unix (\n) - text = text.replace("\r\n", "\n").replace("\r", "\n") - - return text + return text.replace("\r\n", "\n").replace("\r", "\n") diff --git a/pytvpaint/george/client/rpc.py b/pytvpaint/george/client/rpc.py index 6fefb44..85c3b69 100644 --- a/pytvpaint/george/client/rpc.py +++ b/pytvpaint/george/client/rpc.py @@ -100,9 +100,7 @@ def _auto_reconnect(self) -> None: # 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 !" - ) + raise ConnectionRefusedError("Could not establish connection with a tvpaint instance before timeout !") def __del__(self) -> None: """Called when the client goes out of scope.""" @@ -119,9 +117,7 @@ def connect(self, timeout: float | None = None) -> None: if not self.ping_thread: self._ping_start_time = time() - self.ping_thread = threading.Thread( - target=self._auto_reconnect, daemon=True - ) + self.ping_thread = threading.Thread(target=self._auto_reconnect, daemon=True) self.run_forever = True self.ping_thread.start() @@ -156,9 +152,7 @@ def execute_remote( JSONRPCResponse: the JSON-RPC response payload """ if not self.is_connected: - raise ConnectionError( - f"Can't send rpc message because the client is not connected to {self.url}" - ) + raise ConnectionError(f"Can't send rpc message because the client is not connected to {self.url}") payload: JSONRPCPayload = { "jsonrpc": self.jsonrpc_version, diff --git a/pytvpaint/george/exceptions.py b/pytvpaint/george/exceptions.py index 37e3bbd..48a9a0c 100644 --- a/pytvpaint/george/exceptions.py +++ b/pytvpaint/george/exceptions.py @@ -11,9 +11,7 @@ class GeorgeError(Exception): Used for return values in the `[ERROR]` section of functions in TVPaint's documentation. """ - def __init__( - self, message: str | None = None, error_value: Any | None = None - ) -> None: + def __init__(self, message: str | None = None, error_value: Any | None = None) -> None: super().__init__(f"{message}" if message else "") self.error_value = error_value diff --git a/pytvpaint/george/grg_base.py b/pytvpaint/george/grg_base.py index 727090a..c5993f8 100644 --- a/pytvpaint/george/grg_base.py +++ b/pytvpaint/george/grg_base.py @@ -4,12 +4,12 @@ import contextlib import functools +import warnings from collections.abc import Generator from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import Any, Callable, TypeVar, cast, overload -import warnings from packaging import version from typing_extensions import Literal, TypeAlias @@ -348,6 +348,21 @@ def from_extension(cls, extension: str) -> SaveFormat: raise ValueError(f"Could not find format ({extension}) in accepted formats ({SaveFormat})") return cast(SaveFormat, getattr(cls, extension.upper())) + @classmethod + def to_extension(cls, save_format: SaveFormat) -> str: + """Returns the typical tvpaint file extension for the provided format.""" + image_formats = { + SaveFormat.JPG: "jpg", + SaveFormat.MKV: "mkv", + SaveFormat.MOV: "mov", + SaveFormat.MP4: "mp4", + SaveFormat.SGI: "sgi", + SaveFormat.SOFTIMAGE: "pic", + SaveFormat.TIFF: "tiff", + SaveFormat.WEBM: "webm", + } + return f".{image_formats.get(save_format, save_format.value)}" + @classmethod def is_image(cls, extension: str) -> bool: """Returns True if the extension correspond to an image format.""" @@ -638,7 +653,7 @@ def tv_version() -> tuple[str, str, str]: tvp_version (str): version number (ex: 12.0.0) language (str): language (ex: fr, en, etc...) """ - cmd_fields = [ + cmd_fields: FieldTypes = [ ("software_name", str), ("version", str), ("language", str), @@ -743,9 +758,9 @@ def tv_menu_show(menu_element: MenuElement | None = None, *menu_options: Any, cu def add_some_magic(i_am_a_badass: bool = False, magic_number: int | None = None) -> None: - """Don't use this function ! It just might change your life forever...""" + """Don't use this function ! It just might change your life forever !""" if not i_am_a_badass: - log.warning("Sorry, you're not enough of a badass for this function...") + log.warning("Sorry, you're not enough of a badass for this function !") magic_number = magic_number if magic_number is not None else 14 send_cmd("tv_MagicNumber", magic_number) @@ -1113,7 +1128,7 @@ def tv_pen(size: float) -> float: Warning: DEPRECATED: Function `tv_pen` is deprecated, We advise using `tv_penbrush` instead. """ - res = tv_parse_dict(send_cmd("tv_Pen", size), with_fields=[("size", float)]) + res: dict[str, Any] = tv_parse_dict(send_cmd("tv_Pen", size), with_fields=[("size", float)]) return cast(float, res["size"]) @@ -1125,7 +1140,7 @@ def tv_pen_brush_get(tool_mode: bool = False) -> TVPPenBrush: # Remove the first value which is tv_penbrush result = result[(len("tv_penbrush") + 1) :] - res = tv_parse_dict(result, with_fields=TVPPenBrush) + res: dict[str, Any] = tv_parse_dict(result, with_fields=TVPPenBrush) return TVPPenBrush(**res) diff --git a/pytvpaint/george/grg_camera.py b/pytvpaint/george/grg_camera.py index 1195e24..6b2a429 100644 --- a/pytvpaint/george/grg_camera.py +++ b/pytvpaint/george/grg_camera.py @@ -43,9 +43,7 @@ def tv_camera_info_get() -> TVPCamera: if not is_tvp_version_below_12(): # values of pixel aspect ratio and fps have been swapped in versions > 12 fields_keys = list(dict(fields).keys()) - pixel_aspect_index, fps_index = fields_keys.index( - "pixel_aspect_ratio" - ), fields_keys.index("frame_rate") + pixel_aspect_index, fps_index = fields_keys.index("pixel_aspect_ratio"), fields_keys.index("frame_rate") fields[pixel_aspect_index], fields[fps_index] = ( fields[fps_index], fields[pixel_aspect_index], @@ -92,7 +90,7 @@ def tv_camera_interpolation(position: float) -> TVPCameraPoint: def tv_camera_info_frame(frame: int) -> TVPCameraPoint: """Get the position/angle/scale values at the given frame.""" - errors = ["Given frame out of camera layer's range"] + errors = ["The provided frame is out of the camera range"] res = tv_parse_list( send_cmd("tv_CameraInfoFrame", frame, error_values=errors), with_fields=TVPCameraPoint, diff --git a/pytvpaint/george/grg_clip.py b/pytvpaint/george/grg_clip.py index ca9dc4c..cf89059 100644 --- a/pytvpaint/george/grg_clip.py +++ b/pytvpaint/george/grg_clip.py @@ -67,14 +67,12 @@ def tv_clip_info(clip_id: int) -> TVPClip: NoObjectWithIdError: if given an invalid clip id """ result = send_cmd("tv_ClipInfo", clip_id, error_values=[GrgErrorValue.EMPTY]) - clip = tv_parse_dict(result, with_fields=TVPClip) + clip: dict[str, Any] = tv_parse_dict(result, with_fields=TVPClip) clip["id"] = clip_id return TVPClip(**clip) -@try_cmd( - exception_msg="Invalid scene id or clip position or elements have been removed" -) +@try_cmd(exception_msg="Invalid scene id or clip position or elements have been removed") def tv_clip_enum_id(scene_id: int, clip_position: int) -> int: """Get the id of the clip at the given position inside the given scene. @@ -166,9 +164,7 @@ def tv_clip_hidden_set(clip_id: int, new_state: bool) -> None: Raises: NoObjectWithIdError: if given an invalid clip id """ - send_cmd( - "tv_ClipHidden", clip_id, int(new_state), error_values=[GrgErrorValue.EMPTY] - ) + send_cmd("tv_ClipHidden", clip_id, int(new_state), error_values=[GrgErrorValue.EMPTY]) def tv_clip_select(clip_id: int) -> None: @@ -283,8 +279,7 @@ def tv_save_sequence( if not export_path.parent.exists(): raise NotADirectoryError( - "Can't save the sequence because parent" - f"folder does not exist: {export_path.parent.as_posix()}" + "Can't save the sequence because parent" f"folder does not exist: {export_path.parent.as_posix()}" ) args: list[Any] = [export_path.as_posix()] @@ -302,9 +297,7 @@ def tv_bookmarks_enum(position: int) -> int: Raises: GeorgeError: if no bookmark found at provided position """ - return int( - send_cmd("tv_BookmarksEnum", position, error_values=[GrgErrorValue.NONE]) - ) + return int(send_cmd("tv_BookmarksEnum", position, error_values=[GrgErrorValue.NONE])) def tv_bookmark_set(frame: int) -> None: @@ -425,22 +418,28 @@ def tv_clip_save_structure_json( ValueError: the parent folder doesn't exist """ export_path = Path(export_path).resolve() - if not export_path.parent.exists(): - raise ValueError( - "Can't write file because the destination folder doesn't exist" - ) + raise ValueError("Can't write file because the destination folder doesn't exist") + + valid_extensions = [ + SaveFormat.BMP, + SaveFormat.JPG, + SaveFormat.PNG, + SaveFormat.TGA, + SaveFormat.TIFF, + ] + if file_format not in valid_extensions: + raise ValueError(f"File format not in valid formats for json exports : {valid_extensions}") args = [export_path.as_posix(), "JSON"] - dict_args = { - "fileformat": file_format.value, - "background": int(fill_background) if fill_background else None, + "fileformat": SaveFormat.to_extension(file_format)[1:], + "background": int(fill_background) if fill_background is not None else None, "patternfolder": folder_pattern, "patternfile": file_pattern, - "onlyvisiblelayers": int(visible_layers_only), - "allimages": int(all_images), - "ignoreduplicateimages": int(ignore_duplicates), + "onlyvisiblelayers": int(visible_layers_only) if visible_layers_only is not None else None, + "allimages": int(all_images) if all_images is not None else None, + "ignoreduplicateimages": int(ignore_duplicates) if ignore_duplicates is not None else None, "excludenames": (";".join(exclude_names) if exclude_names else None), } args.extend(args_dict_to_list(dict_args)) @@ -467,9 +466,7 @@ def tv_clip_save_structure_psd( export_path = Path(export_path) if not export_path.parent.exists(): - raise ValueError( - "Can't write file because the destination folder doesn't exist" - ) + raise ValueError("Can't write file because the destination folder doesn't exist") args_dict: dict[str, str | int | None] diff --git a/pytvpaint/george/grg_guideline.py b/pytvpaint/george/grg_guideline.py index 643e2ad..f04be0c 100644 --- a/pytvpaint/george/grg_guideline.py +++ b/pytvpaint/george/grg_guideline.py @@ -2,19 +2,18 @@ from __future__ import annotations +from dataclasses import dataclass, field from enum import Enum from pathlib import Path -from dataclasses import dataclass, field -from typing import cast +from typing import Any +from pytvpaint import log from pytvpaint.george.client import send_cmd -from pytvpaint.george.grg_base import GrgErrorValue, RGBAColor from pytvpaint.george.client.parse import ( - DataclassInstance, - get_dataclass_fields, args_dict_to_list, tv_parse_dict, ) +from pytvpaint.george.grg_base import GrgErrorValue, RGBAColor class GuidelineType(Enum): @@ -156,8 +155,8 @@ class TVPGuidelineMarks: position: int = field(metadata={"parsed": False}) - count_x: float - count_y: float + count_x: int = field(metadata={"alt_name": "countx"}) + count_y: int = field(metadata={"alt_name": "county"}) @dataclass(frozen=True) @@ -307,10 +306,7 @@ def tv_guideline_visibility_set_all( is_visible: True to set as visible, False otherwise. apply_all: True to apply to all guidelines regardless of type. """ - if guideline_type is not None and not apply_all: - apply_to = guideline_type.value - else: - apply_to = "all" + apply_to = "all" if apply_all or guideline_type is None else guideline_type.value send_cmd( "tv_GuidelineVisible", @@ -331,11 +327,11 @@ def tv_guideline_margin_get(position: int, on_global: bool = False) -> int: ) -def tv_guideline_margin_set(position: int, margin: int, on_global: bool = False) -> None: +def tv_guideline_margin_set(position: int, margin: int) -> None: """Set the margin of the guideline at the given position.""" send_cmd( "tv_GuidelineMarge", - position if not on_global else "global", + position, margin, error_values=[-1, -2], ) @@ -349,10 +345,7 @@ def tv_guideline_margin_set_all(guideline_type: GuidelineType | None, margin: in margin: the margin to apply. apply_all: True to apply to all guidelines regardless of type. """ - if guideline_type is not None and not apply_all: - apply_to = guideline_type.value - else: - apply_to = "all" + apply_to = "all" if apply_all or guideline_type is None else guideline_type.value send_cmd( "tv_GuidelineMarge", @@ -373,11 +366,11 @@ def tv_guideline_color_get(position: int, on_global: bool = False) -> RGBAColor: return RGBAColor(*[int(c) for c in (r, g, b, a)]) -def tv_guideline_color_set(position: int, color: RGBAColor, on_global: bool = False) -> None: +def tv_guideline_color_set(position: int, color: RGBAColor) -> None: """Set the color of the guideline at the given position.""" send_cmd( "tv_GuidelineColor", - position if not on_global else "global", + position, color.r, color.g, color.b, @@ -394,10 +387,7 @@ def tv_guideline_color_set_all(guideline_type: GuidelineType | None, color: RGBA color: the color to apply. apply_all: True to apply to all guidelines regardless of type. """ - if guideline_type is not None and not apply_all: - apply_to = guideline_type.value - else: - apply_to = "all" + apply_to = "all" if apply_all or guideline_type is None else guideline_type.value send_cmd( "tv_GuidelineColor", @@ -441,10 +431,7 @@ def tv_guideline_snap_set_all(guideline_type: GuidelineType | None, snap: bool, snap: True to snap, False otherwise. apply_all: True to apply to all guidelines regardless of type. """ - if guideline_type is not None and not apply_all: - apply_to = guideline_type.value - else: - apply_to = "all" + apply_to = "all" if apply_all or guideline_type is None else guideline_type.value send_cmd( "tv_GuidelineSnap", @@ -489,13 +476,13 @@ def tv_guideline_add_image( """Set info for the image guideline at the given position.""" args = args_dict_to_list( { - "img_path": img_path, + "path": img_path, "x": x, "y": y, "rotation": rotation, "scale": scale, - "flip": flip, - "alphamode": alpha_mode, + "flip": flip.value if flip is not None else None, + "alphamode": alpha_mode.value if alpha_mode is not None else None, } ) @@ -511,14 +498,13 @@ def tv_guideline_add_image( def tv_guideline_modify_image_get(position: int) -> TVPGuidelineImage: """Get info for the image guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineImage) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineImage) guideline["position"] = position return TVPGuidelineImage(**guideline) @@ -531,31 +517,26 @@ def tv_guideline_modify_image_set( rotation: float | None = None, scale: float | None = None, flip: FlipDirection | None = None, -) -> TVPGuidelineImage: +) -> None: """Set info for the image guideline at the given position.""" args = args_dict_to_list( { - "position": position, - "img_path": img_path, + "path": Path(img_path).as_posix() if img_path is not None else None, "x": x, "y": y, "rotation": rotation, "scale": scale, - "flip": flip, + "flip": flip.value if flip is not None else None, } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - fields = get_dataclass_fields(cast(DataclassInstance, TVPGuidelineImage)) - guideline = tv_parse_dict(result, with_fields=fields) - guideline["position"] = position - return TVPGuidelineImage(**guideline) - def tv_guideline_add_line( x: float | None = None, @@ -583,14 +564,13 @@ def tv_guideline_add_line( def tv_guideline_modify_line_get(position: int) -> TVPGuidelineLine: """Get info for the line guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineLine) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineLine) guideline["position"] = position return TVPGuidelineLine(**guideline) @@ -600,28 +580,23 @@ def tv_guideline_modify_line_set( x: float | None = None, y: float | None = None, angle: float | None = None, -) -> TVPGuidelineLine: +) -> None: """Set info for the line guideline at the given position.""" args = args_dict_to_list( { - "position": position, "x": x, "y": y, "angle": angle, } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - fields = get_dataclass_fields(cast(DataclassInstance, TVPGuidelineLine)) - guideline = tv_parse_dict(result, with_fields=fields) - guideline["position"] = position - return TVPGuidelineLine(**guideline) - def tv_guideline_add_segment( x1: float | None = None, @@ -651,28 +626,27 @@ def tv_guideline_add_segment( def tv_guideline_modify_segment_get(position: int) -> TVPGuidelineSegment: """Get info for the segment guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineSegment) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineSegment) guideline["position"] = position return TVPGuidelineSegment(**guideline) def tv_guideline_modify_segment_set( + position: int, x1: float | None = None, y1: float | None = None, x2: float | None = None, y2: float | None = None, -) -> TVPGuidelineSegment: +) -> None: """Set info for the segment guideline at the given position.""" args = args_dict_to_list( { - "position": position, "x1": x1, "y1": y1, "x2": x2, @@ -680,17 +654,13 @@ def tv_guideline_modify_segment_set( } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - fields = get_dataclass_fields(cast(DataclassInstance, TVPGuidelineSegment)) - guideline = tv_parse_dict(result, with_fields=fields) - guideline["position"] = position - return TVPGuidelineSegment(**guideline) - def tv_guideline_add_circle( x: float | None = None, @@ -718,14 +688,13 @@ def tv_guideline_add_circle( def tv_guideline_modify_circle_get(position: int) -> TVPGuidelineCircle: """Get info for the circle guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineCircle) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineCircle) guideline["position"] = position return TVPGuidelineCircle(**guideline) @@ -735,27 +704,23 @@ def tv_guideline_modify_circle_set( x: float | None = None, y: float | None = None, radius: float | None = None, -) -> TVPGuidelineCircle: +) -> None: """Set info for the circle guideline at the given position.""" args = args_dict_to_list( { - "position": position, "x": x, "y": y, "radius": radius, } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineCircle) - guideline["position"] = position - return TVPGuidelineCircle(**guideline) - def tv_guideline_add_ellipse( x: float | None = None, @@ -785,14 +750,13 @@ def tv_guideline_add_ellipse( def tv_guideline_modify_ellipse_get(position: int) -> TVPGuidelineEllipse: """Get info for the ellipse guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineEllipse) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineEllipse) guideline["position"] = position return TVPGuidelineEllipse(**guideline) @@ -803,11 +767,10 @@ def tv_guideline_modify_ellipse_set( y: float | None = None, radius_a: float | None = None, radius_b: float | None = None, -) -> TVPGuidelineEllipse: +) -> None: """Set info for the ellipse guideline at the given position.""" args = args_dict_to_list( { - "position": position, "x": x, "y": y, "radiusa": radius_a, @@ -815,16 +778,13 @@ def tv_guideline_modify_ellipse_set( } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineEllipse) - guideline["position"] = position - return TVPGuidelineEllipse(**guideline) - def tv_guideline_add_grid( x: float | None = None, @@ -854,14 +814,13 @@ def tv_guideline_add_grid( def tv_guideline_modify_grid_get(position: int) -> TVPGuidelineGrid: """Get info for the grid guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineGrid) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineGrid) guideline["position"] = position return TVPGuidelineGrid(**guideline) @@ -872,11 +831,10 @@ def tv_guideline_modify_grid_set( y: float | None = None, width: float | None = None, height: float | None = None, -) -> TVPGuidelineGrid: +) -> None: """Set info for the grid guideline at the given position.""" args = args_dict_to_list( { - "position": position, "x": x, "y": y, "w": width, @@ -884,16 +842,13 @@ def tv_guideline_modify_grid_set( } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineGrid) - guideline["position"] = position - return TVPGuidelineGrid(**guideline) - def tv_guideline_add_marks( count_x: int | None = None, @@ -902,8 +857,8 @@ def tv_guideline_add_marks( """Set info for the image guideline at the given position.""" args = args_dict_to_list( { - "count_x": count_x, - "count_y": count_y, + "countx": count_x, + "county": count_y, } ) @@ -919,14 +874,13 @@ def tv_guideline_add_marks( def tv_guideline_modify_marks_get(position: int) -> TVPGuidelineMarks: """Get info for the marks guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineMarks) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineMarks) guideline["position"] = position return TVPGuidelineMarks(**guideline) @@ -935,26 +889,28 @@ def tv_guideline_modify_marks_set( position: int, count_x: int | None = None, count_y: int | None = None, -) -> TVPGuidelineMarks: - """Set info for the marks guideline at the given position.""" +) -> None: + """Set info for the marks guideline at the given position. + + Warnings: + function `tv_GuidelineModify` doesn't seem to work in tvpaint, values are never changed. + + """ + log.warning("function `tv_GuidelineModify` doesn't seem to work in tvpaint, values are never changed.") args = args_dict_to_list( { - "position": position, - "count_x": count_x, - "count_y": count_y, + "countx": count_x, + "county": count_y, } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineMarks) - guideline["position"] = position - return TVPGuidelineMarks(**guideline) - def tv_guideline_add_safe_area( sf_out: int | None = None, @@ -980,42 +936,37 @@ def tv_guideline_add_safe_area( def tv_guideline_modify_safe_area_get(position: int) -> TVPGuidelineSafeArea: """Get info for the safe area guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineSafeArea) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineSafeArea) guideline["position"] = position return TVPGuidelineSafeArea(**guideline) def tv_guideline_modify_safe_area_set( position: int, - sf_out: int | None = None, - sf_in: int | None = None, -) -> TVPGuidelineSafeArea: + sf_out: float | None = None, + sf_in: float | None = None, +) -> None: """Set info for the safe area guideline at the given position.""" args = args_dict_to_list( { - "position": position, "out": sf_out, "in": sf_in, } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineSafeArea) - guideline["position"] = position - return TVPGuidelineSafeArea(**guideline) - def tv_guideline_add_vanish_point_1( x: float | None = None, @@ -1027,7 +978,7 @@ def tv_guideline_add_vanish_point_1( { "x": x, "y": y, - "grid": grid, + "grid": int(grid) if grid is not None else None, } ) @@ -1043,14 +994,13 @@ def tv_guideline_add_vanish_point_1( def tv_guideline_modify_vanish_point_1_get(position: int) -> TVPGuidelineVanishPoint1: """Get info for the vanish point 1 guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint1) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint1) guideline["position"] = position return TVPGuidelineVanishPoint1(**guideline) @@ -1061,28 +1011,24 @@ def tv_guideline_modify_vanish_point_1_set( y: float | None = None, ray: int | None = None, grid: bool | None = None, -) -> TVPGuidelineVanishPoint1: +) -> None: """Set info for the vanish point 1 guideline at the given position.""" args = args_dict_to_list( { - "position": position, "x": x, "y": y, "ray": ray, - "grid": grid, + "grid": int(grid) if grid is not None else None, } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint1) - guideline["position"] = position - return TVPGuidelineVanishPoint1(**guideline) - def tv_guideline_add_vanish_point_2( x1: float | None = None, @@ -1112,14 +1058,13 @@ def tv_guideline_add_vanish_point_2( def tv_guideline_modify_vanish_point_2_get(position: int) -> TVPGuidelineVanishPoint2: """Get info for the vanish point 2 guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint2) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint2) guideline["position"] = position return TVPGuidelineVanishPoint2(**guideline) @@ -1131,11 +1076,10 @@ def tv_guideline_modify_vanish_point_2_set( x2: float | None = None, y2: float | None = None, ray: int | None = None, -) -> TVPGuidelineVanishPoint2: +) -> None: """Set info for the vanish point 2 guideline at the given position.""" args = args_dict_to_list( { - "position": position, "x1": x1, "y1": y1, "x2": x2, @@ -1144,16 +1088,13 @@ def tv_guideline_modify_vanish_point_2_set( } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint2) - guideline["position"] = position - return TVPGuidelineVanishPoint2(**guideline) - def tv_guideline_add_vanish_point_3( x1: float | None = None, @@ -1187,14 +1128,13 @@ def tv_guideline_add_vanish_point_3( def tv_guideline_modify_vanish_point_3_get(position: int) -> TVPGuidelineVanishPoint3: """Get info for the vanish point 3 guideline at the given position.""" - result = send_cmd( "tv_GuidelineModify", position, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint3) + guideline: dict[str, Any] = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint3) guideline["position"] = position return TVPGuidelineVanishPoint3(**guideline) @@ -1208,11 +1148,10 @@ def tv_guideline_modify_vanish_point_3_set( x3: float | None = None, y3: float | None = None, ray: int | None = None, -) -> TVPGuidelineVanishPoint3: +) -> None: """Set info for the vanish point 3 guideline at the given position.""" args = args_dict_to_list( { - "position": position, "x1": x1, "y1": y1, "x2": x2, @@ -1223,16 +1162,13 @@ def tv_guideline_modify_vanish_point_3_set( } ) - result = send_cmd( + send_cmd( "tv_GuidelineModify", + position, *args, error_values=[-1, -2], ) - guideline = tv_parse_dict(result, with_fields=TVPGuidelineVanishPoint3) - guideline["position"] = position - return TVPGuidelineVanishPoint3(**guideline) - def tv_guideline_add_field_chart() -> int: """Set info for the image guideline at the given position.""" diff --git a/pytvpaint/george/grg_layer.py b/pytvpaint/george/grg_layer.py index eb45b8c..bc6009e 100644 --- a/pytvpaint/george/grg_layer.py +++ b/pytvpaint/george/grg_layer.py @@ -254,15 +254,13 @@ def tv_layer_move(position: int, folder_id: int | None = None) -> None: Args: position: position to move layer to - folder_id: parent folder id, + folder_id: parent folder id Raises: GeorgeError: if layer could not be moved """ if folder_id is not None and is_tvp_version_below_12(): - log.warning( - "`folder_id` option is only available in TVPaint version 12 and above." - ) + log.warning("`folder_id` option is only available in TVPaint version 12 and above.") args = [position] if not is_tvp_version_below_12() and folder_id: @@ -355,35 +353,48 @@ def tv_layer_create(name: str, layer_type: int = 1) -> int: name: layer name layer_type: 1 for normal layer, 0 for Folder layer (only available for TVPaint 12 and above) + Returns: + layer_id: the id of the newly created layer. """ if layer_type != 1 and is_tvp_version_below_12(): - log.warning( - "`layer_type` selection is only available in TVPaint version 12 and above." - ) + log.warning("`layer_type` selection is only available in TVPaint version 12 and above.") args = [name] if not is_tvp_version_below_12(): args.append(str(layer_type)) + if not name: + log.warning( + "You should provide a name for the layer, " + "otherwise tvpaint will consider that the layer type is the name." + ) - return int(send_cmd("tv_LayerCreate", *args, handle_string=False)) + return int(send_cmd("tv_LayerCreate", *args)) def tv_layer_duplicate(name: str) -> int: """Duplicate the current layer and make it the current one.""" - return int(send_cmd("tv_LayerDuplicate", name, handle_string=False)) + return int(send_cmd("tv_LayerDuplicate", name)) @try_cmd( raise_exc=NoObjectWithIdError, exception_msg="Invalid layer id", ) -def tv_layer_rename(layer_id: int, name: str) -> None: +def tv_layer_rename(layer_id: int, name: str) -> str: """Rename a layer. Raises: - NoObjectWithIdError: if given an invalid layer id + NoObjectWithIdError: if given an invalid layer id. + + Warning: + This function does not seem to work in TVPaint 12. + + Returns: + str: new name """ - send_cmd("tv_LayerRename", layer_id, name) + if not is_tvp_version_below_12(): + log.warning("function `tv_LayerRename` does not seem to work properly in TVPaint 12") + return send_cmd("tv_LayerRename", layer_id, name, error_values=[-1]) @try_cmd( @@ -442,9 +453,7 @@ def tv_layer_display_get(layer_id: int) -> bool: raise_exc=NoObjectWithIdError, exception_msg="Invalid layer id", ) -def tv_layer_display_set( - layer_id: int, new_state: bool, light_table: bool = False -) -> None: +def tv_layer_display_set(layer_id: int, new_state: bool, light_table: bool = False) -> None: """Set the visibility of the given layer. Raises: @@ -570,10 +579,7 @@ def tv_layer_stencil_set(layer_id: int, mode: StencilMode) -> None: Raises: NoObjectWithIdError: if given an invalid layer id """ - if mode in [StencilMode.ON, StencilMode.OFF]: - args = [mode.value] - else: - args = ["on", mode.value] + args = [mode.value] if mode in [StencilMode.ON, StencilMode.OFF] else ["on", mode.value] send_cmd("tv_LayerStencil", layer_id, *args) @@ -590,9 +596,7 @@ def tv_layer_show_thumbnails_get( Raises: NoObjectWithIdError: if given an invalid layer id """ - res = send_cmd( - "tv_LayerShowThumbnails", layer_id, error_values=[GrgErrorValue.ERROR] - ) + res = send_cmd("tv_LayerShowThumbnails", layer_id, error_values=[GrgErrorValue.ERROR]) return res == "1" @@ -681,9 +685,7 @@ def tv_layer_auto_create_instance_set( Raises: NoObjectWithIdError: if given an invalid layer id """ - send_cmd( - "tv_LayerAutoCreateInstance", layer_id, int(state), error_values=[-1, -2, -3] - ) + send_cmd("tv_LayerAutoCreateInstance", layer_id, int(state), error_values=[-1, -2, -3]) @try_cmd( @@ -775,7 +777,13 @@ def tv_preserve_get() -> LayerTransparency: def tv_preserve_set(state: LayerTransparency) -> None: - """Set the preserve transparency state of the current layer.""" + """Set the preserve transparency state of the current layer. + + Warning: + This function does not seem to work in TVPaint 12 + """ + if not is_tvp_version_below_12(): + log.warning("This function does not seem to work in TVPaint 12") send_cmd("tv_Preserve", "alpha", state.value) @@ -796,7 +804,12 @@ def tv_layer_mark_get(layer_id: int, frame: int) -> int: Returns: int: the mark color index """ - return int(send_cmd("tv_LayerMarkGet", layer_id, frame)) + res = send_cmd("tv_LayerMarkGet", layer_id, frame, error_values=[-1]) + if is_tvp_version_below_12(): + return int(res) + + index, _, _, _ = tv_cast_to_type(res, tuple[int, ...]) + return index @try_cmd( @@ -953,7 +966,7 @@ def tv_layer_color_get_color(clip_id: int, color_index: int) -> TVPClipLayerColo LayerColorAction.GETCOLOR.value, clip_id, color_index, - error_values=[GrgErrorValue.ERROR], + error_values=[GrgErrorValue.ERROR, -1, -2], ) parsed = tv_parse_list(result, with_fields=TVPClipLayerColor) return TVPClipLayerColor(**parsed) @@ -974,6 +987,9 @@ def tv_layer_color_set_color( Raises: NoObjectWithIdError: if given an invalid layer id + Warning: + In tvpaint 12, this function will fail when the `name` argument is provided + Note: The color with index 0 is the "Default" color, and it can't be changed """ @@ -987,6 +1003,8 @@ def tv_layer_color_set_color( ] if name: + if not is_tvp_version_below_12(): + log.warning("In tvpaint 12, this function will fail when the `name` argument is provided") args.append(name) send_cmd("tv_LayerColor", *args, error_values=[GrgErrorValue.ERROR]) @@ -1008,7 +1026,11 @@ def tv_layer_color_get(layer_id: int) -> int: layer_id, error_values=[-1], ) - return int(res) + if is_tvp_version_below_12(): + return int(res) + + index, _, _, _ = tv_cast_to_type(res, tuple[int, ...]) + return index @try_cmd(raise_exc=NoObjectWithIdError) @@ -1102,14 +1124,16 @@ def tv_layer_color_visible(color_index: int) -> bool: Raises: NoObjectWithIdError: if given an invalid layer id """ - return bool(int( - send_cmd( - "tv_LayerColor", - LayerColorAction.VISIBLE.value, - color_index, - error_values=[-1], + return bool( + int( + send_cmd( + "tv_LayerColor", + LayerColorAction.VISIBLE.value, + color_index, + error_values=[-1], + ) ) - )) + ) @try_cmd( @@ -1254,6 +1278,9 @@ def tv_exposure_prev() -> int: def tv_save_image(export_path: Path | str) -> None: """Save the current image of the current layer. + Warnings: + This function outputs very low quality images, we recommend using other rendering functions. + Raises: GeorgeError: if the file couldn't be saved or an invalid format was provided """ @@ -1406,7 +1433,7 @@ def tv_ctg_source_remove(ctg_layer_id: int, source_ids: list[int]) -> None: def tv_panning(x: int, y: int, move_fill: bool = False, anti_aliasing: bool = False) -> None: - """Apply a panning FX to teh current layer. + """Apply a panning FX to the current layer. Args: x: new x position of the layer (position is calculated from the top left corner of the layer frame) @@ -1414,5 +1441,4 @@ def tv_panning(x: int, y: int, move_fill: bool = False, anti_aliasing: bool = Fa move_fill: True to moved and fill all screen, False to only move images anti_aliasing: apply antialiasing """ - anti_aliasing = 0 if not anti_aliasing else 2 - send_cmd("tv_Panning ", x, y, int(move_fill), anti_aliasing) + send_cmd("tv_Panning ", x, y, int(move_fill), (2 if anti_aliasing else 0)) diff --git a/pytvpaint/george/grg_project.py b/pytvpaint/george/grg_project.py index bead6a8..4eb7ba5 100644 --- a/pytvpaint/george/grg_project.py +++ b/pytvpaint/george/grg_project.py @@ -64,12 +64,12 @@ def tv_project_info(project_id: str) -> TVPProject: Raises: NoObjectWithIdError: if given an invalid project id """ - result = send_cmd("tv_ProjectInfo", project_id, error_values=[GrgErrorValue.EMPTY]) + result = send_cmd("tv_ProjectInfo", project_id, error_values=[GrgErrorValue.EMPTY, -1]) fields = get_dataclass_fields(cast(DataclassInstance, TVPProject)) if not is_tvp_version_below_12(): # values of field_order have been removed in versions > 12 so for now we provide it ourselves - fields_keys: list[str] = list(dict(fields).keys()) + fields_keys = list(dict(fields).keys()) field_order_index, start_frame_index = fields_keys.index("field_order"), fields_keys.index("start_frame") fields[field_order_index], fields[start_frame_index] = ( fields[start_frame_index], diff --git a/pytvpaint/george/grg_scene.py b/pytvpaint/george/grg_scene.py index 1a54679..e0a9940 100644 --- a/pytvpaint/george/grg_scene.py +++ b/pytvpaint/george/grg_scene.py @@ -53,7 +53,7 @@ def tv_scene_create(clips: list[str]) -> int: scene_id: new scene id Warning: - This function doesn't seem to work for now. + function `tv_scene_create` doesn't seem to work in tvpaint. """ return int(send_cmd("tv_SceneCreate", *clips)) diff --git a/pytvpaint/guideline.py b/pytvpaint/guideline.py index b1b238c..12c054a 100644 --- a/pytvpaint/guideline.py +++ b/pytvpaint/guideline.py @@ -3,8 +3,7 @@ from __future__ import annotations from pathlib import Path -from typing import TypeVar, Generic -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generic, TypeVar from pytvpaint import george from pytvpaint.utils import ( @@ -22,7 +21,7 @@ class Guideline(Removable, Generic[GuidelineDT, GuidelineT]): """A Guideline is an image or shape used as guideline for artists.""" - TYPE: GuidelineT = None + TYPE: GuidelineT def __init__( self, @@ -35,10 +34,6 @@ def __init__( self._project: Project = project self._data = data - def refresh(self) -> None: - """Refreshes the guideline data.""" - super().refresh() - def __repr__(self) -> str: """String representation of the camera point.""" return f"{self.__class__.__name__}({self.name})" @@ -58,7 +53,7 @@ def position(self) -> int: return self._position @property - def data(self) -> GuidelineDT: + def data(self) -> GuidelineDT | None: """Returns the raw data of the guideline.""" return self._data @@ -69,6 +64,7 @@ def project(self) -> Project: @property def name(self) -> str: + """The name of the guideline.""" return george.tv_guideline_name_get(self.position) @name.setter @@ -77,6 +73,7 @@ def name(self, value: str) -> None: @property def is_visible(self) -> bool: + """The guideline visibility.""" return george.tv_guideline_visibility_get(self.position) @is_visible.setter @@ -85,6 +82,7 @@ def is_visible(self, value: bool) -> None: @property def margin(self) -> int: + """The guideline margin.""" return george.tv_guideline_margin_get(self.position) @margin.setter @@ -93,6 +91,7 @@ def margin(self, value: int) -> None: @property def color(self) -> george.RGBAColor: + """The guideline color.""" return george.tv_guideline_color_get(self.position) @color.setter @@ -101,20 +100,54 @@ def color(self, value: george.RGBAColor) -> None: @property def snap(self) -> bool: + """The guideline snap state.""" return george.tv_guideline_snap_get(self.position) @snap.setter def snap(self, value: bool) -> None: + if not self.is_visible: + raise ValueError("Snap will not change if guideline is not visible") george.tv_guideline_snap_set(self.position, value) @property def collapse(self) -> bool: + """The guideline collapse state.""" return george.tv_guideline_collapse_get(self.position) @collapse.setter def collapse(self, value: bool) -> None: george.tv_guideline_collapse_set(self.position, value) + @classmethod + def set_all_visible(cls, value: bool) -> None: + """Set visibility state on all guidelines.""" + george.tv_guideline_visibility_set_all(cls.TYPE, value) + + @classmethod + def set_all_margin(cls, value: int) -> None: + """Set the margin on all guidelines.""" + george.tv_guideline_margin_set_all(cls.TYPE, value) + + @classmethod + def set_all_color(cls, value: george.RGBAColor) -> None: + """Set the color on all guidelines.""" + george.tv_guideline_color_set_all(cls.TYPE, value) + + @classmethod + def set_all_snap(cls, value: bool) -> None: + """Set the snap state on all guidelines.""" + george.tv_guideline_snap_set_all(cls.TYPE, value) + + @staticmethod + def set_global_visible(value: bool) -> None: + """Set the visibility on the global guideline.""" + george.tv_guideline_visibility_set(0, value, on_global=True) + + @staticmethod + def set_global_snap(value: bool) -> None: + """Set snap state on the global guideline.""" + george.tv_guideline_snap_set(0, value, on_global=True) + def remove(self) -> None: """Remove the guideline. @@ -127,6 +160,7 @@ def remove(self) -> None: class GuidelineImage(Guideline[george.TVPGuidelineImage, george.GuidelineType]): + """A GuidelineImage is an image used as guideline for artists.""" TYPE = george.GuidelineType.IMAGE @@ -137,10 +171,10 @@ def __init__( data: george.TVPGuidelineImage | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_image_get(self._position) + self._data: george.TVPGuidelineImage = data or george.tv_guideline_modify_image_get(self._position) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -220,6 +254,7 @@ def new( class GuidelineLine(Guideline[george.TVPGuidelineLine, george.GuidelineType]): + """A GuidelineLine is a line used as guideline for artists.""" TYPE = george.GuidelineType.LINE @@ -230,10 +265,10 @@ def __init__( data: george.TVPGuidelineLine | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_line_get(self._position) + self._data: george.TVPGuidelineLine = data or george.tv_guideline_modify_line_get(self._position) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -241,7 +276,7 @@ def refresh(self) -> None: @refreshed_property def x(self) -> float: - """The x coordinate of the image.""" + """The x coordinate of the guideline.""" return self._data.x @x.setter @@ -250,7 +285,7 @@ def x(self, value: float) -> None: @refreshed_property def y(self) -> float: - """The y coordinate of the image.""" + """The y coordinate of the guideline.""" return self._data.y @y.setter @@ -259,7 +294,7 @@ def y(self, value: float) -> None: @refreshed_property def angle(self) -> float: - """The rotation of the image.""" + """The angle of the guideline.""" return self._data.angle @angle.setter @@ -282,6 +317,7 @@ def new( class GuidelineSegment(Guideline[george.TVPGuidelineSegment, george.GuidelineType]): + """A GuidelineSegment is a segment drawing used as guideline for artists.""" TYPE = george.GuidelineType.SEGMENT @@ -292,10 +328,10 @@ def __init__( data: george.TVPGuidelineSegment | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_segment_get(self._position) + self._data: george.TVPGuidelineSegment = data or george.tv_guideline_modify_segment_get(self._position) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -303,7 +339,7 @@ def refresh(self) -> None: @refreshed_property def x1(self) -> float: - """The x coordinate of the image.""" + """The x coordinate of the guideline.""" return self._data.x1 @x1.setter @@ -312,7 +348,7 @@ def x1(self, value: float) -> None: @refreshed_property def y1(self) -> float: - """The y coordinate of the image.""" + """The y coordinate of the guideline.""" return self._data.y1 @y1.setter @@ -321,7 +357,7 @@ def y1(self, value: float) -> None: @refreshed_property def x2(self) -> float: - """The x coordinate of the image.""" + """The x2 coordinate of the guideline.""" return self._data.x2 @x2.setter @@ -330,7 +366,7 @@ def x2(self, value: float) -> None: @refreshed_property def y2(self) -> float: - """The y coordinate of the image.""" + """The y2 coordinate of the guideline.""" return self._data.y2 @y2.setter @@ -354,6 +390,7 @@ def new( class GuidelineCircle(Guideline[george.TVPGuidelineCircle, george.GuidelineType]): + """A GuidelineCircle is a circle used as guideline for artists.""" TYPE = george.GuidelineType.CIRCLE @@ -364,10 +401,10 @@ def __init__( data: george.TVPGuidelineCircle | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_circle_get(self._position) + self._data: george.TVPGuidelineCircle = data or george.tv_guideline_modify_circle_get(self._position) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -375,7 +412,7 @@ def refresh(self) -> None: @refreshed_property def x(self) -> float: - """The x coordinate of the image.""" + """The x coordinate of the guideline.""" return self._data.x @x.setter @@ -384,7 +421,7 @@ def x(self, value: float) -> None: @refreshed_property def y(self) -> float: - """The y coordinate of the image.""" + """The y coordinate of the guideline.""" return self._data.y @y.setter @@ -393,7 +430,7 @@ def y(self, value: float) -> None: @refreshed_property def radius(self) -> float: - """The rotation of the image.""" + """The radius of the guideline.""" return self._data.radius @radius.setter @@ -416,6 +453,7 @@ def new( class GuidelineEllipse(Guideline[george.TVPGuidelineEllipse, george.GuidelineType]): + """A GuidelineEllipse is an ellipse used as guideline for artists.""" TYPE = george.GuidelineType.ELLIPSE @@ -426,10 +464,10 @@ def __init__( data: george.TVPGuidelineEllipse | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_ellipse_get(self._position) + self._data: george.TVPGuidelineEllipse = data or george.tv_guideline_modify_ellipse_get(self._position) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -437,7 +475,7 @@ def refresh(self) -> None: @refreshed_property def x(self) -> float: - """The x coordinate of the image.""" + """The x coordinate of the guideline.""" return self._data.x @x.setter @@ -446,7 +484,7 @@ def x(self, value: float) -> None: @refreshed_property def y(self) -> float: - """The y coordinate of the image.""" + """The y coordinate of the guideline.""" return self._data.y @y.setter @@ -455,7 +493,7 @@ def y(self, value: float) -> None: @refreshed_property def radius_a(self) -> float: - """The rotation of the image.""" + """The radius_a of the guideline.""" return self._data.radius_a @radius_a.setter @@ -464,7 +502,7 @@ def radius_a(self, value: float) -> None: @refreshed_property def radius_b(self) -> float: - """The rotation of the image.""" + """The radius_b of the guideline.""" return self._data.radius_b @radius_b.setter @@ -488,6 +526,7 @@ def new( class GuidelineGrid(Guideline[george.TVPGuidelineGrid, george.GuidelineType]): + """A GuidelineGrid is a grid used as guideline for artists.""" TYPE = george.GuidelineType.GRID @@ -498,10 +537,10 @@ def __init__( data: george.TVPGuidelineGrid | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_grid_get(self._position) + self._data: george.TVPGuidelineGrid = data or george.tv_guideline_modify_grid_get(self._position) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -509,7 +548,7 @@ def refresh(self) -> None: @refreshed_property def x(self) -> float: - """The x coordinate of the image.""" + """The x coordinate of the guideline.""" return self._data.x @x.setter @@ -518,7 +557,7 @@ def x(self, value: float) -> None: @refreshed_property def y(self) -> float: - """The y coordinate of the image.""" + """The y coordinate of the guideline.""" return self._data.y @y.setter @@ -527,8 +566,8 @@ def y(self, value: float) -> None: @refreshed_property def width(self) -> float: - """The rotation of the image.""" - return self._data.w + """The width of the guideline.""" + return self._data.width @width.setter def width(self, value: float) -> None: @@ -536,7 +575,7 @@ def width(self, value: float) -> None: @refreshed_property def height(self) -> float: - """The rotation of the image.""" + """The height of the guideline.""" return self._data.height @height.setter @@ -560,6 +599,7 @@ def new( class GuidelineMarks(Guideline[george.TVPGuidelineMarks, george.GuidelineType]): + """A GuidelineCircle is a set of marks used as guidelines for artists.""" TYPE = george.GuidelineType.MARKS @@ -570,31 +610,41 @@ def __init__( data: george.TVPGuidelineMarks | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_marks_get(self._position) + self._data: george.TVPGuidelineMarks = data or george.tv_guideline_modify_marks_get(self._position) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return self._data = george.tv_guideline_modify_marks_get(self._position) @refreshed_property - def count_x(self) -> float: - """The x coordinate of the image.""" + def count_x(self) -> int: + """The number of vertical marks.""" return self._data.count_x @count_x.setter - def count_x(self, value: float) -> None: + def count_x(self, value: int) -> None: + """The number of vertical marks. + + Warnings: + function GuidelineMarks.x doesn't seem to work in tvpaint, values are never changed. + """ george.tv_guideline_modify_marks_set(self.position, count_x=value) @refreshed_property - def count_y(self) -> float: - """The y coordinate of the image.""" + def count_y(self) -> int: + """The number of horizontal marks.""" return self._data.count_y @count_y.setter - def count_y(self, value: float) -> None: + def count_y(self, value: int) -> None: + """The number of vertical marks. + + Warnings: + function GuidelineMarks.y doesn't seem to work in tvpaint, values are never changed. + """ george.tv_guideline_modify_marks_set(self.position, count_y=value) @classmethod @@ -612,6 +662,7 @@ def new( class GuidelineFieldChart(Guideline[george.TVPGuideField, george.GuidelineType]): + """A GuidelineFieldChart is a field chart used as guideline for artists.""" TYPE = george.GuidelineType.FIELD_CHART @@ -633,6 +684,7 @@ def new(cls, project: Project) -> GuidelineFieldChart: class GuidelineAnimatorField(Guideline[george.TVPGuideField, george.GuidelineType]): + """A GuidelineAnimatorField is a shape used as guideline for artists.""" TYPE = george.GuidelineType.ANIMATOR_FIELD @@ -654,6 +706,7 @@ def new(cls, project: Project) -> GuidelineAnimatorField: class GuidelineSafeArea(Guideline[george.TVPGuidelineSafeArea, george.GuidelineType]): + """A GuidelineSafeArea is an area used as guideline for artists.""" TYPE = george.GuidelineType.SAFE_AREA @@ -664,10 +717,10 @@ def __init__( data: george.TVPGuidelineSafeArea | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_safe_area_get(self._position) + self._data: george.TVPGuidelineSafeArea = data or george.tv_guideline_modify_safe_area_get(self._position) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -675,7 +728,7 @@ def refresh(self) -> None: @refreshed_property def sf_out(self) -> float: - """The x coordinate of the image.""" + """The out value of the safe area.""" return self._data.sf_out @sf_out.setter @@ -684,7 +737,7 @@ def sf_out(self, value: float) -> None: @refreshed_property def sf_in(self) -> float: - """The y coordinate of the image.""" + """The in value of the safe area.""" return self._data.sf_in @sf_in.setter @@ -706,6 +759,7 @@ def new( class GuidelineVanishPoint1(Guideline[george.TVPGuidelineVanishPoint1, george.GuidelineType]): + """A GuidelineVanishPoint1 is a point used as guideline for artists.""" TYPE = george.GuidelineType.VANISH_POINT_1 @@ -716,10 +770,12 @@ def __init__( data: george.TVPGuidelineVanishPoint1 | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_vanish_point_1_get(self._position) + self._data: george.TVPGuidelineVanishPoint1 = data or george.tv_guideline_modify_vanish_point_1_get( + self._position + ) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -727,7 +783,7 @@ def refresh(self) -> None: @refreshed_property def x(self) -> float: - """The x coordinate of the image.""" + """The x coordinate of the guideline.""" return self._data.x @x.setter @@ -736,7 +792,7 @@ def x(self, value: float) -> None: @refreshed_property def y(self) -> float: - """The y coordinate of the image.""" + """The y coordinate of the guideline.""" return self._data.y @y.setter @@ -745,8 +801,8 @@ def y(self, value: float) -> None: @refreshed_property def ray(self) -> int: - """The rotation of the image.""" - return self._data.grid + """The number of rays in the guideline.""" + return self._data.ray @ray.setter def ray(self, value: int) -> None: @@ -754,7 +810,7 @@ def ray(self, value: int) -> None: @refreshed_property def grid(self) -> bool: - """The rotation of the image.""" + """The grid state of the guideline.""" return self._data.grid @grid.setter @@ -777,6 +833,7 @@ def new( class GuidelineVanishPoint2(Guideline[george.TVPGuidelineVanishPoint2, george.GuidelineType]): + """A GuidelineVanishPoint2 is a set of points used as guideline for artists.""" TYPE = george.GuidelineType.VANISH_POINT_2 @@ -787,10 +844,12 @@ def __init__( data: george.TVPGuidelineVanishPoint2 | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_vanish_point_2_get(self._position) + self._data: george.TVPGuidelineVanishPoint2 = data or george.tv_guideline_modify_vanish_point_2_get( + self._position + ) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return @@ -798,7 +857,7 @@ def refresh(self) -> None: @refreshed_property def x1(self) -> float: - """The x coordinate of the image.""" + """The x1 coordinate of the guideline.""" return self._data.x1 @x1.setter @@ -807,7 +866,7 @@ def x1(self, value: float) -> None: @refreshed_property def y1(self) -> float: - """The y coordinate of the image.""" + """The y1 coordinate of the guideline.""" return self._data.y1 @y1.setter @@ -816,7 +875,7 @@ def y1(self, value: float) -> None: @refreshed_property def x2(self) -> float: - """The x coordinate of the image.""" + """The x2 coordinate of the guideline.""" return self._data.x2 @x2.setter @@ -825,13 +884,22 @@ def x2(self, value: float) -> None: @refreshed_property def y2(self) -> float: - """The y coordinate of the image.""" + """The y2 coordinate of the guideline.""" return self._data.y2 @y2.setter def y2(self, value: float) -> None: george.tv_guideline_modify_vanish_point_2_set(self.position, y2=value) + @refreshed_property + def ray(self) -> int: + """The number of rays in the guideline.""" + return self._data.ray + + @ray.setter + def ray(self, value: int) -> None: + george.tv_guideline_modify_vanish_point_2_set(self.position, ray=value) + @classmethod def new( cls, @@ -848,7 +916,8 @@ def new( return cls(position, project) -class GuidelineVanishPoint3(GuidelineVanishPoint2): +class GuidelineVanishPoint3(Guideline[george.TVPGuidelineVanishPoint3, george.GuidelineType]): + """A GuidelineVanishPoint3 is a set of points used as guideline for artists.""" TYPE = george.GuidelineType.VANISH_POINT_3 @@ -859,24 +928,80 @@ def __init__( data: george.TVPGuidelineVanishPoint3 | None = None, ) -> None: super().__init__(position, project) - self._data = data or george.tv_guideline_modify_vanish_point_3_get(self._position) + self._data: george.TVPGuidelineVanishPoint3 = data or george.tv_guideline_modify_vanish_point_3_get( + self._position + ) def refresh(self) -> None: - """Refreshes the camera point data.""" + """Refreshes the guideline data.""" super().refresh() if not self.refresh_on_call and self._data: return self._data = george.tv_guideline_modify_vanish_point_3_get(self._position) + @refreshed_property + def x1(self) -> float: + """The x1 coordinate of the guideline.""" + return self._data.x1 + + @x1.setter + def x1(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_3_set(self.position, x1=value) + + @refreshed_property + def y1(self) -> float: + """The y1 coordinate of the guideline.""" + return self._data.y1 + + @y1.setter + def y1(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_3_set(self.position, y1=value) + + @refreshed_property + def x2(self) -> float: + """The x2 coordinate of the guideline.""" + return self._data.x2 + + @x2.setter + def x2(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_3_set(self.position, x2=value) + + @refreshed_property + def y2(self) -> float: + """The y2 coordinate of the guideline.""" + return self._data.y2 + + @y2.setter + def y2(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_3_set(self.position, y2=value) + + @refreshed_property + def x3(self) -> float: + """The x3 coordinate of the guideline.""" + return self._data.y3 + + @x3.setter + def x3(self, value: float) -> None: + george.tv_guideline_modify_vanish_point_3_set(self.position, x3=value) + @refreshed_property def y3(self) -> float: - """The y coordinate of the image.""" + """The y3 coordinate of the guideline.""" return self._data.y3 @y3.setter def y3(self, value: float) -> None: george.tv_guideline_modify_vanish_point_3_set(self.position, y3=value) + @refreshed_property + def ray(self) -> int: + """The number of rays in the guideline.""" + return self._data.ray + + @ray.setter + def ray(self, value: int) -> None: + george.tv_guideline_modify_vanish_point_3_set(self.position, ray=value) + @classmethod def new( cls, diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index f6c0cf0..f444dd0 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -400,7 +400,7 @@ def position(self, value: int) -> None: """Moves the layer to the provided position. Note: - This function fixes the issues with positions not been set correctly by TVPaint when value is superior to 0 + This function fixes the issues with positions been set at (value-1) by TVPaint when value is superior to 0 """ if self.position == value: return @@ -936,10 +936,10 @@ def render( """ start = self.start if start is None else start end = self.end if end is None else end + frame_set = FrameSet(f"{start}-{end}") self.clip.render( output_path=output_path, - start=start, - end=end, + frame_set=frame_set, use_camera=use_camera, layer_selection=[self], alpha_mode=alpha_mode, @@ -955,6 +955,7 @@ def render_frame( alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = george.BackgroundMode.NONE, format_opts: list[str] | None = None, + use_camera: bool = False, ) -> Path: """Render a frame from the layer. @@ -964,6 +965,7 @@ def render_frame( alpha_mode: the render alpha mode background_mode: the render background mode format_opts: custom output format options to pass when rendering + use_camera: use the camera for rendering, otherwise render the whole canvas. Defaults to False. Raises: FileNotFoundError: if the render failed or output not found on disk @@ -972,16 +974,12 @@ def render_frame( Path: render output path """ export_path = Path(export_path) - save_format = george.SaveFormat.from_extension(export_path.suffix) - export_path.parent.mkdir(parents=True, exist_ok=True) - frame = frame or self.clip.current_frame self.clip.current_frame = frame self.clip.render( - output_path=output_path, - start=frame, - end=frame, + output_path=export_path, + frame_set=FrameSet(frame), use_camera=use_camera, layer_selection=[self], alpha_mode=alpha_mode, @@ -999,7 +997,8 @@ def render_instances( alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = None, format_opts: list[str] | None = None, - ) -> FileSequence: + use_camera: bool = False, + ) -> None: """Render all layer instances in the provided range for the current layer. Args: @@ -1009,6 +1008,7 @@ def render_instances( alpha_mode: the render alpha mode background_mode: the render background mode format_opts: custom output format options to pass when rendering + use_camera: use the camera for rendering, otherwise render the whole canvas. Defaults to False. Raises: ValueError: if requested range (start-end) not in layer range/bounds @@ -1018,25 +1018,17 @@ def render_instances( Returns: FileSequence: instances output sequence """ - file_sequence, start, end, is_sequence, is_image = utils.handle_output_range( - export_path, self.start, self.end, start, end - ) - - if start < self.start or end > self.end: - raise ValueError(f"Render ({start}-{end}) not in clip range ({(self.start, self.end)})") - if not is_image: - raise ValueError(f"Video formats ({file_sequence.extension()}) are not supported for instance rendering !") - - # render to output - frames = [] - for layer_instance in self.instances: - cur_frame = layer_instance.start - instance_output = Path(file_sequence.frame(cur_frame)) - self.render_frame(instance_output, cur_frame, alpha_mode, background_mode, format_opts) - frames.append(str(cur_frame)) + frames = [layer_instance.start for layer_instance in self.instances] - file_sequence.setFrameSet(FrameSet(",".join(frames))) - return file_sequence + self.clip.render( + output_path=export_path, + frame_set=FrameSet(frames), + use_camera=use_camera, + layer_selection=[self], + alpha_mode=alpha_mode, + background_mode=background_mode, + format_opts=format_opts, + ) @set_as_current def load_image(self, image_path: str | Path, frame: int | None = None, stretch: bool = False) -> None: @@ -1123,7 +1115,7 @@ def clear_marks(self) -> None: @set_as_current def pan(self, position: tuple[int, int], move_fill: bool = False, anti_aliasing: bool = False) -> None: - """Apply a panning FX to teh current layer. + """Apply a panning FX to the current layer. Args: position: new position of the layer (position is calculated from the top left corner of the layer frame) @@ -1195,6 +1187,12 @@ def copy(self) -> None: self.select_all_frames() self.copy_selection() + @set_as_current + def paste(self) -> None: + """Copy the layer (copies all instances in layer).""" + self.select_all_frames() + self.paste_selection() + @refreshed_property @set_as_current def instances(self) -> Iterator[LayerInstance]: @@ -1371,7 +1369,7 @@ def __init__( @property @george.min_version_compatible(min_version="12") def camera(self) -> Camera | None: - """Returns the Camera object linked ot this layer, if no camera is linked to teh layer, returns None.""" + """Returns the Camera object linked ot this layer, if no camera is linked to the layer, returns None.""" if self.layer_type != george.LayerType.CAMERA: return None return self.clip.camera diff --git a/pytvpaint/project.py b/pytvpaint/project.py index d8a9ad1..7380e96 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -3,17 +3,17 @@ from __future__ import annotations import re -from pathlib import Path from collections.abc import Iterator -from typing import TYPE_CHECKING +from pathlib import Path +from typing import TYPE_CHECKING, Any from fileseq.filesequence import FileSequence +from fileseq.frameset import FrameSet -from pytvpaint import george, utils +from pytvpaint import george, guideline, utils from pytvpaint.george.client import parse from pytvpaint.george.exceptions import GeorgeError from pytvpaint.sound import ProjectSound -from pytvpaint import guideline from pytvpaint.utils import ( Refreshable, Renderable, @@ -335,7 +335,6 @@ def get_project( Returns: Project | None: the searched element or None if search was unsuccessful """ - return utils.get_tvp_element( Project.open_projects(), by_id=by_id, by_name=by_name, by_regex=by_regex, by_path=by_path ) @@ -435,7 +434,7 @@ def get_clip( """ clips = self.clips if scene_id: - selected_scene = self.get_scene(by_id=scene_id) + selected_scene = self.get_scene(scene_id=scene_id) clips = selected_scene.clips if selected_scene else clips return utils.get_tvp_element(clips, by_id=by_id, by_name=by_name, by_regex=by_regex) @@ -458,26 +457,28 @@ def add_sound(self, sound_path: Path | str) -> ProjectSound: return ProjectSound.new(sound_path, parent=self) @set_as_current - def guidelines(self, guideline_type: george.GuidelineType | None = None) -> Iterator[guideline.Guideline]: + def guidelines(self, by_type: george.GuidelineType | None = None) -> Iterator[guideline.Guideline[Any, Any]]: """Iterator for the `Guideline` objects of the project.""" - guideline_classes = [ - guideline.GuidelineImage, - guideline.GuidelineLine, - guideline.GuidelineSegment, - guideline.GuidelineCircle, - guideline.GuidelineEllipse, - guideline.GuidelineGrid, - guideline.GuidelineMarks, - guideline.GuidelineSafeArea, - guideline.GuidelineFieldChart, - guideline.GuidelineAnimatorField, - guideline.GuidelineVanishPoint1, - guideline.GuidelineVanishPoint2, - guideline.GuidelineVanishPoint3, - ] - guideline_classes = {c.TYPE: c for c in guideline_classes} + guideline_classes = { + c.TYPE: c # type: ignore[attr-defined] + for c in [ + guideline.GuidelineImage, + guideline.GuidelineLine, + guideline.GuidelineSegment, + guideline.GuidelineCircle, + guideline.GuidelineEllipse, + guideline.GuidelineGrid, + guideline.GuidelineMarks, + guideline.GuidelineSafeArea, + guideline.GuidelineFieldChart, + guideline.GuidelineAnimatorField, + guideline.GuidelineVanishPoint1, + guideline.GuidelineVanishPoint2, + guideline.GuidelineVanishPoint3, + ] + } for g_type in george.GuidelineType: - if guideline_type and guideline_type != g_type: + if by_type and by_type != g_type: continue if not guideline_classes.get(g_type): continue @@ -501,6 +502,113 @@ def add_guideline_image( """Add a new image guideline to the project.""" return guideline.GuidelineImage.new(self, img_path, x, y, rotation, scale, flip, alpha_mode) + def add_guideline_line( + self, + x: float | None = None, + y: float | None = None, + angle: float | None = None, + ) -> guideline.GuidelineLine: + """Add a new line guideline to the project.""" + return guideline.GuidelineLine.new(self, x, y, angle) + + def add_guideline_segment( + self, + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + ) -> guideline.GuidelineSegment: + """Add a new segment guideline to the project.""" + return guideline.GuidelineSegment.new(self, x1, y1, x2, y2) + + def add_guideline_circle( + self, + x: float | None = None, + y: float | None = None, + radius: float | None = None, + ) -> guideline.GuidelineCircle: + """Add a new segment guideline to the project.""" + return guideline.GuidelineCircle.new(self, x, y, radius) + + def add_guideline_ellipse( + self, + x: float | None = None, + y: float | None = None, + radius_a: float | None = None, + radius_b: float | None = None, + ) -> guideline.GuidelineEllipse: + """Add a new ellipse guideline to the project.""" + return guideline.GuidelineEllipse.new(self, x, y, radius_a, radius_b) + + def add_guideline_grid( + self, + x: float | None = None, + y: float | None = None, + width: float | None = None, + height: float | None = None, + ) -> guideline.GuidelineGrid: + """Add a new grid guideline to the project.""" + return guideline.GuidelineGrid.new(self, x, y, width, height) + + def add_guideline_marks( + self, + count_x: int | None = None, + count_y: int | None = None, + ) -> guideline.GuidelineMarks: + """Add a new marks guideline to the project.""" + return guideline.GuidelineMarks.new(self, count_x, count_y) + + def add_guideline_field_chart( + self, + ) -> guideline.GuidelineFieldChart: + """Add a new field chart guideline to the project.""" + return guideline.GuidelineFieldChart.new(self) + + def add_guideline_animator_field( + self, + ) -> guideline.GuidelineAnimatorField: + """Add a new animator field guideline to the project.""" + return guideline.GuidelineAnimatorField.new(self) + + def add_guideline_safe_area( + self, + sf_out: int | None = None, + sf_in: int | None = None, + ) -> guideline.GuidelineSafeArea: + """Add a new safe area guideline to the project.""" + return guideline.GuidelineSafeArea.new(self, sf_out, sf_in) + + def add_guideline_vanishing_point1( + self, + x: float | None = None, + y: float | None = None, + grid: bool | None = None, + ) -> guideline.GuidelineVanishPoint1: + """Add a new vanishing point1 guideline to the project.""" + return guideline.GuidelineVanishPoint1.new(self, x, y, grid) + + def add_guideline_vanishing_point2( + self, + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + ) -> guideline.GuidelineVanishPoint2: + """Add a new vanishing point2 guideline to the project.""" + return guideline.GuidelineVanishPoint2.new(self, x1, y1, x2, y2) + + def add_guideline_vanishing_point3( + self, + x1: float | None = None, + y1: float | None = None, + x2: float | None = None, + y2: float | None = None, + x3: float | None = None, + y3: float | None = None, + ) -> guideline.GuidelineVanishPoint3: + """Add a new vanishing point2 guideline to the project.""" + return guideline.GuidelineVanishPoint3.new(self, x1, y1, x2, y2, x3, y3) + def _validate_range(self, start: int, end: int) -> None: project_start_frame = self.start_frame project_end_frame = self.end_frame @@ -514,12 +622,27 @@ def _validate_range(self, start: int, end: int) -> None: if start < proj_full_range[0] or end > proj_full_range[1]: raise ValueError(f"Range ({start}-{end}) outside of project bounds ({proj_full_range})") - def _get_real_range(self, start: int, end: int) -> tuple[int, int]: + def _get_real_range(self, start: int, end: int, frame_set: FrameSet | None = None) -> tuple[int, int, FrameSet]: project_start_frame = self.start_frame - start -= project_start_frame - end -= project_start_frame - return start, end + def convert_range(x: int) -> int: + return x - project_start_frame + + start = convert_range(start) + end = convert_range(end) + + if frame_set is not None: + start = max(start, convert_range(frame_set.start())) + end = min(end, convert_range(frame_set.end())) + if frame_set.isConsecutive(): + frame_set = FrameSet(f"{start}-{end}") + else: + frames = [convert_range(f) for f in frame_set.items] + [start, end] + frame_set = FrameSet(frames) + else: + frame_set = FrameSet(f"{start}-{end}") + + return start, end, frame_set @set_as_current def render( @@ -527,6 +650,7 @@ def render( output_path: Path | str | FileSequence, start: int | None = None, end: int | None = None, + frame_set: FrameSet | None = None, use_camera: bool = False, alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = None, @@ -538,6 +662,7 @@ def render( output_path: a single file or file sequence pattern start: the start frame to render or the mark in or the project's start frame if None. Defaults to None. end: the end frame to render or the mark out or the project's end frame if None. Defaults to None. + frame_set: a FrameSet with the frames/range to render. Defaults to None. use_camera: use the camera for rendering, otherwise render the whole canvas. Defaults to False. alpha_mode: the alpha mode for rendering. Defaults to george.AlphaSaveMode.PREMULTIPLY. background_mode: the background mode for rendering. Defaults to george.BackgroundMode.NONE. @@ -561,12 +686,13 @@ def render( default_end = self.mark_out or self.end_frame self._render( - output_path, - default_start, - default_end, - start, - end, - use_camera, + output_path=output_path, + default_start=default_start, + default_end=default_end, + start=start, + end=end, + frame_set=frame_set, + use_camera=use_camera, layer_selection=None, alpha_mode=alpha_mode, background_mode=background_mode, @@ -587,14 +713,14 @@ def render_clips( clips = sorted(clips, key=lambda c: c.position) start = clips[0].timeline_start end = clips[-1].timeline_end + self.render( - output_path, - start, - end, - use_camera, - alpha_mode, - background_mode, - format_opts, + output_path=output_path, + frame_set=FrameSet(f"{start}-{end}"), + use_camera=use_camera, + alpha_mode=alpha_mode, + background_mode=background_mode, + format_opts=format_opts, ) @staticmethod diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index e0544b6..aa7a9a0 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -2,8 +2,8 @@ from __future__ import annotations -import re import contextlib +import re from abc import ABC, abstractmethod from collections.abc import Generator, Iterable, Iterator from pathlib import Path @@ -19,7 +19,7 @@ from fileseq.frameset import FrameSet from typing_extensions import ParamSpec, Protocol -from pytvpaint import george +from pytvpaint import george, log from pytvpaint.george.exceptions import GeorgeError if TYPE_CHECKING: @@ -124,7 +124,7 @@ def current_frame(self, frame: int) -> None: """Set the current frame.""" pass - def _get_real_range(self, start: int, end: int) -> tuple[int, int]: + def _get_real_range(self, start: int, end: int, frame_set: FrameSet | None = None) -> tuple[int, int, FrameSet]: """Removes the object in TVPaint.""" raise NotImplementedError("Function refresh() needs to be implemented") @@ -132,27 +132,42 @@ def _validate_range(self, start: int, end: int) -> None: """Raises an exception if given range is invalid.""" raise NotImplementedError("Function refresh() needs to be implemented") - def _render( + def _render( # noqa: C901 self, output_path: Path | str | FileSequence, default_start: int, default_end: int, start: int | None = None, end: int | None = None, - frameset: FrameSet | None = None, + frame_set: FrameSet | None = None, use_camera: bool = False, layer_selection: list[Layer] | None = None, alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = None, format_opts: list[str] | None = None, ) -> None: + if start or end and not frame_set: + log.warning("Use of `start` and `end` is deprecated, prefer using `fileseq.FrameSet()` instead.") + if start and end and frame_set and any(f not in frame_set for f in (start, end)): + log.warning("`start` and/or `end` outside of `frame_set` range, will prioritize FrameSet.") + + if frame_set is not None and not (start and end): + start = start if start is not None else frame_set.start() + end = end if end is not None else frame_set.end() + + # finds range if none provided or in path and clamps it to the correct context file_sequence, start, end, is_sequence, is_image = handle_output_range( output_path, default_start, default_end, start, end ) self._validate_range(start, end) - origin_start = int(start) - start, end = self._get_real_range(start, end) + # we should have a range by now, let's apply/save it + if frame_set is not None: + file_sequence.setFrameSet(frame_set) + else: + frame_set = file_sequence.frameSet() + + start, end, frame_set = self._get_real_range(start, end, frame_set) if not is_image and start == end: raise ValueError("TVPaint will not render a movie that contains a single frame") @@ -166,18 +181,25 @@ def _render( save_format = george.SaveFormat.from_extension(file_sequence.extension().lower()) # render to output + # not using tv_save_sequence since it doesn't handle camera and would require different range math with render_context(alpha_mode, background_mode, save_format, format_opts, layer_selection): - if start == end: - with restore_current_frame(self, origin_start): - george.tv_save_display(first_frame) - else: - # not using tv_save_sequence since it doesn't handle camera and would require different range math + if frame_set.isConsecutive(): george.tv_project_save_sequence( first_frame, start=start, end=end, use_camera=use_camera, ) + else: + for i, real_frame_nb in enumerate(frame_set.items): + frame_nb = list(file_sequence.frameSet().items)[i] # type: ignore[union-attr] + frame_path = Path(file_sequence.frame(frame_nb)) + george.tv_project_save_sequence( + frame_path, + start=frame_nb, + end=frame_nb, + use_camera=use_camera, + ) # make sure the output exists otherwise raise an error if is_sequence: @@ -327,7 +349,6 @@ def render_context( format_opts: the custom format options as strings. Defaults to None. layer_selection: the layers to render. Defaults to None. """ - # Save the current state pre_alpha_save_mode = george.tv_alpha_save_mode_get() pre_save_format, pre_save_args = george.tv_save_mode_get() @@ -406,7 +427,7 @@ def id(self) -> int | str: ... def name(self) -> str: ... -class _TVPElementWithPath(_TVPElement): +class _TVPElementWithPath(_TVPElement, Protocol): @property def path(self) -> Path: ... @@ -440,7 +461,7 @@ def get_tvp_element( """ values = (by_id, by_name, by_regex, by_path) if not any(v is not None for v in values): - raise ValueError(f"At least one value ({' or '.join(values)} must be provided") + raise ValueError(f"At least one value ({' or '.join(str(v) for v in values)} must be provided") for element in tvp_elements: if by_id is not None and element.id != by_id: diff --git a/tests/conftest.py b/tests/conftest.py index c9b3c50..4e14b72 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import struct import wave +import zlib from collections.abc import Generator from pathlib import Path from random import randint @@ -12,39 +13,7 @@ from pytvpaint import george from pytvpaint.clip import Clip from pytvpaint.george.client import send_cmd -from pytvpaint.george.grg_base import tv_pen_brush_set -from pytvpaint.george.grg_clip import ( - TVPClip, - tv_clip_close, - tv_clip_current_id, - tv_clip_enum_id, - tv_clip_info, - tv_clip_new, - tv_sound_clip_new, -) -from pytvpaint.george.grg_layer import ( - TVPLayer, - tv_layer_anim, - tv_layer_create, - tv_layer_get_id, - tv_layer_info, - tv_layer_kill, -) -from pytvpaint.george.grg_project import ( - TVPProject, - tv_project_close, - tv_project_current_id, - tv_project_enum_id, - tv_project_info, - tv_project_new, - tv_sound_project_new, -) -from pytvpaint.george.grg_scene import ( - tv_scene_close, - tv_scene_current_id, - tv_scene_enum_id, - tv_scene_new, -) +from pytvpaint.guideline import GuidelineLine from pytvpaint.layer import Layer from pytvpaint.project import Project from pytvpaint.scene import Scene @@ -54,74 +23,77 @@ FixtureYield = Generator[T, None, None] -def _fix_tvp_12_selection() -> None: - from packaging import version +IS_NOT_TVP12 = not george.tv_version()[1].startswith("12") + - _, tvp_version, _ = george.tv_version() - current_version = version.parse(tvp_version) +def _fix_tvp_12_selection() -> None: + if IS_NOT_TVP12: + return # if tvp_version >= 12 and current_layer is Camera, then select first real layer instead: - if current_version.major >= 12: - current_clip = Project.current_project().current_clip - current_layer = current_clip.current_layer - if current_layer.layer_type == george.LayerType.CAMERA: - # switch to first layer in real/non-camera layers - first_layer = None - for layer in current_clip.get_layers(): - if layer.layer_type == george.LayerType.CAMERA: - continue + current_clip = Project.current_project().current_clip + current_layer = current_clip.current_layer + if current_layer.layer_type == george.LayerType.CAMERA: + # switch to first layer in real/non-camera layers + first_layer = None + for layer in current_clip.get_layers(): + if layer.layer_type == george.LayerType.CAMERA: + continue - first_layer = layer - break + first_layer = layer + break - if first_layer: - first_layer.make_current() + if first_layer: + first_layer.make_current() @pytest.fixture(scope="function") def pen_brush_reset() -> FixtureYield[None]: """Resets the pen brush after the test""" yield - tv_pen_brush_set(reset=True) + george.tv_pen_brush_set(reset=True) @pytest.fixture -def test_project(tmp_path: Path) -> FixtureYield[TVPProject]: +def test_project(tmp_path: Path) -> FixtureYield[george.TVPProject]: """ Fixture to create an empty project and remove it after Useful when you want to isolate a test """ - project_id = tv_project_new(tmp_path / "project.tvpp") + project_id = george.tv_project_new(tmp_path / "project.tvpp") + for p_id in Project.open_projects_ids(): + if p_id == project_id: + continue + george.tv_project_close(p_id) _fix_tvp_12_selection() - yield tv_project_info(project_id) - tv_project_close(project_id) + yield george.tv_project_info(project_id) + george.tv_project_close(project_id) @pytest.fixture -def test_project_obj(test_project: TVPProject) -> FixtureYield[Project]: +def test_project_obj(test_project: george.TVPProject) -> FixtureYield[Project]: p = Project(test_project.id) - _fix_tvp_12_selection() yield p @pytest.fixture def cleanup_current_project() -> FixtureYield[None]: yield - tv_project_close(tv_project_current_id()) + george.tv_project_close(george.tv_project_current_id()) @pytest.fixture -def test_layer() -> FixtureYield[TVPLayer]: +def test_layer() -> FixtureYield[george.TVPLayer]: """Temporary layer for testing""" - layer = tv_layer_create("test") - yield tv_layer_info(layer) - tv_layer_kill(layer) + layer = george.tv_layer_create("test") + yield george.tv_layer_info(layer) + george.tv_layer_kill(layer) @pytest.fixture -def test_layer_obj(test_clip_obj: Clip, test_layer: TVPLayer) -> FixtureYield[Layer]: +def test_layer_obj(test_clip_obj: Clip, test_layer: george.TVPLayer) -> FixtureYield[Layer]: """Temporary layer object for testing""" yield Layer(test_layer.id, test_clip_obj) @@ -134,23 +106,23 @@ def test_anim_layer_obj(test_layer_obj: Layer) -> FixtureYield[Layer]: @pytest.fixture -def test_anim_layer(test_layer: TVPLayer) -> FixtureYield[TVPLayer]: +def test_anim_layer(test_layer: george.TVPLayer) -> FixtureYield[george.TVPLayer]: """Temporary anim layer for testing""" - tv_layer_anim(test_layer.id) + george.tv_layer_anim(test_layer.id) yield test_layer @pytest.fixture -def test_clip() -> FixtureYield[TVPClip]: +def test_clip() -> FixtureYield[george.TVPClip]: """Temporary clip for testing""" - tv_clip_new("test") - clip = tv_clip_current_id() - yield tv_clip_info(clip) - tv_clip_close(clip) + george.tv_clip_new("test") + clip = george.tv_clip_current_id() + yield george.tv_clip_info(clip) + george.tv_clip_close(clip) @pytest.fixture -def test_clip_obj(test_project_obj: Project, test_clip: TVPClip) -> FixtureYield[Clip]: +def test_clip_obj(test_project_obj: Project, test_clip: george.TVPClip) -> FixtureYield[Clip]: """Temporary clip object for testing""" yield Clip(test_clip.id, test_project_obj) @@ -158,10 +130,10 @@ def test_clip_obj(test_project_obj: Project, test_clip: TVPClip) -> FixtureYield @pytest.fixture def test_scene() -> FixtureYield[int]: """Temporary scene for testing""" - tv_scene_new() - scene = tv_scene_current_id() + george.tv_scene_new() + scene = george.tv_scene_current_id() yield scene - tv_scene_close(scene) + george.tv_scene_close(scene) @pytest.fixture @@ -170,6 +142,18 @@ def test_scene_obj(test_project_obj: Project, test_scene: int) -> FixtureYield[S yield Scene(test_scene, test_project_obj) +@pytest.fixture(name="guideline_pos") +def test_guideline(test_project: george.TVPProject) -> FixtureYield[int]: + pos = george.tv_guideline_add_line(x=10, y=10, angle=45) + yield pos + + +@pytest.fixture +def test_guideline_obj(test_project_obj: Project) -> FixtureYield[GuidelineLine]: + guideline_obj = GuidelineLine.new(test_project_obj, x=10, y=10, angle=45) + yield guideline_obj + + @pytest.fixture def count_up_generate(test_clip_obj: Clip) -> None: """Create 5 frames with a text in the middle of the screen for each frame. Useful for debugging render tests.""" @@ -198,32 +182,51 @@ def count_up_generate(test_clip_obj: Clip) -> None: george.tv_update_undo() -def ppm_generate(path: Path, width: int, height: int, levels: int = 255) -> None: +def png_generate(path: Path, width: int, height: int) -> None: """ - Generates an ASCII PPM image file with random gray level pixels - See: https://fr.wikipedia.org/wiki/Portable_pixmap + Generates a valid grayscale PNG image file using pure Python. + No external dependencies required. """ + signature = b"\x89PNG\r\n\x1a\n" + + def make_chunk(chunk_type: bytes, data: bytes) -> bytes: + """Helper to create a standard PNG chunk with length and CRC32.""" + length = struct.pack(">I", len(data)) + crc = struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF) + return length + chunk_type + data + crc + + ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 0, 0, 0, 0) + ihdr = make_chunk(b"IHDR", ihdr_data) + + raw_data = bytearray() + for _ in range(height): + raw_data.append(0) # Filter type 0 + for _ in range(width): + raw_data.append(randint(0, 255)) - with path.open("w") as ppm: - ppm.writelines(["P2\n", f"{width} {height}\n", f"{levels}\n"]) - for _ in range(height): - pixels = (randint(0, levels) for _ in range(width)) - row = " ".join(map(str, pixels)) - ppm.write(f"{row}\n") + idat_data = zlib.compress(raw_data) + idat = make_chunk(b"IDAT", idat_data) + iend = make_chunk(b"IEND", b"") + + with path.open("wb") as f: + f.write(signature) + f.write(ihdr) + f.write(idat) + f.write(iend) @pytest.fixture(scope="session") -def ppm_sequence(tmp_path_factory: pytest.TempPathFactory) -> FixtureYield[list[Path]]: +def png_sequence(tmp_path_factory: pytest.TempPathFactory) -> Generator[list[Path], None, None]: """ - Session scoped fixture to get a sequence of generated PPM images + Session scoped fixture to get a sequence of generated PNG images. """ images_dir = tmp_path_factory.mktemp("images") images: list[Path] = [] for i in range(5): - ppm = images_dir / f"image.{(i + 1):03d}.ppm" - ppm_generate(ppm, 200, 200) - images.append(ppm) + png_path = images_dir / f"image.{(i + 1):03d}.png" + png_generate(png_path, 200, 200) + images.append(png_path) yield images @@ -265,17 +268,17 @@ def create_some_projects(tmp_path: Path) -> FixtureYield[list[Project]]: projects: list[Project] = [] for i in range(5): - p_id = tv_project_new(tmp_path / f"project_{i}.tvpp") + p_id = george.tv_project_new(tmp_path / f"project_{i}.tvpp") projects.append(Project(p_id)) # Remove the default project - tv_project_close(tv_project_enum_id(0)) + george.tv_project_close(george.tv_project_enum_id(0)) yield projects for project in projects: if not project.is_closed: - tv_project_close(project.id) + george.tv_project_close(project.id) @pytest.fixture @@ -284,12 +287,12 @@ def create_some_scenes(test_project_obj: Project) -> FixtureYield[list[Scene]]: scenes: list[Scene] = [] for i in range(5): - tv_scene_new() - scene_id = tv_scene_enum_id(i + 1) + george.tv_scene_new() + scene_id = george.tv_scene_enum_id(i + 1) scenes.append(Scene(scene_id, test_project_obj)) # Remove the default scene - tv_scene_close(tv_scene_enum_id(0)) + george.tv_scene_close(george.tv_scene_enum_id(0)) yield scenes @@ -303,12 +306,12 @@ def create_some_clips( clips: list[Clip] = [] for i in range(5): - tv_clip_new(f"clip_{i}") - clip_id = tv_clip_enum_id(scene.id, i + 1) + george.tv_clip_new(f"clip_{i}") + clip_id = george.tv_clip_enum_id(scene.id, i + 1) clips.append(Clip(clip_id, test_project_obj)) # Remove the default clip - tv_clip_close(tv_clip_enum_id(scene.id, 0)) + george.tv_clip_close(george.tv_clip_enum_id(scene.id, 0)) yield clips @@ -322,12 +325,12 @@ def create_some_layers( layers: list[Layer] = [] for i in range(5): - tv_layer_create(f"layer_{i}") - layer_id = tv_layer_get_id(i + 1) + george.tv_layer_create(f"layer_{i}") + layer_id = george.tv_layer_get_id(i + 1) layers.append(Layer(layer_id, test_clip_obj)) # Remove the default layer - tv_layer_kill(tv_layer_get_id(0)) + george.tv_layer_kill(george.tv_layer_get_id(0)) yield layers @@ -341,25 +344,25 @@ def create_some_layer_folders( layers: list[Layer] = [] for i in range(5): - tv_layer_create(f"layer_{i}", layer_type=0) - layer_id = tv_layer_get_id(i + 1) + george.tv_layer_create(f"layer_{i}", layer_type=0) + layer_id = george.tv_layer_get_id(i + 1) layers.append(Layer(layer_id, test_clip_obj)) # Remove the default layer - tv_layer_kill(tv_layer_get_id(0)) + george.tv_layer_kill(george.tv_layer_get_id(0)) yield layers @pytest.fixture def test_project_sound(test_project_obj: Project, wav_file: Path) -> FixtureYield[ProjectSound]: - tv_sound_project_new(wav_file) + george.tv_sound_project_new(wav_file) yield ProjectSound(0, test_project_obj) @pytest.fixture def test_clip_sound(test_clip_obj: Clip, wav_file: Path) -> FixtureYield[ClipSound]: - tv_sound_clip_new(wav_file) + george.tv_sound_clip_new(wav_file) yield ClipSound(0, test_clip_obj) @@ -368,16 +371,16 @@ def create_some_project_sounds(test_project_obj: Project, wav_file: Path) -> Fix sounds: list[ProjectSound] = [] for i in range(5): - tv_sound_project_new(wav_file) + george.tv_sound_project_new(wav_file) sounds.append(ProjectSound(i, test_project_obj)) yield sounds @pytest.fixture -def with_loaded_sequence(test_clip_obj: Clip, ppm_sequence: list[Path]) -> FixtureYield[Layer]: +def with_loaded_sequence(test_clip_obj: Clip, png_sequence: list[Path]) -> FixtureYield[Layer]: yield test_clip_obj.load_media( - ppm_sequence[0], + png_sequence[0], with_name="images", stretch=False, preload=True, @@ -387,3 +390,11 @@ def with_loaded_sequence(test_clip_obj: Clip, ppm_sequence: list[Path]) -> Fixtu @pytest.fixture(scope="function", autouse=True) def fix_tvp_12_selection() -> None: _fix_tvp_12_selection() + + +def load_sequence_with_name(first_frame: Path, name: str, count: int) -> int: + """Load an image sequence and rename the layer""" + george.tv_load_sequence(first_frame, offset_count=(0, count)) + layer_id = george.tv_layer_current_id() + george.tv_layer_rename(layer_id, name) + return layer_id diff --git a/tests/george/client/test_parse.py b/tests/george/client/test_parse.py index 8fc3da4..cee1a5d 100644 --- a/tests/george/client/test_parse.py +++ b/tests/george/client/test_parse.py @@ -1,20 +1,21 @@ from __future__ import annotations +from collections.abc import MutableSequence +from dataclasses import dataclass from enum import Enum from pathlib import Path -from dataclasses import dataclass -from typing import Any, Tuple, List +from typing import Any import pytest from pytvpaint.george.client.parse import ( DataclassInstance, camel_to_pascal, + normalize_windows_paths, tv_cast_to_type, tv_handle_string, tv_parse_dict, tv_parse_list, - normalize_windows_paths, unescape_everything_safely, ) @@ -55,8 +56,8 @@ class Color(Enum): class LayerType(Enum): - IMG = 1 - SEQ = 2 + IMAGE = 1 + SEQUENCE = 2 @pytest.mark.parametrize( @@ -81,11 +82,11 @@ class LayerType(Enum): ("hel 0.5 C:/test", tuple[str, float, Path], ("hel", 0.5, Path("C:/test"))), ], ) -def test_tv_cast_to_type(value: str, cast: Any, result: str) -> None: +def test_tv_cast_to_type(value: str, cast: Any, result: Any) -> None: assert tv_cast_to_type(value, cast) == result -def test_primitives(): +def test_primitives() -> None: assert tv_cast_to_type("123", int) == 123 assert tv_cast_to_type("123.45", float) == 123.45 assert tv_cast_to_type("true", bool) is True @@ -100,83 +101,82 @@ def test_primitives(): ("67", "is not a valid"), ], ) -def test_tv_cast_to_enum_index_out_of_bounds(input_text, error_match) -> None: +def test_tv_cast_to_enum_index_out_of_bounds(input_text: str, error_match: str) -> None: with pytest.raises(ValueError, match=error_match): tv_cast_to_type(input_text, EnumTest) -def test_homogeneous_list(): +def test_homogeneous_list() -> None: # List[int]: applies int() to all items input_str = "10 20 30" - assert tv_cast_to_type(input_str, List[int]) == [10, 20, 30] + assert tv_cast_to_type(input_str, list[int]) == [10, 20, 30] -def test_variable_tuple(): +def test_variable_tuple() -> None: # Tuple[float, ...]: applies float() to all items input_str = "1.1 2.2 3.3" - assert tv_cast_to_type(input_str, Tuple[float, ...]) == (1.1, 2.2, 3.3) + assert tv_cast_to_type(input_str, tuple[float, ...]) == (1.1, 2.2, 3.3) -def test_fixed_tuple(): +def test_fixed_tuple() -> None: # Tuple[str, int]: First item str, second item int input_str = '"my file.png" 50' expected = ("my file.png", 50) - assert tv_cast_to_type(input_str, Tuple[str, int]) == expected + assert tv_cast_to_type(input_str, tuple[str, int]) == expected -def test_fixed_tuple_mismatch_error(): +def test_fixed_tuple_mismatch_error() -> None: # Tuple expects 2 items, got 3 with pytest.raises(ValueError) as exc: - tv_cast_to_type("1 2 3", Tuple[int, int]) + tv_cast_to_type("1 2 3", tuple[int, int]) assert "Count mismatch" in str(exc.value) -def test_raw_list_quoting(): +def test_raw_list_quoting() -> None: # Raw list (no types), but handles quoted strings correctly input_str = 'item1 "item 2" item3' # Should result in 3 items, not 4 assert tv_cast_to_type(input_str, list) == ["item1", "item 2", "item3"] -def test_enum_strategies(): +def test_enum_strategies() -> None: # 1. Name assert tv_cast_to_type("RED", Color) == Color.RED # 2. Value assert tv_cast_to_type("blue", Color) == Color.BLUE # 3. Int Value (String "1" -> Value 1) - assert tv_cast_to_type("1", LayerType) == LayerType.IMG + assert tv_cast_to_type("1", LayerType) == LayerType.IMAGE # 4. Index (Position 1 -> Second item -> BLUE) # "1" is ambiguous for Color, but since Color values are strings, "1" is treated as index assert tv_cast_to_type("1", Color) == Color.BLUE -def test_nested_path_list(): - # List[Path] +def test_nested_path_list() -> None: input_str = r'"C:\file1.txt" "D:\file2.txt"' - result = tv_cast_to_type(input_str, List[Path]) + result = tv_cast_to_type(input_str, list[Path]) assert result[0] == Path(r"C:\file1.txt") assert isinstance(result[1], Path) -def test_strip_logic_list(): - # Case 1: Wrapped List "item1 item2" +def test_strip_logic_list() -> None: + # Wrapped List "item1 item2" # Expected: Strip quotes -> split -> ['item1', 'item2'] - assert tv_cast_to_type('"item1 item2"', List[str]) == ["item1", "item2"] + assert tv_cast_to_type('"item1 item2"', list[str]) == ["item1", "item2"] - # Case 2: Separate Quoted Items "item1" "item2" + # Separate Quoted Items "item1" "item2" # Expected: Don't strip (middle quote) -> split -> ['item1', 'item2'] - assert tv_cast_to_type('"item1" "item2"', List[str]) == ["item1", "item2"] + assert tv_cast_to_type('"item1" "item2"', list[str]) == ["item1", "item2"] - # Case 3: Mixed content "file.ext" 10 + # Mixed content "file.ext" 10 # Expected: Don't strip (doesn't end in quote) -> split -> ['file.ext', '10'] - assert tv_cast_to_type('"file.ext" 10', List[str]) == ["file.ext", "10"] + assert tv_cast_to_type('"file.ext" 10', list[str]) == ["file.ext", "10"] - # Case 4: Single quoted item '"item1"' + # Single quoted item '"item1"' # Expected: Strip -> 'item1' -> list -> ['item1'] - assert tv_cast_to_type('"item1"', List[str]) == ["item1"] + assert tv_cast_to_type('"item1"', list[str]) == ["item1"] -def test_strip_logic_primitives(): +def test_strip_logic_primitives() -> None: # Ensure stripping still works for single values assert tv_cast_to_type('"123"', int) == 123 assert tv_cast_to_type("'true'", bool) is True @@ -185,7 +185,7 @@ def test_strip_logic_primitives(): assert tv_cast_to_type('"text"', str) == "text" -def test_auto_inference_mixed_list(): +def test_auto_inference_mixed_list() -> None: # Input: String "word" Integer 10 Float 15.5 Bool true input_str = "word 10 15.5 true" @@ -198,7 +198,7 @@ def test_auto_inference_mixed_list(): assert isinstance(result[3], bool) -def test_auto_inference_quoted_strings(): +def test_auto_inference_quoted_strings() -> None: # Quotes should be stripped during inference if they aren't syntactical input_str = '"my string" "10"' @@ -209,7 +209,7 @@ def test_auto_inference_quoted_strings(): assert result == ["my string", 10] -def test_auto_inference_nested_structure(): +def test_auto_inference_nested_structure() -> None: # Raw tuple input_str = '100 false "file.txt"' result = tv_cast_to_type(input_str, tuple) @@ -217,7 +217,7 @@ def test_auto_inference_nested_structure(): assert result == (100, False, "file.txt") -def test_auto_inference_edge_cases(): +def test_auto_inference_edge_cases() -> None: # Negative numbers and zero input_str = "-5 0 0.0" result = tv_cast_to_type(input_str, list) @@ -325,7 +325,7 @@ def test_tv_parse_dict( with_type: type[DataclassInstance], check_keys: dict[str, Any], ) -> None: - result_dict = tv_parse_dict(dict_str, with_fields=with_type) + result_dict: dict[str, Any] = tv_parse_dict(dict_str, with_fields=with_type) assert result_dict == check_keys @@ -334,23 +334,23 @@ class LayerMode(Enum): MULTIPLY = "multiply" -TEST_DEFS = [ +TEST_DEFS: MutableSequence[tuple[tuple[str, str] | str, Any]] = [ ("path", Path), ("x", float), ("y", float), ("opacity", int), - ("tags", List[str]), + ("tags", list[str]), ("mode", LayerMode), ("visible", bool), - ("dims", Tuple[int, int]), + ("dims", tuple[int, int]), ] -def test_happy_path_typed(): +def test_happy_path_typed() -> None: """Verifies standard usage with mixed types.""" input_str = r'path "\\server\ref.png" x 10.5 y 20.2 opacity 255 visible true' - result = tv_parse_dict(input_str, TEST_DEFS) + result: dict[str, Any] = tv_parse_dict(input_str, TEST_DEFS) assert result["path"] == Path(r"\\server\ref.png") assert result["x"] == 10.5 @@ -359,14 +359,14 @@ def test_happy_path_typed(): assert result["visible"] is True -def test_case_insensitivity_normalization(): +def test_case_insensitivity_normalization() -> None: """ Verifies that input keys like 'OPACITY' are matched case-insensitively but stored using the normalized key 'opacity' from definitions. """ input_str = "OPACITY 50 X 100 Visible FALSE" - result = tv_parse_dict(input_str, TEST_DEFS) + result: dict[str, Any] = tv_parse_dict(input_str, TEST_DEFS) assert result["opacity"] == 50 assert result["x"] == 100.0 @@ -377,39 +377,39 @@ def test_case_insensitivity_normalization(): assert "opacity" in result -def test_list_and_enum(): +def test_list_and_enum() -> None: """Verifies generic List[str] and Enum casting.""" input_str = 'tags "tree sky" mode multiply' - result = tv_parse_dict(input_str, TEST_DEFS) + result: dict[str, Any] = tv_parse_dict(input_str, TEST_DEFS) assert result["tags"] == ["tree", "sky"] assert result["mode"] == LayerMode.MULTIPLY -def test_tuple_casting(): +def test_tuple_casting() -> None: """Verifies Tuple[int, int] casting from a space-separated string.""" input_str = 'dims "1920 1080"' - result = tv_parse_dict(input_str, TEST_DEFS) + result: dict[str, Any] = tv_parse_dict(input_str, TEST_DEFS) assert result["dims"] == (1920, 1080) -def test_duplicate_keys_last_wins_case_insensitive(): +def test_duplicate_keys_last_wins_case_insensitive() -> None: """Verifies that the last occurrence of a key overwrites previous ones.""" input_str = "opacity 10 OPACITY 100" - result = tv_parse_dict(input_str, TEST_DEFS) + result: dict[str, Any] = tv_parse_dict(input_str, TEST_DEFS) assert result["opacity"] == 100 -def test_unknown_keys_error(): +def test_unknown_keys_error() -> None: """Verifies that parts of the string not matching known keys are ignored.""" input_str = "x 10 unknown_param 999" with pytest.raises(ValueError): tv_parse_dict(input_str, TEST_DEFS) -def test_empty_input(): +def test_empty_input() -> None: """Verifies empty input handling.""" assert tv_parse_dict("", TEST_DEFS) == {} @@ -422,6 +422,19 @@ class Project: path: Path +@dataclass +class Layer: + visibility: bool + position: int + density: int + name: str + type: LayerType + first_frame: int + last_frame: int + selected: bool + editable: bool + + @dataclass class Truth: true: bool @@ -448,6 +461,21 @@ class Truth: "path": Path("c:/my/path"), }, ), + ( + 'ON 1 0 ""new layer"" Image 0 0 0 0', + Layer, + { + "visibility": True, + "position": 1, + "density": 0, + "name": "new layer", + "type": LayerType.IMAGE, + "first_frame": 0, + "last_frame": 0, + "selected": False, + "editable": False, + }, + ), ('"ON" 0', Truth, {"true": True, "false": False}), ("OFF 1", Truth, {"true": False, "false": True}), ], @@ -466,26 +494,26 @@ class Orientation(Enum): VERTICAL = "vert" -class Layer(Enum): +class LayerBG(Enum): BACKGROUND = 1 FOREGROUND = 2 -ARGS_DEF = [ +ARGS_DEF: MutableSequence[tuple[tuple[str, str] | str, Any]] = [ ("path", Path), ("x", float), ("y", float), ("visible", bool), ("tags", list), ("orient", Orientation), - ("layer", Layer), + ("layer", LayerBG), ] # --- Tests --- -def test_happy_path_complex(): +def test_happy_path_complex() -> None: input_str = r'"C:\my files\img.png" 10.5 20.0 true "tree sky water" horiz 2' result = tv_parse_list(input_str, ARGS_DEF) @@ -495,11 +523,11 @@ def test_happy_path_complex(): assert result["x"] == 10.5 assert result["tags"] == ["tree", "sky", "water"] assert result["orient"] == Orientation.HORIZONTAL - assert result["layer"] == Layer.FOREGROUND + assert result["layer"] == LayerBG.FOREGROUND -def test_enum_lookup_strategies(): - """Verifies the suppress waterfall logic works.""" +def test_enum_lookup_strategies() -> None: + """Verifies the suppression logic works.""" # 1. By Name (VERTICAL) res_name = tv_parse_list(r'"p" 0 0 true [] VERTICAL 1', ARGS_DEF) assert res_name["orient"] == Orientation.VERTICAL @@ -508,10 +536,10 @@ def test_enum_lookup_strategies(): res_val = tv_parse_list(r'"p" 0 0 true [] vert 1', ARGS_DEF) assert res_val["orient"] == Orientation.VERTICAL - # 3. By Int String ("2") -> int(2) -> Layer(2) + # 3. By Int String ("2") -> int(2) -> LayerBG(2) res_int = tv_parse_list(r'"p" 0 0 true [] 1 2', ARGS_DEF) assert res_int["orient"] == Orientation.VERTICAL - assert res_int["layer"] == Layer.FOREGROUND + assert res_int["layer"] == LayerBG.FOREGROUND @pytest.mark.parametrize( @@ -521,21 +549,21 @@ def test_enum_lookup_strategies(): ("10 20.5 word", [10, 20.5, "word"]), ], ) -def test_list_numeric_conversion(input_text: str, expected: list): +def test_list_numeric_conversion(input_text: str, expected: list[Any]) -> None: """Verifies list items are smart-cast to numbers using suppress.""" - defs = [("vals", list)] + defs: MutableSequence[tuple[tuple[str, str] | str, Any]] = [("vals", list)] # "10 20.5 word" -> [10, 20.5, "word"] result = tv_parse_list(input_text, defs) assert result["vals"] == expected -def test_single_arg_optimization_list(): +def test_single_arg_optimization_list() -> None: """ Verifies that a single list argument consumes the entire string without needing quotes. """ # Definition: One argument named 'ids', type is List[int] - defs = [("ids", List[int])] + defs: MutableSequence[tuple[tuple[str, str] | str, Any]] = [("ids", list[int])] # Input: Space-separated numbers (without outer quotes) # OLD behavior: shlex.split -> ['10', '20', '30'] -> Length 3 vs 1 -> Error @@ -546,11 +574,11 @@ def test_single_arg_optimization_list(): assert result["ids"] == [10, 20, 30] -def test_single_arg_complex_quotes(): +def test_single_arg_complex_quotes() -> None: """ Verifies that quotes are handled correctly inside the single argument. """ - defs = [("tags", List[str])] + defs: MutableSequence[tuple[tuple[str, str] | str, Any]] = [("tags", list[str])] # Input: mixed quoting input_str = 'tag1 "tag 2" tag3' @@ -558,11 +586,11 @@ def test_single_arg_complex_quotes(): assert result["tags"] == ["tag1", "tag 2", "tag3"] -def test_multi_arg_still_validates(): +def test_multi_arg_still_validates() -> None: """ Verifies that we didn't break validation for multiple arguments. """ - defs = [("x", int), ("y", int)] + defs: MutableSequence[tuple[tuple[str, str] | str, Any]] = [("x", int), ("y", int)] # Input has 3 tokens, but we expect 2. Should still fail. input_str = "10 20 30" @@ -570,11 +598,11 @@ def test_multi_arg_still_validates(): assert tv_parse_list(input_str, defs) == {"x": 10, "y": 20} -def test_single_arg_primitive(): +def test_single_arg_primitive() -> None: """ Sanity check that simple single primitives still work. """ - defs = [("name", str)] + defs: MutableSequence[tuple[tuple[str, str] | str, Any]] = [("name", str)] assert tv_parse_list("my_layer", defs) == {"name": "my_layer"} # Even if it looks like a list, if type is str, it remains a string @@ -592,7 +620,7 @@ def test_single_arg_primitive(): (r"Bell\bChar", "Bell\bChar"), ], ) -def test_standard_escapes(input_text, expected): +def test_standard_escapes(input_text: str, expected: str) -> None: """Verifies that standard escaped characters are correctly converted.""" assert unescape_everything_safely(input_text) == expected @@ -606,7 +634,7 @@ def test_standard_escapes(input_text, expected): (r"Octal\101", "OctalA"), # Octal 'A' ], ) -def test_hex_octal_conversion(input_text, expected): +def test_hex_octal_conversion(input_text: str, expected: str) -> None: """Verifies that valid hex and octal codes are converted.""" assert unescape_everything_safely(input_text) == expected @@ -625,7 +653,7 @@ def test_hex_octal_conversion(input_text, expected): (r"calc\101value", "calcAvalue"), # Unescaped: 'v' is not a separator ], ) -def test_filename_collision_safety(input_text, expected): +def test_filename_collision_safety(input_text: str, expected: str) -> None: """Verifies that filenames looking like hex/octal are PRESERVED.""" assert unescape_everything_safely(input_text) == expected @@ -639,7 +667,7 @@ def test_filename_collision_safety(input_text, expected): (r"E:\recycled", r"E:/recycled"), # \r protected by (? None: """Verifies that standard escapes are ignored if preceded by a Drive Letter (X:).""" assert unescape_everything_safely(input_text) == expected @@ -658,7 +686,7 @@ def test_drive_letter_protection(input_text, expected): (r"aa\r\nbb\r\n\\server\dir\2.2.0\file", "aa\nbb\n//server/dir/2.2.0/file"), ], ) -def test_unc_paths_and_mixed_content(input_text, expected): +def test_unc_paths_and_mixed_content(input_text: str, expected: str) -> None: """Verifies UNC paths and mixed content handling.""" assert unescape_everything_safely(input_text) == expected @@ -689,7 +717,7 @@ def test_unc_paths_and_mixed_content(input_text, expected): @pytest.mark.parametrize("input_text, expected", TEST_MIXED_CONTENT) -def test_mixed_content_robustness(input_text, expected): +def test_mixed_content_robustness(input_text: str, expected: str) -> None: """Ensures paths are correctly extracted from surrounding text.""" # Test normalization specifically assert normalize_windows_paths(input_text) == expected @@ -705,61 +733,36 @@ def test_mixed_content_robustness(input_text, expected): (r"Tab\tCol", "Tab\tCol"), ], ) -def test_mixed_content(input_text: str, expected: str): +def test_mixed_content(input_text: str, expected: str) -> None: """Verifies that paths embedded in text are handled without breaking standard escapes.""" assert unescape_everything_safely(input_text) == expected TEST_AMBIGUITY = [ - # 1. The "Newline" Trap - # Input: \new_file - # Reason: Starts with "\n". It is NOT a Drive, UNC, or Dot-Relative start. - # Expectation: Ignored (Left for unescape_everything_safely to handle later) (r"Start a \new_line here.", r"Start a \new_line here."), - # 2. The "Tab" Trap - # Input: \table - # Reason: Starts with "\t". Not a valid relative start. (r"Insert a \table row.", r"Insert a \table row."), - # 3. Double Dots without path separator - # Reason: Regex requires at least one '\segment' after the start. (r"Go .. back", r"Go .. back"), - # 4. Drive letter without path separator - # Reason: "C:" alone is not enough, needs "C:\Folder". (r"Drive C: is full", r"Drive C: is full"), - # 5. Broken UNC (No Share Name) - # Reason: UNC regex requires \\Host AND \Share to validate. (r"Pinging \\Server now...", r"Pinging \\Server now..."), - # 6. Escaped Backslashes in Code - # Input: "var x = \\;" - # Reason: Not a path structure. (r"var x = \\;", r"var x = \\;"), - # 7. The "Hidden Path" (Ambiguous valid path ending with newline char) - # Input: C:\Data\n - # Reason: The regex consumes "C:\Data". It stops at "\n" because 'n' is valid - # but the backslash before it belongs to the previous segment. - # However, valid_chars excludes \r\n, so it breaks cleanly. (r"C:\Data\nNextLine", "C:/Data/nNextLine"), ] @pytest.mark.parametrize("input_text, expected", TEST_AMBIGUITY) -def test_ambiguity_safety(input_text, expected): +def test_ambiguity_safety(input_text: str, expected: str) -> None: """Ensures that non-paths (escapes, partials) are strictly ignored.""" # Should be untouched by the path normalizer assert normalize_windows_paths(input_text) == expected -def test_full_pipeline_ambiguity(): +def test_full_pipeline_ambiguity() -> None: """ Verifies the interaction between normalization and unescaping. Standard escapes (\n, \t) should survive normalization and get fixed by unescape. """ - # 1. Input has a real path AND a newline escape + # Input has a real path AND a newline escape raw = r"Path: C:\Users\Admin\nStatus: OK" - - # Step 1: Normalize (C:\Users\Admin -> C:/Users/Admin) - # Result: "Path: C:/Users/Admin\nStatus: OK" - # Step 2: Unescape (\n -> Newline) expected = "Path: C:/Users/Admin\nStatus: OK" assert unescape_everything_safely(raw) != expected @@ -773,7 +776,7 @@ def test_full_pipeline_ambiguity(): (r"Z:\Work\real_v1.0", "Z:/Work/real_v1.0"), ], ) -def test_path_normalization(input_text: str, expected: str): +def test_path_normalization(input_text: str, expected: str) -> None: """Verifies that Windows paths are correctly converted to POSIX style.""" assert normalize_windows_paths(input_text) == expected assert unescape_everything_safely(input_text) == expected @@ -796,7 +799,7 @@ def test_path_normalization(input_text: str, expected: str): (r"Check path \..\..\logs now", "Check path /../../logs now"), ], ) -def test_relative_paths(input_text, expected): +def test_relative_paths(input_text: str, expected: str) -> None: # Verify the regex catches the relative starts assert normalize_windows_paths(input_text) == expected assert unescape_everything_safely(input_text) == expected @@ -813,11 +816,11 @@ def test_relative_paths(input_text, expected): (r"\\My-NAS-01\Backup\v1", "//My-NAS-01/Backup/v1"), ], ) -def test_unc_paths(input_text, expected): +def test_unc_paths(input_text: str, expected: str) -> None: assert normalize_windows_paths(input_text) == expected -def test_safety_check(): +def test_safety_check() -> None: """Ensure generic backslashes don't accidentally become paths.""" # \n should NOT match because 'n' is not '.' or '..' assert normalize_windows_paths(r"Line1\nLine2") == r"Line1\nLine2" @@ -840,12 +843,12 @@ def test_safety_check(): (r"C:\Notes\nLine2", "C:/Notes/nLine2"), ], ) -def test_edge_cases(input_text: str, expected: str): +def test_edge_cases(input_text: str, expected: str) -> None: """Verifies robustness against quotes, spaces, and ambiguous inputs.""" assert unescape_everything_safely(input_text) == expected -def test_hex_integrity(): +def test_hex_integrity() -> None: """Verifies that standard hex unescaping still works when no path is involved.""" assert unescape_everything_safely(r"Value\x41") == "ValueA" assert unescape_everything_safely(r"folder\x_file") == r"folder\x_file" diff --git a/tests/george/client/test_rpc.py b/tests/george/client/test_rpc.py index 5720a15..ca82305 100644 --- a/tests/george/client/test_rpc.py +++ b/tests/george/client/test_rpc.py @@ -37,15 +37,11 @@ def test_rpc_increment_max_sys_int(json_rpc_client: JSONRPCClient) -> None: assert json_rpc_client.rpc_id == 0 -def test_rpc_execute_remote( - mocker: MockFixture, json_rpc_client: JSONRPCClient -) -> None: +def test_rpc_execute_remote(mocker: MockFixture, json_rpc_client: JSONRPCClient) -> None: def send(*args: Any) -> int: return 0 - json_response_test = ( - '{"id": 0, "jsonrpc": "2.0", "result": "TVP Animation 11 Pro 11.5.3 fr"}' - ) + json_response_test = '{"id": 0, "jsonrpc": "2.0", "result": "TVP Animation 11 Pro 11.5.3 fr"}' def recv(w: WebSocket) -> str | bytes: return json_response_test diff --git a/tests/george/test_grg_base.py b/tests/george/test_grg_base.py index 1371544..cd54718 100644 --- a/tests/george/test_grg_base.py +++ b/tests/george/test_grg_base.py @@ -4,59 +4,12 @@ from packaging import version from pytest_mock import MockFixture +from pytvpaint import george from pytvpaint.george.client import send_cmd -from pytvpaint.george.exceptions import GeorgeError -from pytvpaint.george.grg_base import ( - AlphaMode, - AlphaSaveMode, - DrawingMode, - FileMode, - HSLColor, - MarkAction, - MarkReference, - MenuElement, - RectButton, - RGBColor, - SaveFormat, - TVPShape, - tv_alpha_load_mode_get, - tv_alpha_load_mode_set, - tv_alpha_save_mode_get, - tv_alpha_save_mode_set, - tv_get_active_shape, - tv_line, - tv_list_request, - tv_mark_in_get, - tv_mark_in_set, - tv_mark_out_get, - tv_mark_out_set, - tv_menu_show, - tv_pen_brush_get, - tv_pen_brush_set, - tv_rect, - tv_rect_fill, - tv_req_angle, - tv_req_file, - tv_req_float, - tv_req_num, - tv_req_string, - tv_request, - tv_save_mode_get, - tv_save_mode_set, - tv_set_a_pen_hsl, - tv_set_a_pen_rgba, - tv_set_active_shape, - tv_text, - tv_text_brush, - tv_undo, - tv_version, -) -from pytvpaint.george.grg_layer import tv_layer_info -from pytvpaint.george.grg_project import TVPProject def test_tv_version() -> None: - name, tvp_version, lang = tv_version() + name, tvp_version, lang = george.tv_version() min_version = version.parse("11.0") current_version = version.parse(tvp_version) @@ -69,185 +22,185 @@ def test_tv_version() -> None: @pytest.mark.parametrize( "cmd", [ - MenuElement.CLIP, - MenuElement.PROJECT, - MenuElement.XSHEET, - MenuElement.NOTES, + george.MenuElement.CLIP, + george.MenuElement.PROJECT, + george.MenuElement.XSHEET, + george.MenuElement.NOTES, ], ) -def test_tv_menu_show_simple(cmd: MenuElement) -> None: - tv_menu_show(cmd) +def test_tv_menu_show_simple(cmd: george.MenuElement) -> None: + george.tv_menu_show(cmd) @pytest.mark.skip("Will break the UI") @pytest.mark.parametrize( "cmd", [ - MenuElement.SHOW_UI, - MenuElement.HIDE_UI, - MenuElement.CENTER_DISPLAY, - MenuElement.FIT_DISPLAY, - MenuElement.FRONT, - MenuElement.BACK, + george.MenuElement.SHOW_UI, + george.MenuElement.HIDE_UI, + george.MenuElement.CENTER_DISPLAY, + george.MenuElement.FIT_DISPLAY, + george.MenuElement.FRONT, + george.MenuElement.BACK, ], ) @pytest.mark.parametrize("current", [True, False]) -def test_tv_menu_show_current(cmd: MenuElement, current: bool) -> None: - tv_menu_show(cmd, current=current) +def test_tv_menu_show_current(cmd: george.MenuElement, current: bool) -> None: + george.tv_menu_show(cmd, current=current) @pytest.mark.skip("Will break the UI") @pytest.mark.parametrize("args", [(0, 0, 50, 50), (-10, -50, 20, 20)]) @pytest.mark.parametrize("current", [True, False]) def test_tv_menu_show_resize_ui(args: tuple[int], current: bool) -> None: - tv_menu_show(MenuElement.RESIZE_UI, *args, current=current) + george.tv_menu_show(george.MenuElement.RESIZE_UI, *args, current=current) @pytest.mark.skip("Will break the UI") @pytest.mark.parametrize("arg", [0, 1]) @pytest.mark.parametrize("current", [True, False]) def test_tv_menu_show_aspect_ratio(arg: int, current: bool) -> None: - tv_menu_show(MenuElement.ASPECT_RATIO, arg, current=current) + george.tv_menu_show(george.MenuElement.ASPECT_RATIO, arg, current=current) @pytest.mark.skip(reason="Will block TVPaint") def test_tv_request() -> None: - tv_request("ksehfkjhkj", "ouiii", "NOOOOO") + george.tv_request("ksehfkjhkj", "ouiii", "NOOOOO") @pytest.mark.skip(reason="Will block TVPaint") def test_tv_req_num() -> None: - tv_req_num(15, 0, 100, title="Request an int") + george.tv_req_num(15, 0, 100, title="Request an int") @pytest.mark.skip(reason="Will block TVPaint") def test_tv_req_angle(mocker: MockFixture) -> None: - tv_req_angle(15, 0, 100, title="Request an angle") + george.tv_req_angle(15, 0, 100, title="Request an angle") @pytest.mark.skip(reason="Will block TVPaint") def test_tv_req_float(mocker: MockFixture) -> None: - tv_req_float(15.0, 0.0, 100.0, title="Request a float") + george.tv_req_float(15.0, 0.0, 100.0, title="Request a float") @pytest.mark.skip(reason="Will block TVPaint") def test_tv_req_string() -> None: - tv_req_string("Request a string", "Hello this is text\nleo") + george.tv_req_string("Request a string", "Hello this is text\nleo") @pytest.mark.skip(reason="Needs user action") def test_tv_list_request() -> None: - tv_list_request(["a/b/c|d"]) + george.tv_list_request(["a/b/c|d"]) @pytest.mark.skip(reason="Needs user action") def test_tv_req_file() -> None: - tv_req_file(FileMode.LOAD, "Open requester", "C:/Users", "out.py", ".py") + george.tv_req_file(george.FileMode.LOAD, "Open requester", "C:/Users", "out.py", ".py") @pytest.mark.skip("Doesn't work?") -def test_undo_command(test_project: TVPProject) -> None: +def test_undo_command(test_project: george.TVPProject) -> None: def create_layer() -> int: return int(send_cmd("tv_LayerCreate", "test_layer")) layer_id = create_layer() assert layer_id - tv_undo() + george.tv_undo() # The layer doesn't exist anymore - with pytest.raises(GeorgeError): - tv_layer_info(layer_id) + with pytest.raises(george.GeorgeError): + george.tv_layer_info(layer_id) def test_save_mode_get() -> None: - fmt, res = tv_save_mode_get() + fmt, res = george.tv_save_mode_get() assert len(fmt.value) > 2 assert res def test_tv_save_mode_set() -> None: - tv_save_mode_set(SaveFormat.BMP) + george.tv_save_mode_set(george.SaveFormat.BMP) def test_tv_alpha_load_mode_get() -> None: - tv_alpha_load_mode_get() + george.tv_alpha_load_mode_get() -@pytest.mark.parametrize("mode", AlphaMode) -def test_tv_alpha_load_mode_set(test_project: TVPProject, mode: AlphaMode) -> None: - tv_alpha_load_mode_set(mode) - assert tv_alpha_load_mode_get() == mode +@pytest.mark.parametrize("mode", george.AlphaMode) +def test_tv_alpha_load_mode_set(test_project: george.TVPProject, mode: george.AlphaMode) -> None: + george.tv_alpha_load_mode_set(mode) + assert george.tv_alpha_load_mode_get() == mode def test_tv_alpha_save_mode_get() -> None: - tv_alpha_save_mode_get() + george.tv_alpha_save_mode_get() @pytest.mark.skip("It does not work for no reason...") -@pytest.mark.parametrize("mode", list(AlphaSaveMode)) -def test_tv_alpha_save_mode_set(test_project: TVPProject, mode: AlphaSaveMode) -> None: - tv_alpha_save_mode_set(mode) - assert tv_alpha_save_mode_get() == mode +@pytest.mark.parametrize("mode", list(george.AlphaSaveMode)) +def test_tv_alpha_save_mode_set(test_project: george.TVPProject, mode: george.AlphaSaveMode) -> None: + george.tv_alpha_save_mode_set(mode) + assert george.tv_alpha_save_mode_get() == mode -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_in_get(ref: MarkReference) -> None: - tv_mark_in_get(ref) +@pytest.mark.parametrize("ref", george.MarkReference) +def test_tv_mark_in_get(ref: george.MarkReference) -> None: + george.tv_mark_in_get(ref) -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_in_set(test_project: TVPProject, ref: MarkReference) -> None: - tv_mark_in_set(ref, 20, MarkAction.SET) - assert tv_mark_in_get(ref) == (20, MarkAction.SET) +@pytest.mark.parametrize("ref", george.MarkReference) +def test_tv_mark_in_set(test_project: george.TVPProject, ref: george.MarkReference) -> None: + george.tv_mark_in_set(ref, 20, george.MarkAction.SET) + assert george.tv_mark_in_get(ref) == (20, george.MarkAction.SET) -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_in_set_clear(test_project: TVPProject, ref: MarkReference) -> None: - tv_mark_in_set(ref, 20, MarkAction.SET) - assert tv_mark_in_get(ref) == (20, MarkAction.SET) - tv_mark_in_set(ref, 20, MarkAction.CLEAR) - assert tv_mark_in_get(ref) == (20, MarkAction.CLEAR) +@pytest.mark.parametrize("ref", george.MarkReference) +def test_tv_mark_in_set_clear(test_project: george.TVPProject, ref: george.MarkReference) -> None: + george.tv_mark_in_set(ref, 20, george.MarkAction.SET) + assert george.tv_mark_in_get(ref) == (20, george.MarkAction.SET) + george.tv_mark_in_set(ref, 20, george.MarkAction.CLEAR) + assert george.tv_mark_in_get(ref) == (20, george.MarkAction.CLEAR) -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_out_get(ref: MarkReference) -> None: - tv_mark_out_get(ref) +@pytest.mark.parametrize("ref", george.MarkReference) +def test_tv_mark_out_get(ref: george.MarkReference) -> None: + george.tv_mark_out_get(ref) -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_out_set(test_project: TVPProject, ref: MarkReference) -> None: - tv_mark_out_set(ref, 20, MarkAction.SET) - assert tv_mark_out_get(ref) == (20, MarkAction.SET) +@pytest.mark.parametrize("ref", george.MarkReference) +def test_tv_mark_out_set(test_project: george.TVPProject, ref: george.MarkReference) -> None: + george.tv_mark_out_set(ref, 20, george.MarkAction.SET) + assert george.tv_mark_out_get(ref) == (20, george.MarkAction.SET) -@pytest.mark.parametrize("ref", MarkReference) -def test_tv_mark_out_set_clear(test_project: TVPProject, ref: MarkReference) -> None: - tv_mark_out_set(ref, 20, MarkAction.SET) - assert tv_mark_out_get(ref) == (20, MarkAction.SET) - tv_mark_out_set(ref, 20, MarkAction.CLEAR) - assert tv_mark_out_get(ref) == (20, MarkAction.CLEAR) +@pytest.mark.parametrize("ref", george.MarkReference) +def test_tv_mark_out_set_clear(test_project: george.TVPProject, ref: george.MarkReference) -> None: + george.tv_mark_out_set(ref, 20, george.MarkAction.SET) + assert george.tv_mark_out_get(ref) == (20, george.MarkAction.SET) + george.tv_mark_out_set(ref, 20, george.MarkAction.CLEAR) + assert george.tv_mark_out_get(ref) == (20, george.MarkAction.CLEAR) def test_tv_get_active_shape() -> None: - assert tv_get_active_shape() in TVPShape + assert george.tv_get_active_shape() in george.TVPShape -@pytest.mark.parametrize("shape", TVPShape) -def test_tv_set_active_shape(shape: TVPShape) -> None: - tv_set_active_shape(shape) +@pytest.mark.parametrize("shape", george.TVPShape) +def test_tv_set_active_shape(shape: george.TVPShape) -> None: + george.tv_set_active_shape(shape) def test_tv_set_a_pen_rgba() -> None: - c = RGBColor(0, 255, 0) - tv_set_a_pen_rgba(c, alpha=40) - assert tv_set_a_pen_rgba(c, alpha=40) == c + c = george.RGBColor(0, 255, 0) + george.tv_set_a_pen_rgba(c, alpha=40) + assert george.tv_set_a_pen_rgba(c, alpha=40) == c def test_tv_set_a_pen_hsl() -> None: - c = HSLColor(50, 51, 100) - tv_set_a_pen_hsl(c) - result = tv_set_a_pen_hsl(c) + c = george.HSLColor(50, 51, 100) + george.tv_set_a_pen_hsl(c) + result = george.tv_set_a_pen_hsl(c) assert c.h - 1 <= result.h <= c.h + 1 assert c.s - 1 <= result.s <= c.s + 1 assert c.l - 1 <= result.l <= c.l + 1 @@ -255,60 +208,56 @@ def test_tv_set_a_pen_hsl() -> None: @pytest.mark.parametrize("tool_mode", [True, False]) def test_tv_pen_brush_get(tool_mode: bool) -> None: - assert tv_pen_brush_get(tool_mode) + assert george.tv_pen_brush_get(tool_mode) -@pytest.mark.parametrize("mode", DrawingMode) +@pytest.mark.parametrize("mode", george.DrawingMode) @pytest.mark.parametrize("tool_mode", [True, False]) -def test_tv_pen_brush_set( - pen_brush_reset: None, mode: DrawingMode, tool_mode: bool -) -> None: - tv_pen_brush_set(mode, tool_mode=tool_mode) +def test_tv_pen_brush_set(pen_brush_reset: None, mode: george.DrawingMode, tool_mode: bool) -> None: + george.tv_pen_brush_set(mode, tool_mode=tool_mode) -@pytest.mark.parametrize("button", [None, *RectButton]) -def test_tv_rect(test_project: TVPProject, button: RectButton | None) -> None: +@pytest.mark.parametrize("button", [None, *george.RectButton]) +def test_tv_rect(test_project: george.TVPProject, button: george.RectButton | None) -> None: width = test_project.width height = test_project.height - tv_rect(0, 0, width, height) + george.tv_rect(0, 0, width, height) @pytest.mark.parametrize("erase_mode", [True, False]) @pytest.mark.parametrize("tool_mode", [True, False]) -def test_tv_rect_fill( - test_project: TVPProject, erase_mode: bool, tool_mode: bool -) -> None: +def test_tv_rect_fill(test_project: george.TVPProject, erase_mode: bool, tool_mode: bool) -> None: width = test_project.width height = test_project.height - tv_rect_fill(0, 0, width, height, 0, width, erase_mode, tool_mode) + george.tv_rect_fill(0, 0, width, height, 0, width, erase_mode, tool_mode) @pytest.mark.parametrize("xy1, xy2", [[(0, 0), (0, 0)], [(0, 0), (100, 100)]]) @pytest.mark.parametrize("right_click", [True, False]) @pytest.mark.parametrize("dry", [True, False]) def test_tv_line( - test_project: TVPProject, + test_project: george.TVPProject, xy1: tuple[int, int], xy2: tuple[int, int], right_click: bool, dry: bool, ) -> None: - tv_line(xy1, xy2, right_click, dry) + george.tv_line(xy1, xy2, right_click, dry) @pytest.mark.parametrize("text", ["", "Hello", "sp a c e s", "$$"]) @pytest.mark.parametrize("x, y", [(0, 0), (-5, 0), (100, 100)]) @pytest.mark.parametrize("use_b_pen", [True, False]) def test_tv_text( - test_project: TVPProject, + test_project: george.TVPProject, text: str, x: int, y: int, use_b_pen: bool, ) -> None: - tv_text(text, x, y, use_b_pen) + george.tv_text(text, x, y, use_b_pen) @pytest.mark.parametrize("text", ["", "Hello", "sp a c e s", "$$"]) def test_tv_text_brush(text: str) -> None: - tv_text_brush(text) + george.tv_text_brush(text) diff --git a/tests/george/test_grg_camera.py b/tests/george/test_grg_camera.py index 601bc5a..2d90593 100644 --- a/tests/george/test_grg_camera.py +++ b/tests/george/test_grg_camera.py @@ -4,27 +4,15 @@ import pytest -from pytvpaint.george.exceptions import GeorgeError -from pytvpaint.george.grg_base import FieldOrder, is_tvp_version_below_12 -from pytvpaint.george.grg_camera import ( - TVPCameraPoint, - tv_camera_enum_points, - tv_camera_info_get, - tv_camera_info_set, - tv_camera_insert_point, - tv_camera_interpolation, - tv_camera_remove_point, - tv_camera_set_point, -) -from pytvpaint.george.grg_project import TVPProject +from pytvpaint import george -def test_tv_camera_info_get(test_project: TVPProject) -> None: - camera = tv_camera_info_get() +def test_tv_camera_info_get(test_project: george.TVPProject) -> None: + camera = george.tv_camera_info_get() assert camera.width == test_project.width assert camera.height == test_project.height assert camera.pixel_aspect_ratio == test_project.pixel_aspect_ratio - if is_tvp_version_below_12(): + if george.is_tvp_version_below_12(): assert camera.frame_rate == test_project.frame_rate @@ -34,19 +22,19 @@ def test_tv_camera_info_get(test_project: TVPProject) -> None: tuple(), (20, 20), (200, 1009), - (500, 500, FieldOrder.LOWER), - (500, 20, FieldOrder.NONE), - (500, 20, FieldOrder.UPPER), - (500, 20, FieldOrder.UPPER, 12.0), - (500, 20, FieldOrder.UPPER, 50.0, 2.0), - (1, 20, FieldOrder.NONE, 50.0, 2.0), + (500, 500, george.FieldOrder.LOWER), + (500, 20, george.FieldOrder.NONE), + (500, 20, george.FieldOrder.UPPER), + (500, 20, george.FieldOrder.UPPER, 12.0), + (500, 20, george.FieldOrder.UPPER, 50.0, 2.0), + (1, 20, george.FieldOrder.NONE, 50.0, 2.0), ], ) def test_tv_camera_info_set( - test_project: TVPProject, + test_project: george.TVPProject, args: tuple[Any, ...], ) -> None: - tv_camera_info_set(*args) + george.tv_camera_info_set(*args) attrs_check = [ "width", @@ -56,69 +44,73 @@ def test_tv_camera_info_set( "pixel_aspect_ratio", ] - camera = tv_camera_info_get() + camera = george.tv_camera_info_get() for attr, arg in zip(attrs_check, args): current = getattr(camera, attr) err_msg = f"Error checking {attr} (expected: {arg}, current: {current})" - if not is_tvp_version_below_12() and attr == 'frame_rate': + if not george.is_tvp_version_below_12() and attr == "frame_rate": continue assert current == arg, err_msg -def test_tv_camera_enum_points(test_project: TVPProject) -> None: - tv_camera_insert_point(0, 0, 0, 0, 1) - assert tv_camera_enum_points(0) +def test_tv_camera_enum_points(test_project: george.TVPProject) -> None: + george.tv_camera_insert_point(0, 0, 0, 0, 1) + assert george.tv_camera_enum_points(0) -def test_tv_camera_enum_points_wrong_index(test_project: TVPProject) -> None: - with pytest.raises(GeorgeError): - tv_camera_enum_points(0) +def test_tv_camera_enum_points_wrong_index(test_project: george.TVPProject) -> None: + with pytest.raises(george.GeorgeError): + george.tv_camera_enum_points(0) def map_value(start: int, end: int, ratio: float) -> float: return start + (end - start) * ratio -@pytest.mark.skipif(not is_tvp_version_below_12(), reason="Function no longer works properly in TVP 12") -def test_tv_camera_interpolation(test_project: TVPProject) -> None: +@pytest.mark.skipif(not george.is_tvp_version_below_12(), reason="Function no longer works properly in TVP 12") +def test_tv_camera_interpolation(test_project: george.TVPProject) -> None: start_x = 0 start_y = 0 end_x = 50 end_y = 50 - tv_camera_insert_point(0, start_x, start_y, 0, 1) - tv_camera_insert_point(5, end_x, end_y, 0, 3) + george.tv_camera_insert_point(0, start_x, start_y, 0, 1) + george.tv_camera_insert_point(5, end_x, end_y, 0, 3) steps = 10 for i in range(steps + 1): ratio = i / steps - inter = tv_camera_interpolation(ratio) + inter = george.tv_camera_interpolation(ratio) assert round(inter.x) == map_value(start_x, end_x, ratio) assert round(inter.y) == map_value(start_y, end_y, ratio) -@pytest.mark.skipif(not is_tvp_version_below_12(), - reason="Skipping since tv_camera_interpolation no longer works properly in TVP 12") -def test_tv_camera_insert_point(test_project: TVPProject) -> None: - point = TVPCameraPoint(50, 26, 0, scale=0.0) - tv_camera_insert_point(0, point.x, point.y, point.angle, point.angle) - assert tv_camera_interpolation(0.0) == point +@pytest.mark.skipif( + not george.is_tvp_version_below_12(), + reason="Skipping since tv_camera_interpolation no longer works properly in TVP 12", +) +def test_tv_camera_insert_point(test_project: george.TVPProject) -> None: + point = george.TVPCameraPoint(50, 26, 0, scale=0.0) + george.tv_camera_insert_point(0, point.x, point.y, point.angle, point.angle) + assert george.tv_camera_interpolation(0.0) == point -def test_tv_camera_remove_point(test_project: TVPProject) -> None: +def test_tv_camera_remove_point(test_project: george.TVPProject) -> None: # Note : leave `test_project` in test args otherwise test won't work - tv_camera_insert_point(0, 50, 25, 0, 0.0) - tv_camera_remove_point(0) + george.tv_camera_insert_point(0, 50, 25, 0, 0.0) + george.tv_camera_remove_point(0) - with pytest.raises(GeorgeError): - tv_camera_enum_points(0) + with pytest.raises(george.GeorgeError): + george.tv_camera_enum_points(0) -@pytest.mark.skipif(not is_tvp_version_below_12(), - reason="Skipping since tv_camera_enum_points no longer works properly in TVP 12") +@pytest.mark.skipif( + not george.is_tvp_version_below_12(), + reason="Skipping since tv_camera_enum_points no longer works properly in TVP 12", +) def test_tv_camera_set_point() -> None: - tv_camera_insert_point(0, 50, 25, 0, 0.0) - new_point = TVPCameraPoint(67, 34, 1, 0.5) - tv_camera_set_point(0, new_point.x, new_point.y, new_point.angle, new_point.scale) - assert tv_camera_enum_points(0) == new_point + george.tv_camera_insert_point(0, 50, 25, 0, 0.0) + new_point = george.TVPCameraPoint(67, 34, 1, 0.5) + george.tv_camera_set_point(0, new_point.x, new_point.y, new_point.angle, new_point.scale) + assert george.tv_camera_enum_points(0) == new_point diff --git a/tests/george/test_grg_clip.py b/tests/george/test_grg_clip.py index c9e46ad..3813538 100644 --- a/tests/george/test_grg_clip.py +++ b/tests/george/test_grg_clip.py @@ -8,151 +8,82 @@ import pytest -from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError -from pytvpaint.george.grg_base import ( - FieldOrder, - SaveFormat, - SpriteLayout, - tv_save_mode_get, -) -from pytvpaint.george.grg_clip import ( - PSDSaveMode, - TVPClip, - tv_bookmark_clear, - tv_bookmark_next, - tv_bookmark_prev, - tv_bookmark_set, - tv_bookmarks_enum, - tv_clip_action_get, - tv_clip_action_set, - tv_clip_close, - tv_clip_color_get, - tv_clip_color_set, - tv_clip_current_id, - tv_clip_dialog_get, - tv_clip_dialog_set, - tv_clip_duplicate, - tv_clip_enum_id, - tv_clip_hidden_get, - tv_clip_hidden_set, - tv_clip_info, - tv_clip_move, - tv_clip_name_get, - tv_clip_name_set, - tv_clip_new, - tv_clip_note_get, - tv_clip_note_set, - tv_clip_save_structure_csv, - tv_clip_save_structure_flix, - tv_clip_save_structure_json, - tv_clip_save_structure_psd, - tv_clip_save_structure_sprite, - tv_clip_select, - tv_clip_selection_get, - tv_clip_selection_set, - tv_first_image, - tv_last_image, - tv_layer_image, - tv_layer_image_get, - tv_load_sequence, - tv_save_clip, - tv_save_display, - tv_save_sequence, - tv_sound_clip_adjust, - tv_sound_clip_info, - tv_sound_clip_new, - tv_sound_clip_reload, - tv_sound_clip_remove, -) -from pytvpaint.george.grg_layer import ( - LayerType, - TVPLayer, - tv_instance_get_name, - tv_layer_current_id, - tv_layer_display_set, - tv_layer_get_id, - tv_layer_info, - tv_layer_kill, - tv_layer_rename, - tv_layer_set, -) -from pytvpaint.george.grg_project import TVPProject, tv_save_project -from pytvpaint.george.grg_scene import tv_scene_current_id, tv_scene_new -from tests.conftest import FixtureYield, test_scene +from pytvpaint import george +from tests.conftest import FixtureYield, load_sequence_with_name, test_scene -def test_tv_clip_info(test_clip: TVPClip) -> None: - assert tv_clip_info(test_clip.id) +def test_tv_clip_info(test_clip: george.TVPClip) -> None: + assert george.tv_clip_info(test_clip.id) -def test_tv_clip_info_wrong_id(test_clip: TVPClip) -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_info(-2) +def test_tv_clip_info_wrong_id(test_clip: george.TVPClip) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_info(-2) def test_tv_clip_enum_id(test_scene: int) -> None: clips: list[int] = [] for i in range(5): - tv_clip_new(f"clip_{i}") - clips.append(tv_clip_current_id()) + george.tv_clip_new(f"clip_{i}") + clips.append(george.tv_clip_current_id()) for i, clip_id in enumerate(clips): # We offset 1 because of default clip - assert tv_clip_enum_id(test_scene, i + 1) == clip_id + assert george.tv_clip_enum_id(test_scene, i + 1) == clip_id def test_tv_clip_enum_id_wrong_scene_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_enum_id(-2, 0) + with pytest.raises(george.GeorgeError): + george.tv_clip_enum_id(-2, 0) @pytest.mark.parametrize("pos", [-1, 1, 50]) def test_tv_clip_enum_id_wrong_clip_pos(test_scene: int, pos: int) -> None: - with pytest.raises(GeorgeError): - tv_clip_enum_id(test_scene, pos) + with pytest.raises(george.GeorgeError): + george.tv_clip_enum_id(test_scene, pos) def test_tv_clip_current_id() -> None: - assert tv_clip_current_id() + assert george.tv_clip_current_id() @pytest.mark.parametrize("name", ["", "a", "0", "lfseflj0", "clip with spaces"]) def test_tv_clip_new(test_scene: int, name: str) -> None: - tv_clip_new(name) - assert tv_clip_info(tv_clip_current_id()).name == name + george.tv_clip_new(name) + assert george.tv_clip_info(george.tv_clip_current_id()).name == name -def test_tv_clip_duplicate(test_scene: int, test_clip: TVPClip) -> None: - tv_clip_duplicate(test_clip.id) - dup_clip = tv_clip_info(tv_clip_current_id()) +def test_tv_clip_duplicate(test_scene: int, test_clip: george.TVPClip) -> None: + george.tv_clip_duplicate(test_clip.id) + dup_clip = george.tv_clip_info(george.tv_clip_current_id()) assert dup_clip == test_clip -def test_tv_clip_close(test_clip: TVPClip) -> None: - tv_clip_close(test_clip.id) - with pytest.raises(NoObjectWithIdError): - tv_clip_info(test_clip.id) +def test_tv_clip_close(test_clip: george.TVPClip) -> None: + george.tv_clip_close(test_clip.id) + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_info(test_clip.id) -def test_tv_clip_name_get(test_clip: TVPClip) -> None: - assert tv_clip_name_get(test_clip.id) == test_clip.name +def test_tv_clip_name_get(test_clip: george.TVPClip) -> None: + assert george.tv_clip_name_get(test_clip.id) == test_clip.name def test_tv_clip_name_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_name_get(-1) + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_name_get(-1) @pytest.mark.parametrize("name", ["_", "a", "0", "lfseflj0"]) -def test_tv_clip_name_set(test_clip: TVPClip, name: str) -> None: - tv_clip_name_set(test_clip.id, name) - assert tv_clip_info(test_clip.id).name == name +def test_tv_clip_name_set(test_clip: george.TVPClip, name: str) -> None: + george.tv_clip_name_set(test_clip.id, name) + assert george.tv_clip_info(test_clip.id).name == name def test_tv_clip_name_set_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_name_get(-1) + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_name_get(-1) # "Copy" the fixture to use it twice in the above test @@ -161,110 +92,108 @@ def test_tv_clip_name_set_wrong_id() -> None: @pytest.mark.parametrize("new_pos", range(5)) -def test_tv_clip_move( - test_scene: int, test_clip: TVPClip, destination_scene: int, new_pos: int -) -> None: +def test_tv_clip_move(test_scene: int, test_clip: george.TVPClip, destination_scene: int, new_pos: int) -> None: for i in range(5): - tv_clip_new(f"other_clip_{i}") + george.tv_clip_new(f"other_clip_{i}") - tv_clip_move(test_clip.id, destination_scene, new_pos) + george.tv_clip_move(test_clip.id, destination_scene, new_pos) # Ensure that it's not in the first scene - with pytest.raises(GeorgeError): - tv_clip_enum_id(test_scene, 1) + with pytest.raises(george.GeorgeError): + george.tv_clip_enum_id(test_scene, 1) # Ensure that it's at the right position in the other scene - tv_clip_enum_id(destination_scene, new_pos) + george.tv_clip_enum_id(destination_scene, new_pos) -def test_tv_clip_hidden_get(test_clip: TVPClip) -> None: - assert tv_clip_hidden_get(test_clip.id) == test_clip.is_hidden +def test_tv_clip_hidden_get(test_clip: george.TVPClip) -> None: + assert george.tv_clip_hidden_get(test_clip.id) == test_clip.is_hidden def test_tv_clip_hidden_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_hidden_get(-1) + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_hidden_get(-1) @pytest.mark.parametrize("hidden", [True, False]) -def test_tv_clip_hidden_set(test_clip: TVPClip, hidden: bool) -> None: - tv_clip_hidden_set(test_clip.id, hidden) - assert tv_clip_info(test_clip.id).is_hidden == hidden +def test_tv_clip_hidden_set(test_clip: george.TVPClip, hidden: bool) -> None: + george.tv_clip_hidden_set(test_clip.id, hidden) + assert george.tv_clip_info(test_clip.id).is_hidden == hidden @pytest.mark.parametrize("hidden", [True, False]) def test_tv_clip_hidden_set_wrong_id(hidden: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_hidden_set(-1, hidden) + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_hidden_set(-1, hidden) -def test_tv_clip_select(test_scene: int, test_clip: TVPClip) -> None: - first_clip = tv_clip_enum_id(test_scene, 0) - tv_clip_select(first_clip) +def test_tv_clip_select(test_scene: int, test_clip: george.TVPClip) -> None: + first_clip = george.tv_clip_enum_id(test_scene, 0) + george.tv_clip_select(first_clip) - assert not tv_clip_info(test_clip.id).is_current - tv_clip_select(test_clip.id) - assert tv_clip_info(test_clip.id).is_current + assert not george.tv_clip_info(test_clip.id).is_current + george.tv_clip_select(test_clip.id) + assert george.tv_clip_info(test_clip.id).is_current def test_tv_clip_select_also_selects_scene( - test_project: TVPProject, + test_project: george.TVPProject, test_scene: int, - test_clip: TVPClip, + test_clip: george.TVPClip, ) -> None: - tv_scene_new() - assert tv_scene_current_id() != test_scene - tv_clip_select(test_clip.id) - assert tv_scene_current_id() == test_scene + george.tv_scene_new() + assert george.tv_scene_current_id() != test_scene + george.tv_clip_select(test_clip.id) + assert george.tv_scene_current_id() == test_scene -def test_tv_clip_selection_get(test_clip: TVPClip) -> None: - assert tv_clip_selection_get(test_clip.id) == test_clip.is_selected +def test_tv_clip_selection_get(test_clip: george.TVPClip) -> None: + assert george.tv_clip_selection_get(test_clip.id) == test_clip.is_selected def test_tv_clip_selection_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_selection_get(-1) + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_selection_get(-1) @pytest.mark.parametrize("select", [True, False]) -def test_tv_clip_selection_set(test_clip: TVPClip, select: bool) -> None: - tv_clip_selection_set(test_clip.id, select) - assert tv_clip_info(test_clip.id).is_selected == select +def test_tv_clip_selection_set(test_clip: george.TVPClip, select: bool) -> None: + george.tv_clip_selection_set(test_clip.id, select) + assert george.tv_clip_info(test_clip.id).is_selected == select @pytest.mark.parametrize("select", [True, False]) -def test_tv_clip_selection_set_wrong_id(test_clip: TVPClip, select: bool) -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_selection_set(-1, select) +def test_tv_clip_selection_set_wrong_id(test_clip: george.TVPClip, select: bool) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_selection_set(-1, select) -def test_tv_first_image(test_clip: TVPClip) -> None: - assert tv_first_image() == test_clip.first_frame +def test_tv_first_image(test_clip: george.TVPClip) -> None: + assert george.tv_first_image() == test_clip.first_frame -def test_tv_last_image(test_clip: TVPClip) -> None: - assert tv_last_image() == test_clip.last_frame +def test_tv_last_image(test_clip: george.TVPClip) -> None: + assert george.tv_last_image() == test_clip.last_frame @pytest.mark.parametrize("offset_count", [None, *itertools.product([0, 1], [0, 1])]) -@pytest.mark.parametrize("field_order", [None, *FieldOrder]) +@pytest.mark.parametrize("field_order", [None, *george.FieldOrder]) @pytest.mark.parametrize("stretch", [False, True]) @pytest.mark.parametrize("time_stretch", [False, True]) @pytest.mark.parametrize("preload", [False, True]) def test_tv_load_sequence( - ppm_sequence: list[Path], - test_clip: TVPClip, + png_sequence: list[Path], + test_clip: george.TVPClip, offset_count: tuple[int, int] | None, - field_order: FieldOrder | None, + field_order: george.FieldOrder | None, stretch: bool, time_stretch: bool, preload: bool, ) -> None: - total_images = len(ppm_sequence) - first_image = ppm_sequence[0] + total_images = len(png_sequence) + first_image = png_sequence[0] - images_loaded = tv_load_sequence( + images_loaded = george.tv_load_sequence( first_image, offset_count, field_order, @@ -288,187 +217,192 @@ def test_tv_load_sequence( layer_name_cut = str(first_image)[:ext_start] - layer = tv_layer_info(tv_layer_current_id()) + layer = george.tv_layer_info(george.tv_layer_current_id()) assert layer.name == layer_name_cut - assert layer.type == LayerType.SEQUENCE + assert layer.type == george.LayerType.SEQUENCE assert layer.first_frame == 0 assert layer.last_frame == should_load - 1 def test_tv_load_sequence_sequence_does_not_exist(tmp_path: Path) -> None: with pytest.raises(FileNotFoundError, match="File not found"): - tv_load_sequence(tmp_path / "file.001.png") + george.tv_load_sequence(tmp_path / "file.001.png") @pytest.fixture -def bookmarks(test_clip: TVPClip) -> FixtureYield[Iterable[int]]: +def bookmarks(test_clip: george.TVPClip) -> FixtureYield[Iterable[int]]: n_bookmarks = 5 for frame in range(n_bookmarks): - tv_bookmark_set(frame) + george.tv_bookmark_set(frame) yield range(n_bookmarks) def test_tv_bookmarks_enum(bookmarks: Iterable[int]) -> None: for pos, mark in enumerate(bookmarks): - assert tv_bookmarks_enum(pos) == mark + assert george.tv_bookmarks_enum(pos) == mark @pytest.mark.parametrize("frame", range(5)) -def test_tv_bookmark_set(test_clip: TVPClip, frame: int) -> None: - tv_bookmark_set(frame) - assert tv_bookmarks_enum(0) == frame +def test_tv_bookmark_set(test_clip: george.TVPClip, frame: int) -> None: + george.tv_bookmark_set(frame) + assert george.tv_bookmarks_enum(0) == frame @pytest.mark.parametrize("frame", range(5)) -def test_tv_bookmark_clear(test_clip: TVPClip, frame: int) -> None: - tv_bookmark_set(frame) - assert tv_bookmarks_enum(0) == frame +def test_tv_bookmark_clear(test_clip: george.TVPClip, frame: int) -> None: + george.tv_bookmark_set(frame) + assert george.tv_bookmarks_enum(0) == frame - tv_bookmark_clear(frame) + george.tv_bookmark_clear(frame) - with pytest.raises(GeorgeError, match="No bookmark"): - tv_bookmarks_enum(0) + with pytest.raises(george.GeorgeError, match="No bookmark"): + george.tv_bookmarks_enum(0) @pytest.mark.parametrize("frame", range(5)) -def test_tv_bookmark_next(test_clip: TVPClip, frame: int) -> None: - tv_bookmark_set(frame) - tv_bookmark_next() - assert tv_layer_image_get() == frame +def test_tv_bookmark_next(test_clip: george.TVPClip, frame: int) -> None: + george.tv_bookmark_set(frame) + george.tv_bookmark_next() + assert george.tv_layer_image_get() == frame @pytest.mark.parametrize("frame", range(5)) -def test_tv_bookmark_prev(test_clip: TVPClip, frame: int) -> None: +def test_tv_bookmark_prev(test_clip: george.TVPClip, frame: int) -> None: # Set the current image far away - tv_layer_image(50) + george.tv_layer_image(50) - tv_bookmark_set(frame) - tv_bookmark_prev() + george.tv_bookmark_set(frame) + george.tv_bookmark_prev() - assert tv_layer_image_get() == frame + assert george.tv_layer_image_get() == frame -def test_tv_clip_color_get(test_clip: TVPClip) -> None: - assert tv_clip_color_get(test_clip.id) in range(27) +def test_tv_clip_color_get(test_clip: george.TVPClip) -> None: + assert george.tv_clip_color_get(test_clip.id) in range(27) @pytest.mark.parametrize("color_index", range(27)) -def test_tv_clip_color_set(test_clip: TVPClip, color_index: int) -> None: - tv_clip_color_set(test_clip.id, color_index) - assert tv_clip_color_get(test_clip.id) == color_index +def test_tv_clip_color_set(test_clip: george.TVPClip, color_index: int) -> None: + george.tv_clip_color_set(test_clip.id, color_index) + assert george.tv_clip_color_get(test_clip.id) == color_index -def test_tv_clip_action_get(test_clip: TVPClip) -> None: - assert tv_clip_action_get(test_clip.id) == "" +def test_tv_clip_action_get(test_clip: george.TVPClip) -> None: + assert george.tv_clip_action_get(test_clip.id) == "" def test_tv_clip_action_get_wrong_id() -> None: - with pytest.raises(NoObjectWithIdError): - tv_clip_action_get(-3) + with pytest.raises(george.NoObjectWithIdError): + george.tv_clip_action_get(-3) # Note: we removed the \n test because there's a bug with TVPaint that handle control characters -TEST_TEXTS = ["", "l", "0", "ab", "a0l", "ap*"] # "a\nb"] +TEST_TEXTS = ["", "l", "0", "ab", "a0l", "ap*", r"a\nb"] @pytest.mark.parametrize("text", TEST_TEXTS) -def test_tv_clip_action_set(test_clip: TVPClip, text: str) -> None: - tv_clip_action_set(test_clip.id, text) - assert tv_clip_action_get(test_clip.id) == text +def test_tv_clip_action_set(test_clip: george.TVPClip, text: str) -> None: + george.tv_clip_action_set(test_clip.id, text) + assert george.tv_clip_action_get(test_clip.id) == text def test_tv_clip_action_set_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_action_set(-2, "test") + with pytest.raises(george.GeorgeError): + george.tv_clip_action_set(-2, "test") -def test_tv_clip_dialog_get(test_clip: TVPClip) -> None: - assert tv_clip_dialog_get(test_clip.id) == "" +def test_tv_clip_dialog_get(test_clip: george.TVPClip) -> None: + assert george.tv_clip_dialog_get(test_clip.id) == "" def test_tv_clip_dialog_get_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_dialog_get(-2) + with pytest.raises(george.GeorgeError): + george.tv_clip_dialog_get(-2) @pytest.mark.parametrize("text", TEST_TEXTS) -def test_tv_clip_dialog_set(test_clip: TVPClip, text: str) -> None: - tv_clip_dialog_set(test_clip.id, text) - assert tv_clip_dialog_get(test_clip.id) == text +def test_tv_clip_dialog_set(test_clip: george.TVPClip, text: str) -> None: + george.tv_clip_dialog_set(test_clip.id, text) + assert george.tv_clip_dialog_get(test_clip.id) == text def test_tv_clip_dialog_set_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_dialog_set(-2, "test") + with pytest.raises(george.GeorgeError): + george.tv_clip_dialog_set(-2, "test") -def test_tv_clip_note_get(test_clip: TVPClip) -> None: - assert tv_clip_note_get(test_clip.id) == "" +def test_tv_clip_note_get(test_clip: george.TVPClip) -> None: + assert george.tv_clip_note_get(test_clip.id) == "" def test_tv_clip_note_get_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_dialog_get(-2) + with pytest.raises(george.GeorgeError): + george.tv_clip_dialog_get(-2) @pytest.mark.parametrize("text", TEST_TEXTS) -def test_tv_clip_note_set(test_clip: TVPClip, text: str) -> None: - tv_clip_note_set(test_clip.id, text) - assert tv_clip_note_get(test_clip.id) == text +def test_tv_clip_note_set(test_clip: george.TVPClip, text: str) -> None: + george.tv_clip_note_set(test_clip.id, text) + assert george.tv_clip_note_get(test_clip.id) == text def test_tv_clip_note_set_wrong_id() -> None: - with pytest.raises(GeorgeError): - tv_clip_note_set(-2, "test") + with pytest.raises(george.GeorgeError): + george.tv_clip_note_set(-2, "test") def test_tv_save_clip(tmp_path: Path) -> None: clip_tvp = tmp_path / "clip.tvp" - tv_save_clip(clip_tvp) + george.tv_save_clip(clip_tvp) assert clip_tvp.exists() @pytest.mark.skip("Will block the UI") def test_tv_save_clip_folder_does_not_exist(tmp_path: Path) -> None: - with pytest.raises(GeorgeError, match="Can't create file"): - tv_save_clip(tmp_path / "folder" / "out.tvpx") + with pytest.raises(george.GeorgeError, match="Can't create file"): + george.tv_save_clip(tmp_path / "folder" / "out.tvpx") def test_tv_save_display(tmp_path: Path) -> None: - save_ext, _ = tv_save_mode_get() - ext = "jpg" if save_ext == SaveFormat.JPG else save_ext.value + save_ext, _ = george.tv_save_mode_get() + ext = "jpg" if save_ext == george.SaveFormat.JPG else save_ext.value out_display = (tmp_path / "out").with_suffix("." + ext) - tv_save_display(out_display) + george.tv_save_display(out_display) assert out_display.exists() -def current_clip_layers() -> Iterator[TVPLayer]: +def current_clip_layers() -> Iterator[george.TVPLayer]: """Iterates through the current clip layers""" pos = 0 while True: try: - lid = tv_layer_get_id(pos) - yield tv_layer_info(lid) - except GeorgeError: + lid = george.tv_layer_get_id(pos) + l_info = george.tv_layer_info(lid) + if l_info.type == george.LayerType.CAMERA: + george.tv_layer_display_set(lid, False) + pos += 1 + continue + yield george.tv_layer_info(lid) + except george.GeorgeError: break pos += 1 def get_instance_frames() -> Iterator[tuple[int, str]]: """Iterates through the instances of the current layer""" - layer = tv_layer_current_id() + layer = george.tv_layer_current_id() frame = 0 while True: try: - name = tv_instance_get_name(layer, frame) + name = george.tv_instance_get_name(layer, frame) yield frame, name - except NoObjectWithIdError: + except george.NoObjectWithIdError: break frame += 1 -def apply_folder_pattern(initial_pattern: str | None, layer: TVPLayer) -> str: +def apply_folder_pattern(initial_pattern: str | None, layer: george.TVPLayer) -> str: # This is the default folder pattern if initial_pattern is None: return f"[{layer.position:03d}] {layer.name}" @@ -476,7 +410,7 @@ def apply_folder_pattern(initial_pattern: str | None, layer: TVPLayer) -> str: patterns = { r"%li": str(layer.position), r"%ln": layer.name, - r"%fi": r"%fi", # Couldn't make it work + r"%fi": r"%fi", # does not work } for pattern, rep in patterns.items(): @@ -487,7 +421,7 @@ def apply_folder_pattern(initial_pattern: str | None, layer: TVPLayer) -> str: def apply_file_pattern( initial_pattern: str | None, - layer: TVPLayer, + layer: george.TVPLayer, image_index: int, image_name: str, file_index: int, @@ -514,57 +448,45 @@ def apply_file_pattern( return initial_pattern -def load_sequence_with_name(first_frame: Path, name: str, count: int) -> int: - """Load an image sequence and rename the layer""" - tv_load_sequence(first_frame, offset_count=(0, count)) - layer = tv_layer_current_id() - tv_layer_rename(layer, name) - return layer - - -@pytest.mark.parametrize( - "mark_in, mark_out", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)] -) +@pytest.mark.parametrize("mark_in, mark_out", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)]) def test_tv_save_sequence( - test_project: TVPProject, + test_project: george.TVPProject, tmp_path: Path, - ppm_sequence: list[Path], + png_sequence: list[Path], mark_in: int | None, mark_out: int | None, ) -> None: - tv_load_sequence(ppm_sequence[0]) + george.tv_load_sequence(png_sequence[0]) - save_ext, _ = tv_save_mode_get() + save_ext, _ = george.tv_save_mode_get() out_sequence = tmp_path / "out" - tv_save_sequence(out_sequence, mark_in, mark_out) + george.tv_save_sequence(out_sequence, mark_in, mark_out) - clip = tv_clip_info(tv_clip_current_id()) + clip = george.tv_clip_info(george.tv_clip_current_id()) start, end = ( - (mark_in, mark_out) - if mark_in is not None and mark_out is not None - else (clip.first_frame, clip.last_frame) + (mark_in, mark_out) if mark_in is not None and mark_out is not None else (clip.first_frame, clip.last_frame) ) for i in range(end - start): image_name = f"{out_sequence.name}{i:05d}" - image_ext = "." + ("jpg" if save_ext == SaveFormat.JPG else save_ext.value) + image_ext = "." + ("jpg" if save_ext == george.SaveFormat.JPG else save_ext.value) image_path = out_sequence.with_name(f"{image_name}{image_ext}") assert image_path.exists() def test_tv_save_sequence_wrong_path(tmp_path: Path) -> None: with pytest.raises(NotADirectoryError): - tv_save_sequence(tmp_path / "folder" / "out") + george.tv_save_sequence(tmp_path / "folder" / "out") @pytest.mark.parametrize( "file_format", [ - SaveFormat.PNG, - # SaveFormat.JPG, - # SaveFormat.BMP, - # SaveFormat.TGA, - # SaveFormat.TIFF, + george.SaveFormat.PNG, + george.SaveFormat.JPG, + george.SaveFormat.BMP, + george.SaveFormat.TGA, + george.SaveFormat.TIFF, ], ) @pytest.mark.parametrize("fill_background", [False, True]) @@ -573,10 +495,10 @@ def test_tv_save_sequence_wrong_path(tmp_path: Path) -> None: @pytest.mark.parametrize("visible_layers_only", [False, True]) @pytest.mark.parametrize("all_images", [False, True]) def test_tv_clip_save_structure_json( - test_project: TVPProject, - ppm_sequence: list[Path], + test_project: george.TVPProject, + png_sequence: list[Path], tmp_path: Path, - file_format: SaveFormat, + file_format: george.SaveFormat, fill_background: bool, folder_pattern: str, file_pattern: str, @@ -584,27 +506,21 @@ def test_tv_clip_save_structure_json( all_images: bool, ) -> None: # Import some frames - load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) - load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=3) + load_sequence_with_name(png_sequence[0], name="sequence_1", count=5) + load_sequence_with_name(png_sequence[0], name="sequence_2", count=3) # Hide that layer - seq_3 = load_sequence_with_name(ppm_sequence[0], name="sequence_3", count=3) - tv_layer_display_set(seq_3, False) + seq_3 = load_sequence_with_name(png_sequence[0], name="sequence_3", count=3) + george.tv_layer_display_set(seq_3, False) - # Remove the first layer (which is in the last position) - clip_layers = [] - for layer in current_clip_layers(): - if layer.type == LayerType.CAMERA: - tv_layer_display_set(layer.id, False) - continue - clip_layers.append(layer) + clip_layers = list(current_clip_layers()) + # Remove the first/default layer (which should in the last position) last_layer = clip_layers[-1] - # assert True is False - tv_layer_kill(last_layer.id) + george.tv_layer_kill(last_layer.id) out_json_path = tmp_path / "clip.json" - tv_clip_save_structure_json( + george.tv_clip_save_structure_json( out_json_path, file_format, fill_background, @@ -615,17 +531,16 @@ def test_tv_clip_save_structure_json( ) assert out_json_path.exists() - for layer in clip_layers: + for layer in current_clip_layers(): if visible_layers_only and not layer.visibility: continue # Check that the layer folders exist layer_folder_name = apply_folder_pattern(folder_pattern, layer) - print('layer_folder_name ===> ', layer_folder_name) layer_folder = tmp_path / layer_folder_name assert layer_folder.exists() - tv_layer_set(layer.id) + george.tv_layer_set(layer.id) for i, instance in enumerate(get_instance_frames()): frame, name = instance @@ -638,39 +553,39 @@ def test_tv_clip_save_structure_json( ) # Check that the images exist - ext = "." + file_format.value + ext = george.SaveFormat.to_extension(file_format) image_path = (layer_folder / image_name).with_suffix(ext) assert image_path.exists() def test_tv_clip_save_structure_json_file_doesnt_exist(tmp_path: Path) -> None: with pytest.raises(ValueError, match="destination folder doesn't exist"): - tv_clip_save_structure_json(tmp_path / "folder" / "out.json", SaveFormat.PNG) + george.tv_clip_save_structure_json(tmp_path / "folder" / "out.json", george.SaveFormat.PNG) @pytest.mark.parametrize( "test_case", [ - (PSDSaveMode.ALL, {}), - (PSDSaveMode.IMAGE, {"image": 0}), - (PSDSaveMode.MARKIN, {"mark_in": 0, "mark_out": 5}), + (george.PSDSaveMode.ALL, {}), + (george.PSDSaveMode.IMAGE, {"image": 0}), + (george.PSDSaveMode.MARKIN, {"mark_in": 0, "mark_out": 5}), ], ) def test_tv_clip_save_structure_psd( - test_project: TVPProject, - ppm_sequence: list[Path], + test_project: george.TVPProject, + png_sequence: list[Path], tmp_path: Path, - test_case: tuple[PSDSaveMode, dict[str, Any]], + test_case: tuple[george.PSDSaveMode, dict[str, Any]], ) -> None: out_psd = tmp_path / "out.psd" - load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) - load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=2) + load_sequence_with_name(png_sequence[0], name="sequence_1", count=5) + load_sequence_with_name(png_sequence[0], name="sequence_2", count=2) mode, args = test_case - tv_clip_save_structure_psd(out_psd, mode, **args) + george.tv_clip_save_structure_psd(out_psd, mode, **args) - if mode == PSDSaveMode.MARKIN: + if mode == george.PSDSaveMode.MARKIN: # It's a sequence of numbered PSD files for i, _ in enumerate(get_instance_frames()): out_psd_frame = out_psd.with_stem(f"{out_psd.stem}{i:05d}") @@ -680,204 +595,209 @@ def test_tv_clip_save_structure_psd( assert out_psd.exists() -# def test_tv_tv_clip_save_structure_psd_file_does_not_exist(tmp_path: Path) -> None: -# with pytest.raises(ValueError, match="destination folder doesn't exist"): -# tv_clip_save_structure_psd(tmp_path / "folder" / "out.psd", PSDSaveMode.ALL) -# -# -# @pytest.mark.parametrize("all_images", [None, False, True]) -# @pytest.mark.parametrize("exposure_label", [None, "", "expo", "ex po"]) -# def test_tv_clip_save_structure_csv( -# test_project: TVPProject, -# ppm_sequence: list[Path], -# tmp_path: Path, -# all_images: bool | None, -# exposure_label: str | None, -# ) -> None: -# out_csv = tmp_path / "out.csv" -# -# load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) -# load_sequence_with_name(ppm_sequence[0], name="sequence_2", count=2) -# -# # Remove the first layer (which is in the last position) -# last_layer = list(current_clip_layers())[-1] -# tv_layer_kill(last_layer.id) -# -# tv_clip_save_structure_csv(out_csv, all_images, exposure_label) -# -# assert out_csv.exists() -# -# out_layers_folder = out_csv.with_suffix(".layers") -# assert out_layers_folder.exists() -# -# for i, layer in enumerate(current_clip_layers()): -# layer_index = f"{(i + 1):03d}" -# layer_folder_name = f"[{layer_index}] {layer.name}" -# layer_folder = out_layers_folder / layer_folder_name -# assert layer_folder.exists() -# -# tv_layer_set(layer.id) -# for j, _ in enumerate(get_instance_frames()): -# image_name = f"[{layer_index}][{(j + 1):05d}] {layer.name}.png" -# assert (layer_folder / image_name).exists() -# -# -# @pytest.mark.parametrize("layout", [None, *SpriteLayout]) -# @pytest.mark.parametrize("space", [None, 0, 5, 50]) -# def test_tv_clip_save_structure_sprite( -# test_project: TVPProject, -# ppm_sequence: list[Path], -# tmp_path: Path, -# layout: SpriteLayout | None, -# space: int | None, -# ) -> None: -# load_sequence_with_name(ppm_sequence[0], name="sequence_1", count=5) -# -# out_sprite = tmp_path / "out.png" -# tv_clip_save_structure_sprite(out_sprite, layout, space) -# -# assert out_sprite.exists() -# -# -# @pytest.mark.parametrize("mark_in_out", [None, (0, 0), (0, 5), (2, 5)]) -# def test_tv_clip_save_structure_flix( -# test_project: TVPProject, -# ppm_sequence: list[Path], -# tmp_path: Path, -# mark_in_out: tuple[int, int] | 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) -# -# # Export to flix needs to save the project first -# tv_save_project(test_project.path) -# -# mark_in: int | None -# mark_out: int | None -# -# if mark_in_out: -# mark_in, mark_out = mark_in_out -# else: -# mark_in = mark_out = None -# -# out_flix = tmp_path / "out.xml" -# -# tv_clip_save_structure_flix(out_flix, mark_in, mark_out, send=False) -# assert out_flix.exists() -# -# -# def test_tv_sound_clip_info(test_clip: TVPClip, wav_file: Path) -> None: -# tv_sound_clip_new(wav_file) -# sound = tv_sound_clip_info(test_clip.id, 0) -# -# assert sound.sound_in == 0 -# assert Path(sound.path) == wav_file -# -# -# def test_tv_sound_clip_info_wrong_clip_id() -> None: -# with pytest.raises(GeorgeError): -# tv_sound_clip_info(-2, 0) -# -# -# def test_tv_sound_clip_info_wrong_sound_track_pos(test_clip: TVPClip) -> None: -# with pytest.raises(GeorgeError): -# tv_sound_clip_info(test_clip.id, 1) -# -# -# def test_tv_sound_clip_new( -# test_project: TVPProject, -# test_clip: TVPClip, -# wav_file: Path, -# ) -> None: -# for i in range(5): -# tv_sound_clip_new(wav_file) -# assert tv_sound_clip_info(test_clip.id, i) -# -# -# def test_tv_sound_clip_new_wrong_path(tmp_path: Path) -> None: -# with pytest.raises(ValueError): -# tv_sound_clip_new(tmp_path / "unknown.wav") -# -# -# def test_tv_sound_clip_remove(test_clip: TVPClip, wav_file: Path) -> None: -# tv_sound_clip_new(wav_file) -# assert tv_sound_clip_info(test_clip.id, 0) -# tv_sound_clip_remove(0) -# -# with pytest.raises(GeorgeError): -# tv_sound_clip_info(test_clip.id, 0) -# -# -# def test_tv_sound_clip_remove_wrong_pos() -> None: -# with pytest.raises(GeorgeError): -# tv_sound_clip_remove(1) -# -# -# def test_tv_sound_clip_reload(test_clip: TVPClip, wav_file: Path) -> None: -# tv_sound_clip_new(wav_file) -# tv_sound_clip_reload(0, 0) -# -# -# def test_tv_sound_clip_reload_wrong_sound_clip_id() -> None: -# with pytest.raises(GeorgeError): -# tv_sound_clip_reload(6, 0) -# -# -# def test_tv_sound_clip_reload_wrong_track_index(test_clip: TVPClip) -> None: -# with pytest.raises(GeorgeError): -# tv_sound_clip_reload(test_clip.id, 5) -# -# -# def args_optional_combine(args: list[list[Any]]) -> list[list[Any]]: -# return [ -# list(prod) -# for n in range(len(args) + 1) -# for prod in itertools.product(*args[:n]) -# ] -# -# -# @pytest.mark.skip("Too difficult to test") -# @pytest.mark.parametrize( -# "args", -# args_optional_combine( -# [ -# [False, True], # mute -# [0.0, 1.0, 5.0], # volume -# [0, 1.0], # offset -# [5.0], # fade_in_start -# ] -# ), -# ) -# def test_tv_sound_clip_adjust( -# test_clip: TVPClip, wav_file: Path, args: list[Any] -# ) -> None: -# tv_sound_clip_new(wav_file) -# track_index = 0 -# -# tv_sound_clip_adjust(track_index, *args) -# -# sound_clip = tv_sound_clip_info(test_clip.id, track_index) -# -# real_values = [ -# sound_clip.mute, -# sound_clip.volume, -# sound_clip.offset, -# sound_clip.fade_in_start, -# sound_clip.fade_in_stop, -# sound_clip.fade_out_start, -# sound_clip.fade_out_stop, -# sound_clip.color_index, -# ] -# -# for real, expected in zip(real_values, args): -# assert real == expected -# -# -# def test_tv_layer_image_get(test_project: TVPLayer) -> None: -# assert tv_layer_image_get() == 0 -# -# -# @pytest.mark.parametrize("frame", range(10)) -# def test_tv_layer_image(frame: int) -> None: -# tv_layer_image(frame) -# assert tv_layer_image_get() == frame +def test_tv_tv_clip_save_structure_psd_file_does_not_exist(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="destination folder doesn't exist"): + george.tv_clip_save_structure_psd(tmp_path / "folder" / "out.psd", george.PSDSaveMode.ALL) + + +@pytest.mark.parametrize("all_images", [None, False, True]) +@pytest.mark.parametrize("exposure_label", [None, "", "expo", "ex po"]) +def test_tv_clip_save_structure_csv( + test_project: george.TVPProject, + png_sequence: list[Path], + tmp_path: Path, + all_images: bool | None, + exposure_label: str | None, +) -> None: + from packaging import version + + _, tvp_version, _ = george.tv_version() + current_version = version.parse(tvp_version) + out_csv = tmp_path / "out.csv" + + load_sequence_with_name(png_sequence[0], name="sequence_1", count=5) + load_sequence_with_name(png_sequence[0], name="sequence_2", count=2) + + # Remove the first/default layer (which should in the last position) + last_layer = list(current_clip_layers())[-1] + george.tv_layer_kill(last_layer.id) + + george.tv_clip_save_structure_csv(out_csv, all_images, exposure_label) + + assert out_csv.exists() + + out_layers_folder = out_csv.with_suffix(".layers") + assert out_layers_folder.exists() + + for i, layer in enumerate(current_clip_layers()): + layer_index = f"{(i + 1):03d}" + if current_version.major >= 12: # noqa: SIM108 + layer_folder_name = f"[{layer_index}] Layer" + else: + layer_folder_name = f"[{layer_index}] {layer.name}" + layer_folder = out_layers_folder / layer_folder_name + assert layer_folder.exists() + + george.tv_layer_set(layer.id) + + for j, _ in enumerate(get_instance_frames()): + if current_version.major >= 12: + image_name = f"[{layer_index}][{(j + 1):05d}] .png" + else: + image_name = f"[{layer_index}][{(j + 1):05d}] {layer.name}.png" + assert (layer_folder / image_name).exists() + + +@pytest.mark.parametrize("layout", [None, *george.SpriteLayout]) +@pytest.mark.parametrize("space", [None, 0, 5, 50]) +def test_tv_clip_save_structure_sprite( + test_project: george.TVPProject, + png_sequence: list[Path], + tmp_path: Path, + layout: george.SpriteLayout | None, + space: int | None, +) -> None: + load_sequence_with_name(png_sequence[0], name="sequence_1", count=5) + + out_sprite = tmp_path / "out.png" + george.tv_clip_save_structure_sprite(out_sprite, layout, space) + + assert out_sprite.exists() + + +@pytest.mark.parametrize("mark_in_out", [None, (0, 0), (0, 5), (2, 5)]) +def test_tv_clip_save_structure_flix( + test_project: george.TVPProject, + png_sequence: list[Path], + tmp_path: Path, + mark_in_out: tuple[int, int] | None, +) -> None: + load_sequence_with_name(png_sequence[0], name="sequence_1", count=5) + load_sequence_with_name(png_sequence[0], name="sequence_2", count=3) + + # Export to flix needs to save the project first + george.tv_save_project(test_project.path) + + mark_in: int | None + mark_out: int | None + + if mark_in_out: + mark_in, mark_out = mark_in_out + else: + mark_in = mark_out = None + + out_flix = tmp_path / "out.xml" + + george.tv_clip_save_structure_flix(out_flix, mark_in, mark_out, send=False) + assert out_flix.exists() + + +def test_tv_sound_clip_info(test_clip: george.TVPClip, wav_file: Path) -> None: + george.tv_sound_clip_new(wav_file) + sound = george.tv_sound_clip_info(test_clip.id, 0) + + assert sound.sound_in == 0 + assert Path(sound.path) == wav_file + + +def test_tv_sound_clip_info_wrong_clip_id() -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_clip_info(-2, 0) + + +def test_tv_sound_clip_info_wrong_sound_track_pos(test_clip: george.TVPClip) -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_clip_info(test_clip.id, 1) + + +def test_tv_sound_clip_new( + test_project: george.TVPProject, + test_clip: george.TVPClip, + wav_file: Path, +) -> None: + for i in range(5): + george.tv_sound_clip_new(wav_file) + assert george.tv_sound_clip_info(test_clip.id, i) + + +def test_tv_sound_clip_new_wrong_path(tmp_path: Path) -> None: + with pytest.raises(ValueError): + george.tv_sound_clip_new(tmp_path / "unknown.wav") + + +def test_tv_sound_clip_remove(test_clip: george.TVPClip, wav_file: Path) -> None: + george.tv_sound_clip_new(wav_file) + assert george.tv_sound_clip_info(test_clip.id, 0) + george.tv_sound_clip_remove(0) + + with pytest.raises(george.GeorgeError): + george.tv_sound_clip_info(test_clip.id, 0) + + +def test_tv_sound_clip_remove_wrong_pos() -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_clip_remove(1) + + +def test_tv_sound_clip_reload(test_clip: george.TVPClip, wav_file: Path) -> None: + george.tv_sound_clip_new(wav_file) + george.tv_sound_clip_reload(0, 0) + + +def test_tv_sound_clip_reload_wrong_sound_clip_id() -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_clip_reload(6, 0) + + +def test_tv_sound_clip_reload_wrong_track_index(test_clip: george.TVPClip) -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_clip_reload(test_clip.id, 5) + + +def args_optional_combine(args: list[list[Any]]) -> list[list[Any]]: + return [list(prod) for n in range(len(args) + 1) for prod in itertools.product(*args[:n])] + + +@pytest.mark.skip("Too difficult to test") +@pytest.mark.parametrize( + "args", + args_optional_combine( + [ + [False, True], # mute + [0.0, 1.0, 5.0], # volume + [0, 1.0], # offset + [5.0], # fade_in_start + ] + ), +) +def test_tv_sound_clip_adjust(test_clip: george.TVPClip, wav_file: Path, args: list[Any]) -> None: + george.tv_sound_clip_new(wav_file) + track_index = 0 + + george.tv_sound_clip_adjust(track_index, *args) + + sound_clip = george.tv_sound_clip_info(test_clip.id, track_index) + + real_values = [ + sound_clip.mute, + sound_clip.volume, + sound_clip.offset, + sound_clip.fade_in_start, + sound_clip.fade_in_stop, + sound_clip.fade_out_start, + sound_clip.fade_out_stop, + sound_clip.color_index, + ] + + for real, expected in zip(real_values, args): + assert real == expected + + +def test_tv_layer_image_get(test_project: george.TVPLayer) -> None: + assert george.tv_layer_image_get() == 0 + + +@pytest.mark.parametrize("frame", range(10)) +def test_tv_layer_image(frame: int) -> None: + george.tv_layer_image(frame) + assert george.tv_layer_image_get() == frame diff --git a/tests/george/test_grg_guideline.py b/tests/george/test_grg_guideline.py new file mode 100644 index 0000000..4e89659 --- /dev/null +++ b/tests/george/test_grg_guideline.py @@ -0,0 +1,281 @@ +import contextlib +from pathlib import Path + +import pytest + +from pytvpaint import george +from tests.conftest import FixtureYield + + +@pytest.fixture(autouse=True) +def clean_guidelines() -> FixtureYield[None]: + """Wipe all guidelines before and after each test to ensure a clean TVP state.""" + + def _remove_all() -> None: + for g_type in george.GuidelineType: + with contextlib.suppress(Exception): + george.tv_guideline_remove_all(g_type) + + _remove_all() + yield + _remove_all() + + +def test_grg_guideline_base_properties(guideline_pos: int) -> None: + """Test getting and setting global/common properties on a guideline.""" + assert george.tv_guideline_enum(guideline_pos, george.GuidelineType.LINE) == guideline_pos + + # Name + george.tv_guideline_name_set(guideline_pos, "test_line") + assert george.tv_guideline_name_get(guideline_pos) == "test_line" + + # Collapse + george.tv_guideline_collapse_set(guideline_pos, collapse=True) + assert george.tv_guideline_collapse_get(guideline_pos) is True + + # Removal + george.tv_guideline_remove(guideline_pos, george.GuidelineType.LINE) + with pytest.raises(Exception): # Assuming your client raises a GeorgeError on invalid enum + george.tv_guideline_enum(guideline_pos, george.GuidelineType.LINE) + + +def test_grg_guideline_visibility(guideline_pos: int) -> None: + george.tv_guideline_visibility_set(guideline_pos, is_visible=False) + assert george.tv_guideline_visibility_get(guideline_pos) is False + # Visibility Apply All + george.tv_guideline_visibility_set_all(george.GuidelineType.LINE, is_visible=True) + assert george.tv_guideline_visibility_get(guideline_pos) is True + george.tv_guideline_visibility_set_all(None, is_visible=False, apply_all=True) + assert george.tv_guideline_visibility_get(guideline_pos) is False + # Visibility Global + george.tv_guideline_visibility_set(0, is_visible=False, on_global=True) + assert george.tv_guideline_visibility_get(0, on_global=True) is False + george.tv_guideline_visibility_set(0, is_visible=True, on_global=True) + assert george.tv_guideline_visibility_get(0, on_global=True) is True + + +def test_grg_guideline_margin(guideline_pos: int) -> None: + george.tv_guideline_margin_set(guideline_pos, margin=10) + assert george.tv_guideline_margin_get(guideline_pos) == 10 + # Margin Apply All + george.tv_guideline_margin_set_all(george.GuidelineType.LINE, margin=20) + assert george.tv_guideline_margin_get(guideline_pos) == 20 + george.tv_guideline_margin_set_all(None, margin=35, apply_all=True) + assert george.tv_guideline_margin_get(guideline_pos) == 35 + + +def test_grg_guideline_color(guideline_pos: int) -> None: + test_color = george.RGBAColor(255, 128, 64, 255) + george.tv_guideline_color_set(guideline_pos, test_color) + assert george.tv_guideline_color_get(guideline_pos) == test_color + # Color Apply All + test_color2 = george.RGBAColor(24, 165, 123, 255) + george.tv_guideline_color_set_all(george.GuidelineType.LINE, color=test_color2) + assert george.tv_guideline_color_get(guideline_pos) == test_color2 + + test_color3 = george.RGBAColor(76, 46, 58, 255) + george.tv_guideline_color_set_all(None, color=test_color3, apply_all=True) + assert george.tv_guideline_color_get(guideline_pos) == test_color3 + + +def test_grg_guideline_snap(guideline_pos: int) -> None: + # set visibility ot True otherwise snap functions won't work + george.tv_guideline_visibility_set_all(None, is_visible=True, apply_all=True) + + george.tv_guideline_snap_set(guideline_pos, snap=False) + assert george.tv_guideline_snap_get(guideline_pos) is False + george.tv_guideline_snap_set(guideline_pos, snap=True) + assert george.tv_guideline_snap_get(guideline_pos) is True + george.tv_guideline_snap_set(guideline_pos, snap=False) + assert george.tv_guideline_snap_get(guideline_pos) is False + # Color Snap All + george.tv_guideline_snap_set_all(george.GuidelineType.LINE, snap=True) + assert george.tv_guideline_snap_get(guideline_pos) is True + george.tv_guideline_snap_set_all(None, snap=False, apply_all=True) + assert george.tv_guideline_snap_get(guideline_pos) is False + # Snap Global + george.tv_guideline_snap_set(0, snap=True, on_global=True) + assert george.tv_guideline_snap_get(0, on_global=True) is True + george.tv_guideline_snap_set(0, snap=False, on_global=True) + assert george.tv_guideline_snap_get(0, on_global=True) is False + + +def test_guideline_image(test_project: george.TVPProject, png_sequence: list[Path]) -> None: + """Test adding and modifying an image guideline.""" + dummy_img = png_sequence[0] + guideline_pos = george.tv_guideline_add_image( + img_path=dummy_img, x=100, y=100, rotation=45, scale=2.0, flip=george.FlipDirection.X + ) + + get_res = george.tv_guideline_modify_image_get(guideline_pos) + assert get_res.x == 100 + assert get_res.y == 100 + assert get_res.rotation == 45 + assert get_res.scale == 2.0 + assert get_res.flip == george.FlipDirection.X + + george.tv_guideline_modify_image_set(guideline_pos, rotation=90, scale=3.0, flip=george.FlipDirection.Y) + + set_res = george.tv_guideline_modify_image_get(guideline_pos) + assert set_res.rotation == 90 + assert set_res.scale == 3.0 + assert set_res.flip == george.FlipDirection.Y + + +def test_guideline_line(test_project: george.TVPProject) -> None: + """Test adding and modifying a line guideline.""" + guideline_pos = george.tv_guideline_add_line(x=50, y=60, angle=15.5) + + get_res = george.tv_guideline_modify_line_get(guideline_pos) + assert get_res.x == 50 + assert get_res.y == 60 + assert get_res.angle == 15.5 + + george.tv_guideline_modify_line_set(guideline_pos, angle=180) + + set_res = george.tv_guideline_modify_line_get(guideline_pos) + assert set_res.angle == 180 + + +def test_guideline_segment(test_project: george.TVPProject) -> None: + """Test adding and modifying a segment guideline.""" + guideline_pos = george.tv_guideline_add_segment(x1=10, y1=10, x2=100, y2=100) + + get_res = george.tv_guideline_modify_segment_get(guideline_pos) + assert get_res.x1 == 10 + assert get_res.y1 == 10 + assert get_res.x2 == 100 + assert get_res.y2 == 100 + + george.tv_guideline_modify_segment_set(guideline_pos, x2=200) + set_res = george.tv_guideline_modify_segment_get(guideline_pos) + assert set_res.x2 == 200 + + +def test_guideline_circle(test_project: george.TVPProject) -> None: + """Test adding and modifying a circle guideline.""" + guideline_pos = george.tv_guideline_add_circle(x=200, y=200, radius=50) + + get_res = george.tv_guideline_modify_circle_get(guideline_pos) + assert get_res.x == 200 + assert get_res.y == 200 + assert get_res.radius == 50 + + george.tv_guideline_modify_circle_set(guideline_pos, radius=75) + set_res = george.tv_guideline_modify_circle_get(guideline_pos) + assert set_res.radius == 75 + + +def test_guideline_ellipse(test_project: george.TVPProject) -> None: + """Test adding and modifying an ellipse guideline.""" + guideline_pos = george.tv_guideline_add_ellipse(x=100, y=100, radius_a=50, radius_b=25) + + get_res = george.tv_guideline_modify_ellipse_get(guideline_pos) + assert get_res.x == 100 + assert get_res.y == 100 + assert get_res.radius_a == 50 + assert get_res.radius_b == 25 + + george.tv_guideline_modify_ellipse_set(guideline_pos, radius_b=30) + set_res = george.tv_guideline_modify_ellipse_get(guideline_pos) + assert set_res.radius_b == 30 + + +def test_guideline_grid(test_project: george.TVPProject) -> None: + """Test adding and modifying a grid guideline.""" + guideline_pos = george.tv_guideline_add_grid(x=0, y=0, width=1920, height=1080) + + get_res = george.tv_guideline_modify_grid_get(guideline_pos) + assert get_res.x == 0 + assert get_res.y == 0 + assert get_res.width == 1920 + assert get_res.height == 1080 + + george.tv_guideline_modify_grid_set(guideline_pos, width=1280, height=720) + + set_res = george.tv_guideline_modify_grid_get(guideline_pos) + assert set_res.width == 1280 + assert set_res.height == 720 + + +def test_guideline_marks(test_project: george.TVPProject) -> None: + """Test adding and modifying a marks guideline.""" + guideline_pos = george.tv_guideline_add_marks(count_x=4, count_y=3) + + get_res = george.tv_guideline_modify_marks_get(guideline_pos) + assert get_res.count_x == 4 + assert get_res.count_y == 3 + + # FIXME This function doesn't seem to work in tvpaint, values are never changed ! + # george.tv_guideline_modify_marks_set(guideline_pos, count_x=5) # noqa: ERA001 + # set_res = george.tv_guideline_modify_marks_get(guideline_pos) # noqa: ERA001 + # assert set_res.count_x == 5 # noqa: ERA001 + + +def test_guideline_safe_area(test_project: george.TVPProject) -> None: + """Test adding and modifying a safe area guideline.""" + guideline_pos = george.tv_guideline_add_safe_area(sf_out=10, sf_in=20) + + get_res = george.tv_guideline_modify_safe_area_get(guideline_pos) + assert get_res.sf_out == 10 + assert get_res.sf_in == 20 + + george.tv_guideline_modify_safe_area_set(guideline_pos, sf_in=25) + set_res = george.tv_guideline_modify_safe_area_get(guideline_pos) + assert set_res.sf_in == 25 + + +def test_guideline_vanish_point1(test_project: george.TVPProject) -> None: + """Test adding and modifying vanish point guidelines (1, 2, and 3).""" + # VP 1 + pos1 = george.tv_guideline_add_vanish_point_1(x=100, y=100, grid=True) + get_res1 = george.tv_guideline_modify_vanish_point_1_get(pos1) + assert get_res1.x == 100 + assert get_res1.y == 100 + assert get_res1.grid is True + + george.tv_guideline_modify_vanish_point_1_set(pos1, x=25, grid=False) + set_res1 = george.tv_guideline_modify_vanish_point_1_get(pos1) + assert set_res1.x == 25 + assert set_res1.grid is False + + +def test_guideline_vanish_point2(test_project: george.TVPProject) -> None: + pos2 = george.tv_guideline_add_vanish_point_2(x1=10, y1=10, x2=100, y2=100) + get_res2 = george.tv_guideline_modify_vanish_point_2_get(pos2) + assert get_res2.x1 == 10 + assert get_res2.y1 == 10 + assert get_res2.x2 == 100 + assert get_res2.y2 == 100 + + george.tv_guideline_modify_vanish_point_2_set(pos2, y1=20, y2=50, ray=5) + set_res2 = george.tv_guideline_modify_vanish_point_2_get(pos2) + assert set_res2.y1 == 20 + assert set_res2.y2 == 50 + assert set_res2.ray == 5 + + +def test_guideline_vanish_point3(test_project: george.TVPProject) -> None: + pos3 = george.tv_guideline_add_vanish_point_3(x1=10, y1=10, x2=100, y2=100, x3=50, y3=50) + get_res3 = george.tv_guideline_modify_vanish_point_3_get(pos3) + assert get_res3.x1 == 10 + assert get_res3.y1 == 10 + assert get_res3.x2 == 100 + assert get_res3.y2 == 100 + assert get_res3.x3 == 50 + assert get_res3.x3 == 50 + + george.tv_guideline_modify_vanish_point_3_set(pos3, y1=20, y2=50, y3=60) + set_res3 = george.tv_guideline_modify_vanish_point_3_get(pos3) + assert set_res3.y1 == 20 + assert set_res3.y2 == 50 + assert set_res3.y3 == 60 + + +def test_guideline_charts(test_project: george.TVPProject) -> None: + """Test adding field and animator charts (these typically lack modify endpoints).""" + pos_field = george.tv_guideline_add_field_chart() + assert pos_field >= 0 + + pos_anim = george.tv_guideline_add_animator_chart() + assert pos_anim >= 0 diff --git a/tests/george/test_grg_layer.py b/tests/george/test_grg_layer.py index dc17c96..02263df 100644 --- a/tests/george/test_grg_layer.py +++ b/tests/george/test_grg_layer.py @@ -1,971 +1,899 @@ -# from __future__ import annotations -# -# from pathlib import Path -# -# import pytest -# -# from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError -# from pytvpaint.george.grg_base import ( -# BlendingMode, -# RGBColor, -# SaveFormat, -# tv_version, -# tv_rect, -# tv_save_mode_get, -# ) -# from pytvpaint.george.grg_clip import TVPClip, tv_clip_current_id, tv_layer_image -# from pytvpaint.george.grg_layer import ( -# InsertDirection, -# InstanceNamingMode, -# InstanceNamingProcess, -# LayerBehavior, -# LayerColorDisplayOpt, -# LayerTransparency, -# StencilMode, -# LayerType, -# TVPLayer, -# tv_instance_get_name, -# tv_instance_name, -# tv_instance_set_name, -# tv_layer_anim, -# tv_layer_auto_break_instance_get, -# tv_layer_auto_break_instance_set, -# tv_layer_auto_create_instance_get, -# tv_layer_auto_create_instance_set, -# tv_layer_blending_mode_get, -# tv_layer_blending_mode_set, -# tv_layer_collapse_get, -# tv_layer_collapse_set, -# tv_layer_color_get, -# tv_layer_color_get_color, -# tv_layer_color_hide, -# tv_layer_color_lock, -# tv_layer_color_select, -# tv_layer_color_set, -# tv_layer_color_set_color, -# tv_layer_color_show, -# tv_layer_color_unlock, -# tv_layer_color_unselect, -# tv_layer_color_visible, -# tv_layer_copy, -# tv_layer_create, -# tv_layer_current_id, -# tv_layer_cut, -# tv_layer_density_get, -# tv_layer_density_set, -# tv_layer_display_get, -# tv_layer_display_set, -# tv_layer_duplicate, -# tv_layer_get_id, -# tv_layer_get_pos, -# tv_layer_info, -# tv_layer_insert_image, -# tv_layer_kill, -# tv_layer_folder_delete, -# tv_layer_load_dependencies, -# tv_layer_lock_get, -# tv_layer_lock_position_get, -# tv_layer_lock_position_set, -# tv_layer_lock_set, -# tv_layer_mark_get, -# tv_layer_mark_set, -# tv_layer_merge, -# tv_layer_merge_all, -# tv_layer_move, -# tv_layer_paste, -# tv_layer_post_behavior_get, -# tv_layer_post_behavior_set, -# tv_layer_pre_behavior_get, -# tv_layer_pre_behavior_set, -# tv_layer_rename, -# tv_layer_select, -# tv_layer_selection_get, -# tv_layer_selection_set, -# tv_layer_set, -# tv_layer_shift, -# tv_layer_show_thumbnails_get, -# tv_layer_show_thumbnails_set, -# tv_layer_stencil_get, -# tv_layer_stencil_set, -# tv_load_image, -# tv_preserve_get, -# tv_preserve_set, -# tv_save_image, -# ) -# from pytvpaint.george.grg_project import TVPProject -# from pytvpaint.layer import Layer -# from tests.conftest import FixtureYield -# -# -# IS_NOT_TVP12 = not tv_version()[1].startswith('12') -# -# -# def test_tv_layer_current_id(test_project: TVPProject) -> None: -# assert tv_layer_current_id() -# -# -# def test_tv_layer_get_id(test_project: TVPProject) -> None: -# assert tv_layer_get_id(0) == tv_layer_current_id() -# -# -# def test_tv_layer_get_id_neg_pos_error(test_project: TVPProject) -> None: -# with pytest.raises(GeorgeError, match="No layer at provided position"): -# tv_layer_get_id(-1) -# -# -# def test_tv_layer_get_pos(test_project: TVPProject) -> None: -# assert tv_layer_get_pos(tv_layer_current_id()) == 0 -# -# -# def test_tv_layer_get_pos_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_get_pos(56) -# -# -# def test_tv_layer_info() -> None: -# info = tv_layer_info(tv_layer_current_id()) -# assert info.id -# -# -# def test_tv_layer_info_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_info(-4) -# -# -# def test_tv_layer_move(test_project: TVPProject) -> None: -# current_layer = tv_layer_current_id() -# total_layers = 10 -# -# for i in range(total_layers): -# tv_layer_create(f"layer_{i}") -# -# # Make the current layer the original one -# tv_layer_set(current_layer) -# -# for i in range(total_layers): -# new_pos = i + 1 -# tv_layer_move(new_pos) -# layer_info = tv_layer_info(current_layer) -# # Position starts at 1 -# assert layer_info.position + 1 == new_pos -# -# -# @pytest.mark.parametrize("pos", [-1, 2, 100, 1000]) -# def test_tv_layer_move_wrong_pos(test_project: TVPProject, pos: int) -> None: -# with pytest.raises( -# GeorgeError, -# match="Couldn't move current layer to position", -# ): -# tv_layer_move(pos) -# -# -# def test_tv_layer_set(test_project: TVPProject) -> None: -# layers = [tv_layer_create(f"layer_{i}") for i in range(5)] -# -# for layer in layers: -# tv_layer_set(layer) -# assert tv_layer_current_id() == layer -# -# -# def test_tv_layer_set_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_set(-16) -# -# -# def test_tv_layer_selection_get(test_layer: TVPLayer) -> None: -# assert not tv_layer_selection_get(test_layer.id) -# -# -# def test_tv_layer_selection_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_selection_get(-1) -# -# -# @pytest.mark.parametrize("selected", [True, False]) -# def test_tv_layer_selection_set(test_project: TVPProject, selected: bool) -> None: -# layers = [tv_layer_create(f"layer_{i}") for i in range(5)] -# -# for layer in layers: -# tv_layer_selection_set(layer, new_state=selected) -# assert tv_layer_info(layer).selected == selected -# -# -# @pytest.mark.parametrize("selected", [True, False]) -# def test_tv_layer_selection_set_wrong_id(selected: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_selection_set(-1, selected) -# -# -# def test_tv_layer_selection_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_selection_set(-16, True) -# -# -# def test_tv_layer_select_n(test_project: TVPProject, test_anim_layer: TVPLayer) -> None: -# end_frame = 10 -# -# # Draw something in order to select frames -# tv_layer_copy() -# tv_layer_image(end_frame) -# tv_layer_paste() -# tv_rect(0, 0, 200, 200) -# -# selected_frames = tv_layer_select(0, end_frame) -# assert selected_frames == end_frame -# -# -# LAYER_NAMES_TO_TEST = ["new_layer", "0", "new layer", ""] -# -# -# @pytest.mark.parametrize("name", LAYER_NAMES_TO_TEST) -# def test_tv_layer_create(test_project: TVPProject, name: str) -> None: -# new_layer = tv_layer_create(name) -# assert tv_layer_info(new_layer).name == name -# -# -# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") -# @pytest.mark.parametrize("name", LAYER_NAMES_TO_TEST) -# def test_tv_layer_folder_create(test_project: TVPProject, name: str) -> None: -# new_layer = tv_layer_create(name, layer_type=0) -# assert tv_layer_info(new_layer).type == LayerType.FOLDER -# assert tv_layer_info(new_layer).name == name -# -# -# @pytest.mark.parametrize("new_name", LAYER_NAMES_TO_TEST) -# def test_tv_layer_duplicate(test_project: TVPProject, new_name: str) -> None: -# dup_layer_id = tv_layer_duplicate(new_name) -# assert tv_layer_current_id() == dup_layer_id -# assert tv_layer_info(tv_layer_current_id()).name == new_name -# -# -# @pytest.mark.parametrize("new_name", LAYER_NAMES_TO_TEST) -# def test_tv_layer_rename(test_layer: TVPLayer, new_name: str) -> None: -# tv_layer_rename(tv_layer_current_id(), new_name) -# -# -# def test_tv_layer_rename_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_rename(-1, "test") -# -# -# def test_tv_layer_kill(test_project: TVPProject) -> None: -# new_layer = tv_layer_create("destroy") -# tv_layer_kill(new_layer) -# -# # Layer shouldn't exist anymore -# with pytest.raises(NoObjectWithIdError): -# tv_layer_get_pos(new_layer) -# -# -# def test_tv_layer_kill_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_kill(-5) -# -# -# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") -# @pytest.mark.parametrize("remove_children", (True, False)) -# def test_tv_layer_folder_delete(test_project: TVPProject, remove_children) -> None: -# new_layer = tv_layer_create("destroy", layer_type=0) -# tv_layer_folder_delete(new_layer, remove_children=remove_children) -# -# # Layer shouldn't exist anymore -# with pytest.raises(NoObjectWithIdError): -# tv_layer_get_pos(new_layer) -# -# -# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") -# def test_tv_layer_folder_delete_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_folder_delete(-5, True) -# -# -# def test_tv_layer_density_get() -> None: -# assert 0 <= tv_layer_density_get() <= 100 -# -# -# @pytest.mark.parametrize("density", [0, 50, 100, 25, 150, -50]) -# def test_tv_layer_density_set(density: int) -> None: -# tv_layer_density_set(density) -# assert tv_layer_density_get() == min(max(0, density), 100) -# -# -# def test_tv_layer_display_get(test_layer: TVPLayer) -> None: -# current_layer = tv_layer_info(tv_layer_current_id()) -# assert current_layer.visibility == tv_layer_display_get(current_layer.id) -# -# -# def test_tv_layer_display_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_display_get(-1) -# -# -# @pytest.mark.parametrize("new_state", [True, False]) -# def test_tv_layer_display_set(test_layer: TVPLayer, new_state: bool) -> None: -# current_layer = tv_layer_info(tv_layer_current_id()) -# tv_layer_display_set(current_layer.id, new_state) -# assert tv_layer_info(current_layer.id).visibility == new_state -# -# -# def test_tv_layer_lock_get(test_layer: TVPLayer) -> None: -# tv_layer_lock_get(tv_layer_current_id()) -# -# -# def test_tv_layer_lock_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_lock_get(-1) -# -# -# @pytest.mark.parametrize("lock", [True, False]) -# def test_tv_layer_lock_set(test_layer: TVPLayer, lock: bool) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_lock_set(current_layer, lock) -# assert tv_layer_lock_get(current_layer) == lock -# -# -# @pytest.mark.parametrize("lock", [True, False]) -# def test_tv_layer_lock_set_wrong_id(lock: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_lock_set(-1, lock) -# -# -# def test_tv_layer_collapse_get() -> None: -# tv_layer_collapse_get(tv_layer_current_id()) -# -# -# def test_tv_layer_collapse_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_collapse_get(-1) -# -# -# @pytest.mark.parametrize("collapse", [True, False]) -# def test_tv_layer_collapse_set(test_layer: TVPLayer, collapse: bool) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_collapse_set(current_layer, collapse) -# assert tv_layer_collapse_get(current_layer) == collapse -# -# -# @pytest.mark.parametrize("collapse", [True, False]) -# def test_tv_layer_collapse_set_wrong_id(collapse: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_collapse_set(-1, collapse) -# -# -# def test_tv_layer_blending_mode_get() -> None: -# assert tv_layer_blending_mode_get(tv_layer_current_id()) in list(BlendingMode) -# -# -# def test_tv_layer_blending_mode_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_blending_mode_get(-1) -# -# -# @pytest.mark.parametrize("mode", BlendingMode) -# def test_tv_layer_blending_mode_set(test_layer: TVPLayer, mode: BlendingMode) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_blending_mode_set(current_layer, mode) -# assert tv_layer_blending_mode_get(current_layer) == mode -# -# -# def test_tv_layer_blending_mode_set_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_blending_mode_set(-1, BlendingMode.ADD) -# -# -# def test_tv_layer_stencil_get() -> None: -# tv_layer_stencil_get(tv_layer_current_id()) -# -# -# def test_tv_layer_stencil_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_stencil_get(-1) -# -# -# @pytest.mark.parametrize("mode", StencilMode) -# def test_tv_layer_stencil_set(test_layer: TVPLayer, mode: StencilMode) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_stencil_set(current_layer, mode) -# -# current_mode = tv_layer_stencil_get(current_layer) -# -# if mode == StencilMode.ON: -# assert current_mode == StencilMode.NORMAL -# else: -# assert current_mode == mode -# -# -# @pytest.mark.parametrize("mode", StencilMode) -# def test_tv_layer_stencil_set_wrong_id(mode: StencilMode) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_stencil_set(-1, mode) -# -# -# def test_tv_layer_show_thumbnails_get() -> None: -# tv_layer_show_thumbnails_get(tv_layer_current_id()) -# -# -# def test_tv_layer_show_thumbnails_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_show_thumbnails_get(-1) -# -# -# @pytest.mark.parametrize("state", [True, False]) -# def test_tv_layer_show_thumbnails_set(test_layer: TVPLayer, state: bool) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_show_thumbnails_set(current_layer, state) -# assert tv_layer_show_thumbnails_get(current_layer) == state -# -# -# @pytest.mark.parametrize("state", [True, False]) -# def test_tv_layer_show_thumbnails_set_wrong_id(state: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_show_thumbnails_set(-1, state) -# -# -# def test_tv_layer_auto_break_instance_get() -> None: -# tv_layer_auto_break_instance_get(tv_layer_current_id()) -# -# -# def test_tv_layer_auto_break_instance_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_auto_break_instance_get(-1) -# -# -# @pytest.mark.parametrize("state", [True, False]) -# def test_tv_layer_auto_break_instance_set( -# test_project: TVPProject, state: bool -# ) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_auto_break_instance_set(current_layer, state) -# assert tv_layer_auto_break_instance_get(current_layer) == state -# -# -# @pytest.mark.parametrize("state", [True, False]) -# def test_tv_layer_auto_break_instance_set_wrong_id(state: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_auto_break_instance_set(-1, state) -# -# -# def test_tv_layer_auto_create_instance_get() -> None: -# tv_layer_auto_create_instance_get(tv_layer_current_id()) -# -# -# def test_tv_layer_auto_create_instance_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_auto_create_instance_get(-1) -# -# -# @pytest.mark.parametrize("state", [True, False]) -# def test_tv_layer_auto_create_instance_set(test_layer: TVPLayer, state: bool) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_auto_create_instance_set(current_layer, state) -# assert tv_layer_auto_create_instance_get(current_layer) == state -# -# -# @pytest.mark.parametrize("state", [True, False]) -# def test_tv_layer_auto_create_instance_set_wrong_id(state: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_auto_create_instance_set(-1, state) -# -# -# def test_tv_layer_pre_behavior_get() -> None: -# tv_layer_pre_behavior_get(tv_layer_current_id()) -# -# -# def test_tv_layer_pre_behavior_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_pre_behavior_get(-1) -# -# -# @pytest.mark.parametrize("behavior", LayerBehavior) -# def test_tv_layer_pre_behavior_set( -# test_layer: TVPLayer, behavior: LayerBehavior -# ) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_pre_behavior_set(current_layer, behavior) -# assert tv_layer_pre_behavior_get(current_layer) == behavior -# -# -# @pytest.mark.parametrize("behavior", LayerBehavior) -# def test_tv_layer_pre_behavior_set_wrong_id(behavior: LayerBehavior) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_pre_behavior_set(-1, behavior) -# -# -# def test_tv_layer_post_behavior_get() -> None: -# tv_layer_post_behavior_get(tv_layer_current_id()) -# -# -# def test_tv_layer_post_behavior_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_post_behavior_get(-1) -# -# -# @pytest.mark.parametrize("behavior", LayerBehavior) -# def test_tv_layer_post_behavior_set( -# test_layer: TVPLayer, behavior: LayerBehavior -# ) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_post_behavior_set(current_layer, behavior) -# assert tv_layer_post_behavior_get(current_layer) == behavior -# -# -# @pytest.mark.parametrize("behavior", LayerBehavior) -# def test_tv_layer_post_behavior_set_wrong_id(behavior: LayerBehavior) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_post_behavior_set(-1, behavior) -# -# -# def test_tv_layer_lock_position_get() -> None: -# tv_layer_lock_position_get(tv_layer_current_id()) -# -# -# def test_tv_layer_lock_position_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_lock_position_get(-1) -# -# -# @pytest.mark.parametrize("state", [True, False]) -# def test_tv_layer_lock_position_set(test_layer: TVPLayer, state: bool) -> None: -# current_layer = tv_layer_current_id() -# tv_layer_lock_position_set(current_layer, state) -# assert tv_layer_lock_position_get(current_layer) == state -# -# -# @pytest.mark.parametrize("state", [True, False]) -# def test_tv_layer_lock_position_set_wrong_id(state: bool) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_lock_position_set(-1, state) -# -# -# def test_tv_preserve_get() -> None: -# tv_preserve_get() -# -# -# @pytest.mark.parametrize("state", LayerTransparency) -# def test_tv_preserve_set(test_layer: TVPLayer, state: LayerTransparency) -> None: -# tv_preserve_set(state) -# new_state = tv_preserve_get() -# -# transparency_map = { -# LayerTransparency.MINUS_1: LayerTransparency.ON, -# LayerTransparency.NONE: LayerTransparency.OFF, -# } -# -# assert transparency_map.get(state, state) == new_state -# -# -# def test_tv_layer_mark_get() -> None: -# tv_layer_mark_get(tv_layer_current_id(), 0) -# -# -# def test_tv_layer_mark_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_mark_get(-1, 0) -# -# -# @pytest.mark.parametrize("mark", range(27)) -# def test_tv_layer_mark_set(test_anim_layer: TVPLayer, mark: int) -> None: -# tv_layer_mark_set(test_anim_layer.id, 0, mark) -# assert tv_layer_mark_get(test_anim_layer.id, 0) == mark -# -# -# def test_tv_layer_mark_set_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_mark_set(-1, 0, 0) -# -# -# def test_tv_layer_anim(test_layer: TVPLayer) -> None: -# tv_layer_anim(test_layer.id) -# -# assert tv_layer_auto_break_instance_get(test_layer.id) -# assert tv_layer_auto_create_instance_get(test_layer.id) -# assert not tv_layer_lock_get(test_layer.id) -# -# -# def test_tv_layer_load_dependencies() -> None: -# tv_layer_load_dependencies(tv_layer_current_id()) -# -# -# @pytest.mark.parametrize("color_index", range(1, 27)) -# def test_tv_layer_color_get_color(color_index: int) -> None: -# current_clip = tv_clip_current_id() -# color = tv_layer_color_get_color(current_clip, color_index) -# assert color.clip_id == current_clip -# assert color.color_index == color_index -# -# -# def test_tv_layer_color_get_color_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_color_get_color(-1, 0) -# -# -# # We skip index 0 because it's the "Default" color and can't be changed -# @pytest.mark.parametrize("color_index", range(1, 27)) -# @pytest.mark.parametrize("name", [None, "test"]) -# @pytest.mark.parametrize("rgb", [RGBColor(255, 0, 0), RGBColor(0, 255, 0)]) -# def test_tv_layer_color_set_color( -# test_clip: TVPClip, color_index: int, name: str | None, rgb: RGBColor -# ) -> None: -# tv_layer_color_set_color(test_clip.id, color_index, rgb, name) -# -# color = tv_layer_color_get_color(test_clip.id, color_index) -# -# assert color.color_index == color_index -# assert color.color_r == rgb.r -# assert color.color_g == rgb.g -# assert color.color_b == rgb.b -# assert color.clip_id == test_clip.id -# -# if name is not None: -# assert color.name == name -# -# -# def test_tv_layer_color_set_color_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_color_set_color(-1, 1, RGBColor(0, 0, 0)) -# -# -# def test_tv_layer_color_get(test_layer: TVPLayer) -> None: -# index = tv_layer_color_get(test_layer.id) -# assert 0 <= index <= 26 -# -# -# def test_tv_layer_color_get_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_layer_color_get(-1) -# -# -# @pytest.mark.parametrize("color_index", range(27)) -# def test_tv_layer_color_set_s(test_layer: TVPLayer, color_index: int) -> None: -# tv_layer_color_set(test_layer.id, color_index) -# assert tv_layer_color_get(test_layer.id) == color_index -# -# -# @pytest.fixture -# def layers_with_colors(test_project: TVPProject) -> FixtureYield[tuple[int, list[int]]]: -# """ -# Fixture that create some layers with a color -# """ -# color_index = 5 -# layers = [tv_layer_create(f"layer_{i}") for i in range(10)] -# color_layers = [layer for i, layer in enumerate(layers) if i % 2 == 0] -# -# # Set the color of all layers -# for layer in color_layers: -# tv_layer_color_set(layer, color_index) -# -# yield color_index, color_layers -# -# -# def test_tv_layer_color_lock(layers_with_colors: tuple[int, list[int]]) -> None: -# color_index, layers_to_lock = layers_with_colors -# assert tv_layer_color_lock(color_index) == len(layers_to_lock) -# assert all(tv_layer_lock_get(layer) for layer in layers_to_lock) -# -# -# def test_tv_layer_color_unlock(layers_with_colors: tuple[int, list[int]]) -> None: -# color_index, layers_to_lock = layers_with_colors -# -# for layer in layers_to_lock: -# tv_layer_lock_set(layer, True) -# -# assert tv_layer_color_unlock(color_index) == len(layers_to_lock) -# assert all(not tv_layer_lock_get(layer) for layer in layers_to_lock) -# -# -# @pytest.mark.parametrize("display", LayerColorDisplayOpt) -# def test_tv_layer_color_show( -# layers_with_colors: tuple[int, list[int]], display: LayerColorDisplayOpt -# ) -> None: -# color_index, layers_to_show = layers_with_colors -# -# is_display = display == LayerColorDisplayOpt.DISPLAY -# -# for layer in layers_to_show: -# if is_display: -# tv_layer_display_set(layer, False) -# else: -# tv_layer_collapse_set(layer, True) -# -# tv_layer_color_show(display, color_index) -# -# check_fn = tv_layer_display_get if is_display else tv_layer_collapse_get -# -# assert all(check_fn(layer) for layer in layers_to_show) -# -# -# @pytest.mark.parametrize("display", LayerColorDisplayOpt) -# def test_tv_layer_color_hide( -# layers_with_colors: tuple[int, list[int]], display: LayerColorDisplayOpt -# ) -> None: -# color_index, layers_to_hide = layers_with_colors -# -# tv_layer_color_hide(display, color_index) -# -# check_fn = ( -# tv_layer_display_get -# if display == LayerColorDisplayOpt.DISPLAY -# else tv_layer_collapse_get -# ) -# -# assert all(not check_fn(layer) for layer in layers_to_hide) -# -# -# @pytest.mark.parametrize("color_index", range(27)) -# def test_tv_layer_color_visible(color_index: int) -> None: -# # It seems that there's no way to set the visibility of a layer color group -# # So we only test that all groups are visible -# assert tv_layer_color_visible(color_index) -# -# -# def test_tv_layer_color_select(layers_with_colors: tuple[int, list[int]]) -> None: -# color_index, layers_to_select = layers_with_colors -# -# tv_layer_color_select(color_index) -# assert all(tv_layer_selection_get(layer) for layer in layers_to_select) -# -# -# def test_tv_layer_color_unselect(layers_with_colors: tuple[int, list[int]]) -> None: -# color_index, layers_to_select = layers_with_colors -# -# for layer in layers_to_select: -# tv_layer_selection_set(layer, True) -# -# tv_layer_color_unselect(color_index) -# assert all(not tv_layer_selection_get(layer) for layer in layers_to_select) -# -# -# def can_be_parsed_as_int(value: str) -> bool: -# if " " in value: -# return False -# -# try: -# int(value) -# except ValueError: -# return False -# -# 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"]) -# @pytest.mark.parametrize("process", [None, *InstanceNamingProcess]) -# @pytest.mark.parametrize("initial_name", ["", "5", " 7", "fest", "test_3"]) -# def test_tv_instance_name( -# test_layer: TVPLayer, -# mode: InstanceNamingMode, -# prefix: str | None, -# suffix: str | None, -# process: InstanceNamingProcess | None, -# initial_name: str, -# ) -> None: -# instance = 0 -# -# # Assign an initial name to the instance -# tv_instance_set_name(test_layer.id, instance, name=initial_name) -# -# # Rename all instances -# tv_instance_name(test_layer.id, mode, prefix, suffix, process) -# -# if mode == InstanceNamingMode.ALL: -# expected_name = " " + str(instance + 1) -# if prefix: -# expected_name = prefix + expected_name -# else: # SMART mode -# no_prefix = prefix is None -# no_suffix = suffix is None -# -# cond_text = ( -# len(initial_name) -# and process == InstanceNamingProcess.TEXT -# and (can_be_parsed_as_int(initial_name)) -# ) -# -# cond_empty = ( -# len(initial_name) -# and process == InstanceNamingProcess.EMPTY -# and (no_prefix != no_suffix and not can_be_parsed_as_int(initial_name)) -# ) -# -# cond_number = ( -# len(initial_name) -# and process == InstanceNamingProcess.NUMBER -# and not (no_prefix == no_suffix and not can_be_parsed_as_int(initial_name)) -# and not can_be_parsed_as_int(initial_name) -# ) -# -# if cond_text or cond_empty or cond_number: -# expected_name = initial_name -# else: -# expected_name = str(instance + 1) -# -# if suffix: -# expected_name += suffix -# -# if prefix: -# expected_name = prefix + expected_name -# -# assert expected_name == tv_instance_get_name(test_layer.id, instance) -# -# -# @pytest.mark.skip("Crashed TVPaint") -# @pytest.mark.parametrize("mode", InstanceNamingMode) -# def test_tv_instance_name_wrong_id(mode: InstanceNamingMode) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_instance_name(-1, mode) -# -# -# def test_tv_instance_get_name(test_layer: TVPLayer) -> None: -# # By default there's an instance at frame zero -# tv_instance_get_name(test_layer.id, 0) -# -# -# def test_tv_instance_get_name_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_instance_get_name(-1, 0) -# -# -# @pytest.mark.parametrize("name", ["", "5", " 7", "fest", "test_3"]) -# def test_tv_instance_set_name(test_layer: TVPLayer, name: str) -> None: -# tv_instance_set_name(test_layer.id, 0, name) -# assert tv_instance_get_name(test_layer.id, 0) == name -# -# -# def test_tv_instance_set_name_wrong_id() -> None: -# with pytest.raises(GeorgeError): -# tv_instance_set_name(-1, 0, "test") -# -# -# def instance_exists(frame: int) -> bool: -# try: -# tv_instance_get_name(tv_layer_current_id(), frame) -# except NoObjectWithIdError: -# return False -# return True -# -# -# @pytest.mark.parametrize("frame", range(5)) -# def test_tv_layer_copy_paste(test_anim_layer: TVPLayer, frame: int) -> None: -# tv_layer_image(0) -# tv_layer_copy() -# tv_layer_image(frame) -# tv_layer_paste() -# -# assert instance_exists(frame) -# -# -# @pytest.mark.parametrize("frame", range(1, 6)) -# def test_tv_layer_cut_paste(test_anim_layer: TVPLayer, frame: int) -> None: -# cut_frame = 10 -# -# # Add another instance because if we cut the first it deletes the layer -# tv_layer_copy() -# tv_layer_image(cut_frame) -# tv_layer_paste() -# -# # Cut that instance -# tv_layer_image(cut_frame) -# tv_layer_cut() -# -# # The instance should be removed -# assert not instance_exists(cut_frame) -# -# # Paste at another frame -# tv_layer_image(frame) -# tv_layer_paste() -# -# assert instance_exists(frame) -# -# -# def test_tv_layer_insert_image_duplicate(test_anim_layer: TVPLayer) -> None: -# initial_frame = 5 -# -# # Copy the instance in the middle -# tv_layer_image(0) -# tv_layer_copy() -# tv_layer_image(initial_frame) -# tv_layer_paste() -# -# tv_layer_insert_image(duplicate=True) -# assert instance_exists(initial_frame + 1) -# -# -# @pytest.mark.parametrize("count", range(1, 8)) -# @pytest.mark.parametrize("direction", InsertDirection) -# def test_tv_layer_insert_image( -# test_anim_layer: TVPLayer, count: int, direction: InsertDirection -# ) -> None: -# initial_frame = 10 -# -# # Copy the instance in the middle -# tv_layer_image(0) -# tv_layer_copy() -# tv_layer_image(initial_frame) -# tv_layer_paste() -# -# tv_layer_insert_image(count, direction) -# -# offset = 0 -# while offset < count: -# if direction == InsertDirection.AFTER: -# next_frame = initial_frame + offset -# else: -# next_frame = initial_frame - offset -# -# assert instance_exists(next_frame) -# offset += 1 -# -# -# @pytest.mark.parametrize("start", [0, 5, 10, 100]) -# def test_tv_layer_shift(test_layer: TVPLayer, start: int) -> None: -# tv_layer_shift(test_layer.id, start) -# -# -# @pytest.mark.parametrize("blending", BlendingMode) -# @pytest.mark.parametrize("stamp", [True, False]) -# def test_tv_layer_merge( -# create_some_layers: list[Layer], blending: BlendingMode, stamp: bool -# ) -> None: -# tv_layer_merge(create_some_layers[0].id, blending, stamp) -# -# -# @pytest.mark.parametrize("keep_color_grp", [False, True]) -# @pytest.mark.parametrize("keep_img_mark", [False, True]) -# @pytest.mark.parametrize("keep_instance_name", [False, True]) -# def test_tv_layer_merge_all( -# create_some_layers: list[Layer], -# keep_color_grp: bool, -# keep_img_mark: bool, -# keep_instance_name: bool, -# ) -> None: -# tv_layer_merge_all(keep_color_grp, keep_img_mark, keep_instance_name) -# -# -# def test_tv_save_image(tmp_path: Path) -> None: -# save_ext, _ = tv_save_mode_get() -# ext = "jpg" if save_ext == SaveFormat.JPG else save_ext.value -# out_img = (tmp_path / "out").with_suffix("." + ext) -# tv_save_image(out_img) -# assert out_img.exists() -# -# -# @pytest.mark.parametrize("stretch", [False, True]) -# def test_tv_load_image( -# test_clip: TVPClip, -# test_layer: TVPLayer, -# ppm_sequence: list[Path], -# stretch: bool, -# ) -> None: -# tv_load_image(ppm_sequence[0], stretch) -# # Verify that there's an instance frame -# assert tv_instance_get_name(test_layer.id, 0) == "" -# -# -# @pytest.mark.skip("Will block the UI") -# def test_tv_load_image_file_does_not_exist(tmp_path: Path) -> None: -# with pytest.raises(FileNotFoundError): -# tv_load_image(tmp_path / "file.png") +from __future__ import annotations + +from pathlib import Path + +import pytest + +from pytvpaint import george +from pytvpaint.layer import Layer +from tests.conftest import FixtureYield + +IS_NOT_TVP12 = not george.tv_version()[1].startswith("12") + + +def _first_layer_id() -> int: + # fix for tvpaint 12 having the first/current layer in a new project always be the new camera layer + first_layer_id = george.tv_layer_get_id(0) + if george.tv_layer_info(first_layer_id).type == george.LayerType.CAMERA: + first_layer_id = george.tv_layer_get_id(1) + + return first_layer_id + + +def test_tv_layer_current_id(test_project: george.TVPProject) -> None: + assert ( + george.tv_layer_current_id() + and george.tv_layer_info(george.tv_layer_current_id()).type != george.LayerType.CAMERA + ) + + +def test_tv_layer_get_id(test_project: george.TVPProject) -> None: + assert _first_layer_id() == george.tv_layer_current_id() + + +def test_tv_layer_get_id_neg_pos_error(test_project: george.TVPProject) -> None: + with pytest.raises(george.GeorgeError, match="No layer at provided position"): + george.tv_layer_get_id(-1) + + +def test_tv_layer_get_pos(test_project: george.TVPProject) -> None: + assert george.tv_layer_get_pos(george.tv_layer_current_id()) == george.tv_layer_get_pos(_first_layer_id()) + + +def test_tv_layer_get_pos_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_get_pos(56) + + +def test_tv_layer_info() -> None: + info = george.tv_layer_info(george.tv_layer_current_id()) + assert info.id + + +def test_tv_layer_info_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_info(-4) + + +def test_tv_layer_move(test_project: george.TVPProject) -> None: + current_layer = george.tv_layer_current_id() + total_layers = 10 + + for i in range(total_layers): + george.tv_layer_create(f"layer_{i}") + + # Make the current layer the original one + george.tv_layer_set(current_layer) + current_pos = george.tv_layer_get_pos(current_layer) + + for i in range(total_layers): + new_pos = current_pos + i + 1 + george.tv_layer_move(new_pos) + layer_info = george.tv_layer_info(current_layer) + assert (layer_info.position + 1) == new_pos + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_layer_move` no longer returns -1 when given a bad position.") +@pytest.mark.parametrize("pos", [-1, 3, 100, 1000]) +def test_tv_layer_move_wrong_pos(test_project: george.TVPProject, pos: int) -> None: + with pytest.raises( + george.GeorgeError, + match="Couldn't move current layer to position", + ): + george.tv_layer_move(pos) + + +def test_tv_layer_set(test_project: george.TVPProject) -> None: + layers = [george.tv_layer_create(f"layer_{i}") for i in range(5)] + + for layer in layers: + george.tv_layer_set(layer) + assert george.tv_layer_current_id() == layer + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_layer_set` no longer returns -1 when given a bad layer id.") +def test_tv_layer_set_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_set(-16) + + +def test_tv_layer_selection_get(test_layer: george.TVPLayer) -> None: + assert not george.tv_layer_selection_get(test_layer.id) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerSelection` no longer returns -1 when given a bad layer id.") +def test_tv_layer_selection_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_selection_get(-1) + + +@pytest.mark.parametrize("selected", [True, False]) +def test_tv_layer_selection_set(test_project: george.TVPProject, selected: bool) -> None: + layers = [george.tv_layer_create(f"layer_{i}") for i in range(5)] + + for layer in layers: + george.tv_layer_selection_set(layer, new_state=selected) + assert george.tv_layer_info(layer).selected == selected + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerSelection` no longer returns -1 when given a bad layer id.") +@pytest.mark.parametrize("selected", [True, False]) +def test_tv_layer_selection_set_wrong_id(selected: bool) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_selection_set(-1, selected) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerSelection` no longer returns -1 when given a bad layer id.") +def test_tv_layer_selection_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_selection_set(-16, True) + + +def test_tv_layer_select_n(test_project: george.TVPProject, test_anim_layer: george.TVPLayer) -> None: + end_frame = 10 + + # Draw something in order to select frames + george.tv_layer_copy() + george.tv_layer_image(end_frame) + george.tv_layer_paste() + george.tv_rect(0, 0, 200, 200) + + selected_frames = george.tv_layer_select(0, end_frame) + assert selected_frames == end_frame + + +LAYER_NAMES_TO_TEST = ["new_layer", "0", "new layer", ""] +if not IS_NOT_TVP12: + LAYER_NAMES_TO_TEST = LAYER_NAMES_TO_TEST[:-1] + + +@pytest.mark.parametrize("name", LAYER_NAMES_TO_TEST) +def test_tv_layer_create(test_project: george.TVPProject, name: str) -> None: + new_layer = george.tv_layer_create(name) + assert george.tv_layer_info(new_layer).name == name + + +@pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +@pytest.mark.parametrize("name", LAYER_NAMES_TO_TEST) +def test_tv_layer_folder_create(test_project: george.TVPProject, name: str) -> None: + new_layer = george.tv_layer_create(name, layer_type=0) + assert george.tv_layer_info(new_layer).type == george.LayerType.FOLDER + assert george.tv_layer_info(new_layer).name == name + + +@pytest.mark.parametrize("new_name", LAYER_NAMES_TO_TEST) +def test_tv_layer_duplicate(test_project: george.TVPProject, new_name: str) -> None: + dup_layer_id = george.tv_layer_duplicate(new_name) + assert george.tv_layer_current_id() == dup_layer_id + assert george.tv_layer_info(george.tv_layer_current_id()).name == new_name + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="This function no longer works in TVP12") +@pytest.mark.parametrize("new_name", LAYER_NAMES_TO_TEST) +def test_tv_layer_rename(test_layer: george.TVPLayer, new_name: str) -> None: + cur_layer_id = george.tv_layer_current_id() + george.tv_layer_rename(cur_layer_id, new_name) + assert george.tv_layer_info(cur_layer_id).name == new_name + + +def test_tv_layer_rename_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_rename(-1, "test") + + +def test_tv_layer_kill(test_project: george.TVPProject) -> None: + new_layer = george.tv_layer_create("destroy") + george.tv_layer_kill(new_layer) + + # Layer shouldn't exist anymore + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_get_pos(new_layer) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_layer_kill` no longer returns -1 when given a bad folder id.") +def test_tv_layer_kill_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_kill(-5) + + +@pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +@pytest.mark.parametrize("remove_children", (True, False)) +def test_tv_layer_folder_delete(test_project: george.TVPProject, remove_children: bool) -> None: + new_layer = george.tv_layer_create("destroy", layer_type=0) + george.tv_layer_folder_delete(new_layer, remove_children=remove_children) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_layer_folder_delete` does not return -1 when given a bad folder id.") +@pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +def test_tv_layer_folder_delete_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_folder_delete(-5, True) + + +def test_tv_layer_density_get() -> None: + assert 0 <= george.tv_layer_density_get() <= 100 + + +@pytest.mark.parametrize("density", [0, 50, 100, 25, 150, -50]) +def test_tv_layer_density_set(density: int) -> None: + george.tv_layer_density_set(density) + assert george.tv_layer_density_get() == min(max(0, density), 100) + + +def test_tv_layer_display_get(test_layer: george.TVPLayer) -> None: + current_layer = george.tv_layer_info(george.tv_layer_current_id()) + assert current_layer.visibility == george.tv_layer_display_get(current_layer.id) + + +def test_tv_layer_display_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_display_get(-1) + + +@pytest.mark.parametrize("new_state", [True, False]) +def test_tv_layer_display_set(test_layer: george.TVPLayer, new_state: bool) -> None: + current_layer = george.tv_layer_info(george.tv_layer_current_id()) + george.tv_layer_display_set(current_layer.id, new_state) + assert george.tv_layer_info(current_layer.id).visibility == new_state + + +def test_tv_layer_lock_get(test_layer: george.TVPLayer) -> None: + george.tv_layer_lock_get(george.tv_layer_current_id()) + + +def test_tv_layer_lock_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_lock_get(-1) + + +@pytest.mark.parametrize("lock", [True, False]) +def test_tv_layer_lock_set(test_layer: george.TVPLayer, lock: bool) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_lock_set(current_layer, lock) + assert george.tv_layer_lock_get(current_layer) == lock + + +@pytest.mark.parametrize("lock", [True, False]) +def test_tv_layer_lock_set_wrong_id(lock: bool) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_lock_set(-1, lock) + + +def test_tv_layer_collapse_get() -> None: + george.tv_layer_collapse_get(george.tv_layer_current_id()) + + +def test_tv_layer_collapse_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_collapse_get(-1) + + +@pytest.mark.parametrize("collapse", [True, False]) +def test_tv_layer_collapse_set(test_layer: george.TVPLayer, collapse: bool) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_collapse_set(current_layer, collapse) + assert george.tv_layer_collapse_get(current_layer) == collapse + + +@pytest.mark.parametrize("collapse", [True, False]) +def test_tv_layer_collapse_set_wrong_id(collapse: bool) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_collapse_set(-1, collapse) + + +def test_tv_layer_blending_mode_get() -> None: + assert george.tv_layer_blending_mode_get(george.tv_layer_current_id()) in list(george.BlendingMode) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerBlendingMode` does not return -1 when given a bad layer id.") +def test_tv_layer_blending_mode_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_blending_mode_get(-1) + + +@pytest.mark.parametrize("mode", george.BlendingMode) +def test_tv_layer_blending_mode_set(test_layer: george.TVPLayer, mode: george.BlendingMode) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_blending_mode_set(current_layer, mode) + assert george.tv_layer_blending_mode_get(current_layer) == mode + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerBlendingMode` does not return -1 when given a bad layer id.") +def test_tv_layer_blending_mode_set_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_blending_mode_set(-1, george.BlendingMode.ADD) + + +def test_tv_layer_stencil_get() -> None: + george.tv_layer_stencil_get(george.tv_layer_current_id()) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerStencil` does not return -1 when given a bad layer id.") +def test_tv_layer_stencil_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_stencil_get(-1) + + +@pytest.mark.parametrize("mode", george.StencilMode) +def test_tv_layer_stencil_set(test_layer: george.TVPLayer, mode: george.StencilMode) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_stencil_set(current_layer, mode) + + current_mode = george.tv_layer_stencil_get(current_layer) + + if mode == george.StencilMode.ON: + assert current_mode == george.StencilMode.NORMAL + else: + assert current_mode == mode + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerStencil` does not return -1 when given a bad layer id.") +@pytest.mark.parametrize("mode", george.StencilMode) +def test_tv_layer_stencil_set_wrong_id(mode: george.StencilMode) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_stencil_set(-1, mode) + + +def test_tv_layer_show_thumbnails_get() -> None: + george.tv_layer_show_thumbnails_get(george.tv_layer_current_id()) + + +def test_tv_layer_show_thumbnails_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_show_thumbnails_get(-1) + + +@pytest.mark.parametrize("state", [True, False]) +def test_tv_layer_show_thumbnails_set(test_layer: george.TVPLayer, state: bool) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_show_thumbnails_set(current_layer, state) + assert george.tv_layer_show_thumbnails_get(current_layer) == state + + +@pytest.mark.parametrize("state", [True, False]) +def test_tv_layer_show_thumbnails_set_wrong_id(state: bool) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_show_thumbnails_set(-1, state) + + +def test_tv_layer_auto_break_instance_get() -> None: + george.tv_layer_auto_break_instance_get(george.tv_layer_current_id()) + + +def test_tv_layer_auto_break_instance_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_auto_break_instance_get(-1) + + +@pytest.mark.parametrize("state", [True, False]) +def test_tv_layer_auto_break_instance_set(test_project: george.TVPProject, state: bool) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_auto_break_instance_set(current_layer, state) + assert george.tv_layer_auto_break_instance_get(current_layer) == state + + +@pytest.mark.parametrize("state", [True, False]) +def test_tv_layer_auto_break_instance_set_wrong_id(state: bool) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_auto_break_instance_set(-1, state) + + +def test_tv_layer_auto_create_instance_get() -> None: + george.tv_layer_auto_create_instance_get(george.tv_layer_current_id()) + + +def test_tv_layer_auto_create_instance_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_auto_create_instance_get(-1) + + +@pytest.mark.parametrize("state", [True, False]) +def test_tv_layer_auto_create_instance_set(test_layer: george.TVPLayer, state: bool) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_auto_create_instance_set(current_layer, state) + assert george.tv_layer_auto_create_instance_get(current_layer) == state + + +@pytest.mark.parametrize("state", [True, False]) +def test_tv_layer_auto_create_instance_set_wrong_id(state: bool) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_auto_create_instance_set(-1, state) + + +def test_tv_layer_pre_behavior_get() -> None: + george.tv_layer_pre_behavior_get(george.tv_layer_current_id()) + + +def test_tv_layer_pre_behavior_get_wrong_id() -> None: + with pytest.raises((george.NoObjectWithIdError, ValueError)): + george.tv_layer_pre_behavior_get(-1) + + +@pytest.mark.parametrize("behavior", george.LayerBehavior) +def test_tv_layer_pre_behavior_set(test_layer: george.TVPLayer, behavior: george.LayerBehavior) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_pre_behavior_set(current_layer, behavior) + assert george.tv_layer_pre_behavior_get(current_layer) == behavior + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerPreBehavior` does not return -1 when given a bad layer id.") +@pytest.mark.parametrize("behavior", george.LayerBehavior) +def test_tv_layer_pre_behavior_set_wrong_id(behavior: george.LayerBehavior) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_pre_behavior_set(-1, behavior) + + +def test_tv_layer_post_behavior_get() -> None: + george.tv_layer_post_behavior_get(george.tv_layer_current_id()) + + +def test_tv_layer_post_behavior_get_wrong_id() -> None: + with pytest.raises((george.NoObjectWithIdError, ValueError)): + george.tv_layer_post_behavior_get(-1) + + +@pytest.mark.parametrize("behavior", george.LayerBehavior) +def test_tv_layer_post_behavior_set(test_layer: george.TVPLayer, behavior: george.LayerBehavior) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_post_behavior_set(current_layer, behavior) + assert george.tv_layer_post_behavior_get(current_layer) == behavior + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerPostBehavior` does not return -1 when given a bad layer id.") +@pytest.mark.parametrize("behavior", george.LayerBehavior) +def test_tv_layer_post_behavior_set_wrong_id(behavior: george.LayerBehavior) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_post_behavior_set(-1, behavior) + + +def test_tv_layer_lock_position_get() -> None: + george.tv_layer_lock_position_get(george.tv_layer_current_id()) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerLockPosition` does not return -1 when given a bad layer id.") +def test_tv_layer_lock_position_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_lock_position_get(-1) + + +@pytest.mark.parametrize("state", [True, False]) +def test_tv_layer_lock_position_set(test_layer: george.TVPLayer, state: bool) -> None: + current_layer = george.tv_layer_current_id() + george.tv_layer_lock_position_set(current_layer, state) + assert george.tv_layer_lock_position_get(current_layer) == state + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerLockPosition` does not return -1 when given a bad layer id.") +@pytest.mark.parametrize("state", [True, False]) +def test_tv_layer_lock_position_set_wrong_id(state: bool) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_lock_position_set(-1, state) + + +def test_tv_preserve_get() -> None: + george.tv_preserve_get() + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_preserve_set` does not work in tvpaint 12.") +@pytest.mark.parametrize("state", george.LayerTransparency) +def test_tv_preserve_set(test_layer: george.TVPLayer, state: george.LayerTransparency) -> None: + george.tv_preserve_set(state) + new_state = george.tv_preserve_get() + + transparency_map = { + george.LayerTransparency.MINUS_1: george.LayerTransparency.ON, + george.LayerTransparency.NONE: george.LayerTransparency.OFF, + } + + assert transparency_map.get(state, state) == new_state + + +def test_tv_layer_mark_get() -> None: + george.tv_layer_mark_get(george.tv_layer_current_id(), 0) + + +def test_tv_layer_mark_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_mark_get(-1, 0) + + +@pytest.mark.parametrize("mark", range(27)) +def test_tv_layer_mark_set(test_anim_layer: george.TVPLayer, mark: int) -> None: + george.tv_layer_mark_set(test_anim_layer.id, 0, mark) + assert george.tv_layer_mark_get(test_anim_layer.id, 0) == mark + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerMarkSet` does not return -1 when given a bad layer id.") +def test_tv_layer_mark_set_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_mark_set(-1, 0, 0) + + +def test_tv_layer_anim(test_layer: george.TVPLayer) -> None: + george.tv_layer_anim(test_layer.id) + + assert george.tv_layer_auto_break_instance_get(test_layer.id) + assert george.tv_layer_auto_create_instance_get(test_layer.id) + assert not george.tv_layer_lock_get(test_layer.id) + + +def test_tv_layer_load_dependencies() -> None: + george.tv_layer_load_dependencies(george.tv_layer_current_id()) + + +@pytest.mark.parametrize("color_index", range(1, 27)) +def test_tv_layer_color_get_color(color_index: int) -> None: + current_clip = george.tv_clip_current_id() + color = george.tv_layer_color_get_color(current_clip, color_index) + assert color.clip_id == current_clip + assert color.color_index == color_index + + +def test_tv_layer_color_get_color_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_color_get_color(-1, 0) + + +name_args = [None, "test"] if IS_NOT_TVP12 else [None] + + +# We skip index 0 because it's the "Default" color and can't be changed +@pytest.mark.parametrize("color_index", range(1, 27)) +@pytest.mark.parametrize("name", name_args) +@pytest.mark.parametrize("rgb", [george.RGBColor(255, 0, 0), george.RGBColor(0, 255, 0)]) +def test_tv_layer_color_set_color( + test_clip: george.TVPClip, color_index: int, name: str | None, rgb: george.RGBColor +) -> None: + george.tv_layer_color_set_color(test_clip.id, color_index, rgb, name) + + color = george.tv_layer_color_get_color(test_clip.id, color_index) + + assert color.color_index == color_index + assert color.color_r == rgb.r + assert color.color_g == rgb.g + assert color.color_b == rgb.b + assert color.clip_id == test_clip.id + + if name is not None: + assert color.name == name + + +@pytest.mark.skipif( + not IS_NOT_TVP12, reason="`tv_LayerColor setcolor` does not return -1 when given a bad color index." +) +def test_tv_layer_color_set_color_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_color_set_color(-1, 1, george.RGBColor(0, 0, 0)) + + +def test_tv_layer_color_get(test_layer: george.TVPLayer) -> None: + index = george.tv_layer_color_get(test_layer.id) + assert 0 <= index <= 26 + + +def test_tv_layer_color_get_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_layer_color_get(-1) + + +@pytest.mark.parametrize("color_index", range(27)) +def test_tv_layer_color_set_s(test_layer: george.TVPLayer, color_index: int) -> None: + george.tv_layer_color_set(test_layer.id, color_index) + assert george.tv_layer_color_get(test_layer.id) == color_index + + +@pytest.fixture +def layers_with_colors(test_project: george.TVPProject) -> FixtureYield[tuple[int, list[int]]]: + """ + Fixture that create some layers with a color + """ + color_index = 5 + layers = [george.tv_layer_create(f"layer_{i}") for i in range(10)] + color_layers = [layer for i, layer in enumerate(layers) if i % 2 == 0] + + # Set the color of all layers + for layer in color_layers: + george.tv_layer_color_set(layer, color_index) + + yield color_index, color_layers + + +@pytest.mark.skipif(reason="Skip when running full tests otherwise it fails for some reason...") +@pytest.mark.parametrize("color_index", range(27)) +def test_tv_layer_color_visible(test_project: george.TVPProject, color_index: int) -> None: + # It seems that there's no way to set the visibility of a layer color group + # So we only test that all groups are visible + assert george.tv_layer_color_visible(color_index) + + +def test_tv_layer_color_lock(layers_with_colors: tuple[int, list[int]]) -> None: + color_index, layers_to_lock = layers_with_colors + assert george.tv_layer_color_lock(color_index) == len(layers_to_lock) + assert all(george.tv_layer_lock_get(layer) for layer in layers_to_lock) + + +def test_tv_layer_color_unlock(layers_with_colors: tuple[int, list[int]]) -> None: + color_index, layers_to_lock = layers_with_colors + + for layer in layers_to_lock: + george.tv_layer_lock_set(layer, True) + + assert george.tv_layer_color_unlock(color_index) == len(layers_to_lock) + assert all(not george.tv_layer_lock_get(layer) for layer in layers_to_lock) + + +@pytest.mark.parametrize("display", george.LayerColorDisplayOpt) +def test_tv_layer_color_show(layers_with_colors: tuple[int, list[int]], display: george.LayerColorDisplayOpt) -> None: + color_index, layers_to_show = layers_with_colors + + is_display = display == george.LayerColorDisplayOpt.DISPLAY + + for layer in layers_to_show: + if is_display: + george.tv_layer_display_set(layer, False) + else: + george.tv_layer_collapse_set(layer, True) + + george.tv_layer_color_show(display, color_index) + + check_fn = george.tv_layer_display_get if is_display else george.tv_layer_collapse_get + + assert all(check_fn(layer) for layer in layers_to_show) + + +@pytest.mark.parametrize("display", george.LayerColorDisplayOpt) +def test_tv_layer_color_hide(layers_with_colors: tuple[int, list[int]], display: george.LayerColorDisplayOpt) -> None: + color_index, layers_to_hide = layers_with_colors + + george.tv_layer_color_hide(display, color_index) + + check_fn = ( + george.tv_layer_display_get if display == george.LayerColorDisplayOpt.DISPLAY else george.tv_layer_collapse_get + ) + + assert all(not check_fn(layer) for layer in layers_to_hide) + + +def test_tv_layer_color_select(layers_with_colors: tuple[int, list[int]]) -> None: + color_index, layers_to_select = layers_with_colors + + george.tv_layer_color_select(color_index) + assert all(george.tv_layer_selection_get(layer) for layer in layers_to_select) + + +def test_tv_layer_color_unselect(layers_with_colors: tuple[int, list[int]]) -> None: + color_index, layers_to_select = layers_with_colors + + for layer in layers_to_select: + george.tv_layer_selection_set(layer, True) + + george.tv_layer_color_unselect(color_index) + assert all(not george.tv_layer_selection_get(layer) for layer in layers_to_select) + + +def can_be_parsed_as_int(value: str) -> bool: + if " " in value: + return False + + try: + int(value) + except ValueError: + return False + + 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", george.InstanceNamingMode) +@pytest.mark.parametrize("prefix", [None, "pre_"]) +@pytest.mark.parametrize("suffix", [None, "_suf"]) +@pytest.mark.parametrize("process", [None, *george.InstanceNamingProcess]) +@pytest.mark.parametrize("initial_name", ["", "5", " 7", "fest", "test_3"]) +def test_tv_instance_name( + test_layer: george.TVPLayer, + mode: george.InstanceNamingMode, + prefix: str | None, + suffix: str | None, + process: george.InstanceNamingProcess | None, + initial_name: str, +) -> None: + instance = 0 + + # Assign an initial name to the instance + george.tv_instance_set_name(test_layer.id, instance, name=initial_name) + + # Rename all instances + george.tv_instance_name(test_layer.id, mode, prefix, suffix, process) + + if mode == george.InstanceNamingMode.ALL: + expected_name = " " + str(instance + 1) + if prefix: + expected_name = prefix + expected_name + else: # SMART mode + no_prefix = prefix is None + no_suffix = suffix is None + + cond_text = ( + len(initial_name) and process == george.InstanceNamingProcess.TEXT and (can_be_parsed_as_int(initial_name)) + ) + + cond_empty = ( + len(initial_name) + and process == george.InstanceNamingProcess.EMPTY + and (no_prefix != no_suffix and not can_be_parsed_as_int(initial_name)) + ) + + cond_number = ( + len(initial_name) + and process == george.InstanceNamingProcess.NUMBER + and not (no_prefix == no_suffix and not can_be_parsed_as_int(initial_name)) + and not can_be_parsed_as_int(initial_name) + ) + + if cond_text or cond_empty or cond_number: + expected_name = initial_name + else: + expected_name = str(instance + 1) + + if suffix: + expected_name += suffix + + if prefix: + expected_name = prefix + expected_name + + assert expected_name == george.tv_instance_get_name(test_layer.id, instance) + + +@pytest.mark.skip("Crashed TVPaint") +@pytest.mark.parametrize("mode", george.InstanceNamingMode) +def test_tv_instance_name_wrong_id(mode: george.InstanceNamingMode) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_instance_name(-1, mode) + + +def test_tv_instance_get_name(test_layer: george.TVPLayer) -> None: + # By default there's an instance at frame zero + george.tv_instance_get_name(test_layer.id, 0) + + +def test_tv_instance_get_name_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_instance_get_name(-1, 0) + + +@pytest.mark.parametrize("name", ["", "5", " 7", "fest", "test_3"]) +def test_tv_instance_set_name(test_layer: george.TVPLayer, name: str) -> None: + george.tv_instance_set_name(test_layer.id, 0, name) + assert george.tv_instance_get_name(test_layer.id, 0) == name + + +def test_tv_instance_set_name_wrong_id() -> None: + with pytest.raises(george.GeorgeError): + george.tv_instance_set_name(-1, 0, "test") + + +def instance_exists(frame: int) -> bool: + try: + george.tv_instance_get_name(george.tv_layer_current_id(), frame) + except george.NoObjectWithIdError: + return False + return True + + +@pytest.mark.parametrize("frame", range(5)) +def test_tv_layer_copy_paste_single(test_anim_layer: george.TVPLayer, frame: int) -> None: + george.tv_layer_image(0) + george.tv_layer_copy() + george.tv_layer_image(frame) + george.tv_layer_paste() + + assert instance_exists(frame) + + +@pytest.mark.parametrize("frame", range(1, 6)) +def test_tv_layer_cut_paste_single(test_anim_layer: george.TVPLayer, frame: int) -> None: + cut_frame = 10 + + # Add another instance because if we cut the first it deletes the layer + george.tv_layer_copy() + george.tv_layer_image(cut_frame) + george.tv_layer_paste() + + # Cut that instance + george.tv_layer_image(cut_frame) + george.tv_layer_cut() + + # The instance should be removed + assert not instance_exists(cut_frame) + + # Paste at another frame + george.tv_layer_image(frame) + george.tv_layer_paste() + + assert instance_exists(frame) + + +def test_tv_layer_insert_image_duplicate(test_anim_layer: george.TVPLayer) -> None: + initial_frame = 5 + + # Copy the instance in the middle + george.tv_layer_image(0) + george.tv_layer_copy() + george.tv_layer_image(initial_frame) + george.tv_layer_paste() + + george.tv_layer_insert_image(duplicate=True) + assert instance_exists(initial_frame + 1) + + +@pytest.mark.parametrize("count", range(1, 8)) +@pytest.mark.parametrize("direction", george.InsertDirection) +def test_tv_layer_insert_image(test_anim_layer: george.TVPLayer, count: int, direction: george.InsertDirection) -> None: + initial_frame = 10 + + # Copy the instance in the middle + george.tv_layer_image(0) + george.tv_layer_copy() + george.tv_layer_image(initial_frame) + george.tv_layer_paste() + + george.tv_layer_insert_image(count, direction) + + offset = 0 + while offset < count: + if direction == george.InsertDirection.AFTER: # noqa: SIM108 + next_frame = initial_frame + offset + else: + next_frame = initial_frame - offset + + assert instance_exists(next_frame) + offset += 1 + + +@pytest.mark.parametrize("start", [0, 5, 10, 100]) +def test_tv_layer_shift(test_layer: george.TVPLayer, start: int) -> None: + george.tv_layer_shift(test_layer.id, start) + + +@pytest.mark.parametrize("blending", george.BlendingMode) +@pytest.mark.parametrize("stamp", [True, False]) +def test_tv_layer_merge(create_some_layers: list[Layer], blending: george.BlendingMode, stamp: bool) -> None: + george.tv_layer_merge(create_some_layers[0].id, blending, stamp) + + +@pytest.mark.parametrize("keep_color_grp", [False, True]) +@pytest.mark.parametrize("keep_img_mark", [False, True]) +@pytest.mark.parametrize("keep_instance_name", [False, True]) +def test_tv_layer_merge_all( + create_some_layers: list[Layer], + keep_color_grp: bool, + keep_img_mark: bool, + keep_instance_name: bool, +) -> None: + george.tv_layer_merge_all(keep_color_grp, keep_img_mark, keep_instance_name) + + +def test_tv_save_image(tmp_path: Path) -> None: + save_ext, _ = george.tv_save_mode_get() + ext = "jpg" if save_ext == george.SaveFormat.JPG else save_ext.value + out_img = (tmp_path / "out").with_suffix("." + ext) + george.tv_save_image(out_img) + assert out_img.exists() + + +@pytest.mark.parametrize("stretch", [False, True]) +def test_tv_load_image( + test_clip: george.TVPClip, + test_layer: george.TVPLayer, + png_sequence: list[Path], + stretch: bool, +) -> None: + george.tv_load_image(png_sequence[0], stretch) + # Verify that there's an instance frame + assert george.tv_instance_get_name(test_layer.id, 0) == "" + + +@pytest.mark.skip("Will block the UI") +def test_tv_load_image_file_does_not_exist(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError): + george.tv_load_image(tmp_path / "file.png") diff --git a/tests/george/test_grg_project.py b/tests/george/test_grg_project.py index 9d8b500..6c8087d 100644 --- a/tests/george/test_grg_project.py +++ b/tests/george/test_grg_project.py @@ -1,573 +1,509 @@ -# from __future__ import annotations -# -# import itertools -# from pathlib import Path -# from typing import Any -# -# import pytest -# -# from pytvpaint.george.exceptions import GeorgeError, NoObjectWithIdError -# from pytvpaint.george.grg_base import ( -# FieldOrder, -# ResizeOption, -# RGBColor, -# SaveFormat, -# tv_save_mode_get, -# tv_save_mode_set, -# ) -# from pytvpaint.george.grg_camera import tv_camera_insert_point -# from pytvpaint.george.grg_clip import ( -# tv_clip_current_id, -# tv_clip_info, -# tv_load_sequence, -# ) -# from pytvpaint.george.grg_project import ( -# BackgroundMode, -# TVPProject, -# tv_background_get, -# tv_background_set, -# tv_frame_rate_get, -# tv_frame_rate_set, -# tv_get_field, -# tv_get_height, -# tv_get_width, -# tv_load_palette, -# tv_load_project, -# tv_project_close, -# tv_project_current_frame_get, -# tv_project_current_frame_set, -# tv_project_current_id, -# tv_project_duplicate, -# tv_project_enum_id, -# tv_project_header_author_get, -# tv_project_header_author_set, -# tv_project_header_info_get, -# tv_project_header_info_set, -# tv_project_header_notes_get, -# tv_project_header_notes_set, -# tv_project_info, -# tv_project_new, -# tv_project_render_camera, -# tv_project_save_audio_dependencies, -# tv_project_save_sequence, -# tv_project_save_video_dependencies, -# tv_project_select, -# tv_ratio, -# tv_resize_page, -# tv_resize_project, -# tv_save_palette, -# tv_save_project, -# tv_sound_project_adjust, -# tv_sound_project_info, -# tv_sound_project_new, -# tv_sound_project_reload, -# tv_sound_project_remove, -# tv_start_frame_get, -# tv_start_frame_set, -# ) -# from tests.conftest import FixtureYield -# -# COLORS = [RGBColor(255, 0, 0), RGBColor(0, 255, 0), RGBColor(0, 0, 255)] -# -# -# @pytest.mark.parametrize( -# "mode, colors", -# [ -# *[(BackgroundMode.COLOR, c) for c in COLORS], -# *[(BackgroundMode.CHECK, cs) for cs in itertools.combinations(COLORS, 2)], -# (BackgroundMode.NONE, None), -# ], -# ) -# def test_tv_background( -# mode: BackgroundMode, colors: tuple[RGBColor, RGBColor] | RGBColor | None -# ) -> None: -# tv_background_set(mode, colors) -# -# current_mode, current_colors = tv_background_get() -# -# assert mode == current_mode -# assert current_colors == colors -# -# # reset color -# tv_background_set(BackgroundMode.COLOR, RGBColor(255, 255, 255)) -# -# -# @pytest.mark.parametrize("width", [500, 1920]) -# @pytest.mark.parametrize("height", [500, 1080]) -# @pytest.mark.parametrize("pixel_aspect_ratio", [1.0, 2.0, 10.0]) -# @pytest.mark.parametrize("frame_rate", [24.0, 12.0]) -# @pytest.mark.parametrize("field_order", FieldOrder) -# @pytest.mark.parametrize("start_frame", [1, 50]) -# def test_tv_project_new( -# tmp_path: Path, -# cleanup_current_project: None, -# width: int, -# height: int, -# pixel_aspect_ratio: float, -# frame_rate: float, -# field_order: FieldOrder, -# start_frame: int, -# ) -> None: -# project_path = tmp_path / "project.tvpp" -# tv_project_new( -# project_path, -# width, -# height, -# pixel_aspect_ratio, -# frame_rate, -# field_order, -# start_frame, -# ) -# -# project = tv_project_info(tv_project_current_id()) -# assert Path(project.path) == project_path -# assert project.width == width -# assert project.height == height -# assert project.field_order == field_order -# assert project.start_frame == start_frame -# -# -# @pytest.fixture -# def other_saved_project(tmp_path: Path) -> FixtureYield[TVPProject]: -# other_project_path = tmp_path / "other.tvpp" -# other_project_id = tv_project_new(other_project_path, width=200, height=200) -# other_project = tv_project_info(other_project_id) -# -# tv_save_project(other_project.path) -# tv_project_close(other_project.id) -# -# yield other_project -# -# -# def test_tv_load_project( -# other_saved_project: TVPProject, cleanup_current_project: None -# ) -> None: -# # Load the project -# pid = tv_load_project(other_saved_project.path) -# assert tv_project_info(pid) == other_saved_project -# -# -# def test_tv_load_project_wrong_path(tmp_path: Path) -> None: -# with pytest.raises(FileNotFoundError): -# tv_load_project(tmp_path / "folder" / "project.tvpp") -# -# -# @pytest.mark.parametrize("ext", [".tvpp", ".abc", ".tvpx"]) -# def test_tv_save_project(test_project: TVPProject, tmp_path: Path, ext: str) -> None: -# project_path = (tmp_path / "save").with_suffix(ext) -# tv_save_project(project_path) -# assert project_path.with_suffix(".tvpp").exists() -# -# -# def test_tv_save_project_wrong_path(test_project: TVPProject, tmp_path: Path) -> None: -# with pytest.raises(ValueError): -# tv_save_project(tmp_path / "folder" / "project.tvpp") -# -# -# def projects_equal(p1: TVPProject, p2: TVPProject) -> bool: -# """Compares two project omitting 'id' and 'path' attributes""" -# from dataclasses import asdict -# -# p1_dict = asdict(p1) -# p2_dict = asdict(p2) -# -# 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, -# ) -> None: -# tv_project_duplicate() -# dup_project = tv_project_info(tv_project_current_id()) -# assert projects_equal(test_project, dup_project) -# -# -# def test_tv_project_enum_id() -> None: -# assert tv_project_enum_id(0) == tv_project_current_id() -# -# -# @pytest.mark.parametrize("pos", [-1, 100, 58]) -# def test_tv_project_enum_id_wrong_pos(pos: int) -> None: -# with pytest.raises(GeorgeError): -# tv_project_enum_id(pos) -# -# -# def test_tv_project_current_id(test_project: TVPProject) -> None: -# assert tv_project_current_id() == test_project.id -# -# -# def test_tv_project_info(test_project: TVPProject) -> None: -# assert tv_project_info(test_project.id) == test_project -# -# -# @pytest.mark.skip("Doesn't return an empty string") -# def test_tv_project_info_wrong_id() -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_project_info("unknown") -# -# -# def test_tv_project_select(test_project: TVPProject, tmp_path: Path) -> None: -# other = tv_project_new(tmp_path / "other.tvpp") -# assert tv_project_current_id() == other -# -# tv_project_select(test_project.id) -# assert tv_project_current_id() == test_project.id -# tv_project_close(other) -# -# -# def test_tv_project_close(tmp_path: Path) -> None: -# pid = tv_project_new(tmp_path / "close.tvpp") -# tv_project_close(pid) -# -# with pytest.raises(NoObjectWithIdError): -# tv_project_info(pid) -# -# -# def get_project_pos(pid: str) -> int: -# """Return the given project position""" -# pos = 0 -# while True: -# try: -# if tv_project_enum_id(pos) == pid: -# return pos -# except GeorgeError as err: -# raise ValueError("Project does not exist") from err -# pos += 1 -# -# -# @pytest.mark.parametrize( -# "res", -# [(200, 200), (1000, 100), (0, 500)], -# ) -# def test_tv_resize_project( -# test_project: TVPProject, -# res: tuple[int, int], -# cleanup_current_project: None, -# ) -> None: -# current_pos = get_project_pos(test_project.id) -# -# width, height = res -# tv_resize_project(width, height) -# -# resized_project_id = tv_project_enum_id(current_pos) -# resized_project = tv_project_info(resized_project_id) -# -# assert resized_project.width == width -# assert resized_project.height == height -# -# -# @pytest.mark.parametrize( -# "res", -# [(200, 200), (1000, 100), (0, 500)], -# ) -# @pytest.mark.parametrize("resize", ResizeOption) -# def test_tv_resize_page( -# test_project: TVPProject, -# res: tuple[int, int], -# resize: ResizeOption, -# cleanup_current_project: None, -# ) -> None: -# current_pos = get_project_pos(test_project.id) -# -# width, height = res -# tv_resize_page(width, height, resize) -# -# resized_id = tv_project_enum_id(current_pos) -# resized = tv_project_info(resized_id) -# -# assert resized.width == width -# assert resized.height == height -# -# -# def test_tv_get_width(test_project: TVPProject) -> None: -# assert tv_get_width() == test_project.width -# -# -# def test_tv_get_height(test_project: TVPProject) -> None: -# assert tv_get_height() == test_project.height -# -# -# @pytest.mark.skip("Does not work, returns empty string") -# def test_tv_ratio(test_project: TVPProject) -> None: -# assert tv_ratio() == test_project.pixel_aspect_ratio -# -# -# def test_tv_get_field(test_project: TVPProject) -> None: -# assert tv_get_field() == test_project.field_order -# -# -# @pytest.mark.parametrize("use_camera", [False, True]) -# @pytest.mark.parametrize("start, end", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)]) -# def test_tv_project_save_sequence( -# test_project: TVPProject, -# tmp_path: Path, -# ppm_sequence: list[Path], -# use_camera: bool, -# start: int | None, -# end: int | None, -# ) -> None: -# tv_load_sequence(ppm_sequence[0]) -# -# if use_camera: -# tv_camera_insert_point(0, 50, 50, 0, 1) -# -# out_sequence = tmp_path / "out" -# tv_project_save_sequence(out_sequence, use_camera, start, end) -# -# clip = tv_clip_info(tv_clip_current_id()) -# save_ext, _ = tv_save_mode_get() -# start_end = ( -# (start, end) -# if (start is not None and end is not None) -# else (clip.first_frame, clip.last_frame) -# ) -# -# tv_save_mode_set(SaveFormat.JPG) -# -# for i in range(start_end[1] - start_end[0]): -# image_name = f"{out_sequence.name}{i:05d}" -# image_ext = "." + ("jpg" if save_ext == SaveFormat.JPG else save_ext.value) -# image_path = out_sequence.with_name(f"{image_name}{image_ext}") -# assert image_path.exists() -# -# -# def test_tv_project_render_camera( -# test_project: TVPProject, -# ppm_sequence: list[Path], -# cleanup_current_project: None, -# ) -> None: -# tv_load_sequence(ppm_sequence[0]) -# -# tv_camera_insert_point(0, 50, 50, 0, 1) -# tv_camera_insert_point(1, 100, 50, 0, 0.5) -# -# new_project_id = tv_project_render_camera(test_project.id) -# assert projects_equal(tv_project_info(new_project_id), test_project) -# -# -# def test_tv_frame_rate_get(test_project: TVPProject) -> None: -# project_frame_rate, playback_frame_rate = tv_frame_rate_get() -# assert test_project.frame_rate == playback_frame_rate -# assert test_project.frame_rate == project_frame_rate -# -# -# @pytest.mark.parametrize("frame_rate", [1, 25, 50, 100]) -# def test_tv_frame_rate_set_project(test_project: TVPProject, frame_rate: float) -> None: -# tv_frame_rate_set(frame_rate) -# project_frame_rate, _ = tv_frame_rate_get() -# assert project_frame_rate == frame_rate -# -# -# @pytest.mark.parametrize("frame_rate", [1, 25, 50, 100]) -# def test_tv_frame_rate_set_preview(test_project: TVPProject, frame_rate: float) -> None: -# tv_frame_rate_set(frame_rate, preview=True) -# _, preview_frame_rate = tv_frame_rate_get() -# assert preview_frame_rate == frame_rate -# -# -# def test_tv_project_current_frame_get(test_project: TVPProject) -> None: -# assert tv_project_current_frame_get() == 0 -# -# -# @pytest.mark.parametrize("frame", [1, 25, 50, 100]) -# def test_tv_project_current_frame_set(test_project: TVPProject, frame: int) -> None: -# tv_project_current_frame_set(frame) -# assert tv_project_current_frame_get() == frame -# -# -# def test_tv_load_palette(tmp_path: Path) -> None: -# palette_path = tmp_path / "palette.tvpx" -# tv_save_palette(palette_path) -# tv_load_palette(palette_path) -# -# -# def test_tv_load_palette_wrong_path(tmp_path: Path) -> None: -# with pytest.raises(FileNotFoundError): -# tv_load_palette(tmp_path / "palette.tvpx") -# -# -# def test_tv_save_palette_wrong_path(tmp_path: Path) -> None: -# with pytest.raises(NotADirectoryError): -# tv_save_palette(tmp_path / "out" / "palette.tvpx") -# -# -# def test_tv_project_save_video_dependencies(test_project: TVPProject) -> None: -# with pytest.raises(GeorgeError): -# tv_project_save_video_dependencies("") -# -# assert tv_project_save_video_dependencies(test_project.id) == 0 -# assert tv_project_save_video_dependencies(test_project.id, True, False) == 1 -# assert tv_project_save_video_dependencies(test_project.id, True, True) == 1 -# assert tv_project_save_video_dependencies(test_project.id, False, True) == 1 -# -# -# def test_tv_project_save_audio_dependencies(test_project: TVPProject) -> None: -# with pytest.raises(GeorgeError): -# tv_project_save_audio_dependencies("") -# -# assert tv_project_save_audio_dependencies(test_project.id) == 0 -# assert tv_project_save_audio_dependencies(test_project.id, True) == 1 -# assert tv_project_save_audio_dependencies(test_project.id, False) == 1 -# -# -# def test_tv_sound_project_info(test_project: TVPProject, wav_file: Path) -> None: -# tv_sound_project_new(wav_file) -# tv_sound_project_info(test_project.id, 0) -# -# -# def test_tv_sound_project_info_wrong_project_id() -> None: -# with pytest.raises(GeorgeError): -# tv_sound_project_info("lo", 0) -# -# -# def test_tv_sound_project_info_wrong_track_index(test_project: TVPProject) -> None: -# with pytest.raises(GeorgeError): -# tv_sound_project_info(test_project.id, 0) -# -# -# def test_tv_sound_project_new_wrong_path(tmp_path: Path) -> None: -# with pytest.raises(ValueError): -# tv_sound_project_new(tmp_path / "sound.wav") -# -# -# def test_tv_sound_project_remove(test_project: TVPProject, wav_file: Path) -> None: -# tv_sound_project_new(wav_file) -# tv_sound_project_info(test_project.id, 0) -# tv_sound_project_remove(0) -# -# with pytest.raises(GeorgeError): -# tv_sound_project_info(test_project.id, 0) -# -# -# def test_tv_sound_project_remove_wrong_track_index(test_project: TVPProject) -> None: -# with pytest.raises(GeorgeError): -# tv_sound_project_remove(0) -# -# -# def test_tv_sound_project_reload(test_project: TVPProject, wav_file: Path) -> None: -# tv_sound_project_new(wav_file) -# tv_sound_project_reload(test_project.id, 0) -# -# -# def test_tv_sound_project_reload_wrong_project_id() -> None: -# with pytest.raises(GeorgeError): -# tv_sound_project_reload("lo", 0) -# -# -# def test_tv_sound_project_reload_wrong_track_index(test_project: TVPProject) -> None: -# with pytest.raises(GeorgeError): -# tv_sound_project_reload(test_project.id, 0) -# -# -# @pytest.mark.parametrize( -# "args", -# [ -# (True, 5), -# (False, 10), -# (True,), -# (True, 2, 5), -# (False, 1.5, 0, 1, 4, 4, 4), -# ], -# ) -# def test_tv_sound_project_adjust( -# test_project: TVPProject, -# wav_file: Path, -# args: tuple[Any, ...], -# ) -> None: -# tv_sound_project_new(wav_file) -# tv_sound_project_adjust(0, *args) -# -# attrs_check = [ -# "mute", -# "volume", -# "offset", -# "fade_in_start", -# "fade_in_stop", -# "fade_out_start", -# "fade_out_stop", -# "color_index", -# ] -# -# sound = tv_sound_project_info(test_project.id, 0) -# for attr, arg in zip(attrs_check, args): -# current = getattr(sound, attr) -# err_msg = f"Error checking {attr} (expected: {arg}, current: {current})" -# assert current == arg, err_msg -# -# -# def test_tv_project_header_info_get(test_project: TVPProject) -> None: -# assert tv_project_header_info_get(test_project.id) == "" -# -# -# def test_tv_project_header_info_get_wrong_id(test_project: TVPProject) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_project_header_info_get("ll") -# -# -# @pytest.mark.parametrize("header", ["", "Hello", "THis is a project header"]) -# def test_tv_project_header_info_set(test_project: TVPProject, header: str) -> None: -# tv_project_header_info_set(test_project.id, header) -# assert tv_project_header_info_get(test_project.id) == header -# -# -# def test_tv_project_header_info_set_wrong_id(test_project: TVPProject) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_project_header_info_set("ll", "header") -# -# -# def test_tv_project_header_author_get(test_project: TVPProject) -> None: -# import getpass -# -# assert tv_project_header_author_get(test_project.id) == getpass.getuser() -# -# -# def test_tv_project_header_author_get_wrong_id(test_project: TVPProject) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_project_header_author_get("ll") -# -# -# @pytest.mark.parametrize("author", ["l", "Hello", "THis is a project author"]) -# def test_tv_project_header_author_set(test_project: TVPProject, author: str) -> None: -# tv_project_header_author_set(test_project.id, author) -# assert tv_project_header_author_get(test_project.id) == author -# -# -# def test_tv_project_header_author_set_wrong_id(test_project: TVPProject) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_project_header_author_set("ll", "header") -# -# -# def test_tv_project_header_notes_get(test_project: TVPProject) -> None: -# assert tv_project_header_notes_get(test_project.id) == "" -# -# -# def test_tv_project_header_notes_get_wrong_id(test_project: TVPProject) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_project_header_notes_get("ll") -# -# -# @pytest.mark.parametrize("notes", ["l", "Hello", "THis is a project notes"]) -# def test_tv_project_header_notes_set(test_project: TVPProject, notes: str) -> None: -# tv_project_header_notes_set(test_project.id, notes) -# assert tv_project_header_notes_get(test_project.id) == notes -# -# -# def test_tv_project_header_notes_set_wrong_id(test_project: TVPProject) -> None: -# with pytest.raises(NoObjectWithIdError): -# tv_project_header_notes_set("ll", "header") -# -# -# def test_tv_start_frame_get(test_project: TVPProject) -> None: -# tv_start_frame_set(12) -# assert tv_start_frame_get() == 12 -# -# -# @pytest.mark.parametrize("start", [0, 1, 50, 100]) -# def test_tv_start_frame_set(test_project: TVPProject, start: int) -> None: -# tv_start_frame_set(start) -# assert tv_start_frame_get() == start +from __future__ import annotations + +import itertools +from pathlib import Path +from typing import Any + +import pytest + +from pytvpaint import george +from tests.conftest import FixtureYield + +COLORS = [george.RGBColor(255, 0, 0), george.RGBColor(0, 255, 0), george.RGBColor(0, 0, 255)] + + +@pytest.mark.parametrize( + "mode, colors", + [ + *[(george.BackgroundMode.COLOR, c) for c in COLORS], + *[(george.BackgroundMode.CHECK, cs) for cs in itertools.combinations(COLORS, 2)], + (george.BackgroundMode.NONE, None), + ], +) +def test_tv_background( + mode: george.BackgroundMode, colors: tuple[george.RGBColor, george.RGBColor] | george.RGBColor | None +) -> None: + george.tv_background_set(mode, colors) + + current_mode, current_colors = george.tv_background_get() + + assert mode == current_mode + assert current_colors == colors + + # reset color + george.tv_background_set(george.BackgroundMode.COLOR, george.RGBColor(255, 255, 255)) + + +@pytest.mark.parametrize("width", [500, 1920]) +@pytest.mark.parametrize("height", [500, 1080]) +@pytest.mark.parametrize("pixel_aspect_ratio", [1.0, 2.0, 10.0]) +@pytest.mark.parametrize("frame_rate", [24.0, 12.0]) +@pytest.mark.parametrize("field_order", george.FieldOrder) +@pytest.mark.parametrize("start_frame", [1, 50]) +def test_tv_project_new( + tmp_path: Path, + cleanup_current_project: None, + width: int, + height: int, + pixel_aspect_ratio: float, + frame_rate: float, + field_order: george.FieldOrder, + start_frame: int, +) -> None: + project_path = tmp_path / "project.tvpp" + george.tv_project_new( + project_path, + width, + height, + pixel_aspect_ratio, + frame_rate, + field_order, + start_frame, + ) + + project = george.tv_project_info(george.tv_project_current_id()) + assert Path(project.path) == project_path + assert project.width == width + assert project.height == height + assert project.field_order == field_order + assert project.start_frame == start_frame + + +@pytest.fixture +def other_saved_project(tmp_path: Path) -> FixtureYield[george.TVPProject]: + other_project_path = tmp_path / "other.tvpp" + other_project_id = george.tv_project_new(other_project_path, width=200, height=200) + other_project = george.tv_project_info(other_project_id) + + george.tv_save_project(other_project.path) + george.tv_project_close(other_project.id) + + yield other_project + + +def test_tv_load_project(other_saved_project: george.TVPProject, cleanup_current_project: None) -> None: + # Load the project + pid = george.tv_load_project(other_saved_project.path) + assert george.tv_project_info(pid) == other_saved_project + + +def test_tv_load_project_wrong_path(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError): + george.tv_load_project(tmp_path / "folder" / "project.tvpp") + + +@pytest.mark.parametrize("ext", [".tvpp", ".abc", ".tvpx"]) +def test_tv_save_project(test_project: george.TVPProject, tmp_path: Path, ext: str) -> None: + project_path = (tmp_path / "save").with_suffix(ext) + george.tv_save_project(project_path) + assert project_path.with_suffix(".tvpp").exists() + + +def test_tv_save_project_wrong_path(test_project: george.TVPProject, tmp_path: Path) -> None: + with pytest.raises(ValueError): + george.tv_save_project(tmp_path / "folder" / "project.tvpp") + + +def projects_equal(p1: george.TVPProject, p2: george.TVPProject) -> bool: + """Compares two project omitting 'id' and 'path' attributes""" + from dataclasses import asdict + + p1_dict = asdict(p1) + p2_dict = asdict(p2) + + 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: george.TVPProject, + cleanup_current_project: None, +) -> None: + george.tv_project_duplicate() + dup_project = george.tv_project_info(george.tv_project_current_id()) + assert projects_equal(test_project, dup_project) + + +def test_tv_project_enum_id() -> None: + assert george.tv_project_enum_id(0) == george.tv_project_current_id() + + +@pytest.mark.parametrize("pos", [-1, 100, 58]) +def test_tv_project_enum_id_wrong_pos(pos: int) -> None: + with pytest.raises(george.GeorgeError): + george.tv_project_enum_id(pos) + + +def test_tv_project_current_id(test_project: george.TVPProject) -> None: + assert george.tv_project_current_id() == test_project.id + + +def test_tv_project_info(test_project: george.TVPProject) -> None: + assert george.tv_project_info(test_project.id) == test_project + + +@pytest.mark.skip("Doesn't return an empty string") +def test_tv_project_info_wrong_id() -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_project_info("unknown") + + +def test_tv_project_select(test_project: george.TVPProject, tmp_path: Path) -> None: + other = george.tv_project_new(tmp_path / "other.tvpp") + assert george.tv_project_current_id() == other + + george.tv_project_select(test_project.id) + assert george.tv_project_current_id() == test_project.id + george.tv_project_close(other) + + +def test_tv_project_close(tmp_path: Path) -> None: + pid = george.tv_project_new(tmp_path / "close.tvpp") + george.tv_project_close(pid) + + with pytest.raises(george.NoObjectWithIdError): + george.tv_project_info(pid) + + +def get_project_pos(pid: str) -> int: + """Return the given project position""" + pos = 0 + while True: + try: + if george.tv_project_enum_id(pos) == pid: + return pos + except george.GeorgeError as err: + raise ValueError("Project does not exist") from err + pos += 1 + + +@pytest.mark.parametrize( + "res", + [(200, 200), (1000, 100), (0, 500)], +) +def test_tv_resize_project( + test_project: george.TVPProject, + res: tuple[int, int], + cleanup_current_project: None, +) -> None: + current_pos = get_project_pos(test_project.id) + + width, height = res + george.tv_resize_project(width, height) + + resized_project_id = george.tv_project_enum_id(current_pos) + resized_project = george.tv_project_info(resized_project_id) + + assert resized_project.width == width + assert resized_project.height == height + + +@pytest.mark.parametrize( + "res", + [(200, 200), (1000, 100), (0, 500)], +) +@pytest.mark.parametrize("resize", george.ResizeOption) +def test_tv_resize_page( + test_project: george.TVPProject, + res: tuple[int, int], + resize: george.ResizeOption, + cleanup_current_project: None, +) -> None: + current_pos = get_project_pos(test_project.id) + + width, height = res + george.tv_resize_page(width, height, resize) + + resized_id = george.tv_project_enum_id(current_pos) + resized = george.tv_project_info(resized_id) + + assert resized.width == width + assert resized.height == height + + +def test_tv_get_width(test_project: george.TVPProject) -> None: + assert george.tv_get_width() == test_project.width + + +def test_tv_get_height(test_project: george.TVPProject) -> None: + assert george.tv_get_height() == test_project.height + + +@pytest.mark.skip("Does not work, returns empty string") +def test_tv_ratio(test_project: george.TVPProject) -> None: + assert george.tv_ratio() == test_project.pixel_aspect_ratio + + +def test_tv_get_field(test_project: george.TVPProject) -> None: + assert george.tv_get_field() == test_project.field_order + + +@pytest.mark.parametrize("use_camera", [False, True]) +@pytest.mark.parametrize("start, end", [(None, None), (0, 5), (0, 0), (0, 1), (2, 5)]) +def test_tv_project_save_sequence( + test_project: george.TVPProject, + tmp_path: Path, + png_sequence: list[Path], + use_camera: bool, + start: int | None, + end: int | None, +) -> None: + george.tv_load_sequence(png_sequence[0]) + + if use_camera: + george.tv_camera_insert_point(0, 50, 50, 0, 1) + + out_sequence = tmp_path / "out" + george.tv_project_save_sequence(out_sequence, use_camera, start, end) + + clip = george.tv_clip_info(george.tv_clip_current_id()) + save_ext, _ = george.tv_save_mode_get() + start_end = (start, end) if (start is not None and end is not None) else (clip.first_frame, clip.last_frame) + + george.tv_save_mode_set(george.SaveFormat.JPG) + + for i in range(start_end[1] - start_end[0]): + image_name = f"{out_sequence.name}{i:05d}" + image_ext = "." + ("jpg" if save_ext == george.SaveFormat.JPG else save_ext.value) + image_path = out_sequence.with_name(f"{image_name}{image_ext}") + assert image_path.exists() + + +def test_tv_project_render_camera( + test_project: george.TVPProject, + png_sequence: list[Path], + cleanup_current_project: None, +) -> None: + george.tv_load_sequence(png_sequence[0]) + + george.tv_camera_insert_point(0, 50, 50, 0, 1) + george.tv_camera_insert_point(1, 100, 50, 0, 0.5) + + new_project_id = george.tv_project_render_camera(test_project.id) + assert projects_equal(george.tv_project_info(new_project_id), test_project) + + +def test_tv_frame_rate_get(test_project: george.TVPProject) -> None: + project_frame_rate, playback_frame_rate = george.tv_frame_rate_get() + assert test_project.frame_rate == playback_frame_rate + assert test_project.frame_rate == project_frame_rate + + +@pytest.mark.parametrize("frame_rate", [1, 25, 50, 100]) +def test_tv_frame_rate_set_project(test_project: george.TVPProject, frame_rate: float) -> None: + george.tv_frame_rate_set(frame_rate) + project_frame_rate, _ = george.tv_frame_rate_get() + assert project_frame_rate == frame_rate + + +@pytest.mark.parametrize("frame_rate", [1, 25, 50, 100]) +def test_tv_frame_rate_set_preview(test_project: george.TVPProject, frame_rate: float) -> None: + george.tv_frame_rate_set(frame_rate, preview=True) + _, preview_frame_rate = george.tv_frame_rate_get() + assert preview_frame_rate == frame_rate + + +def test_tv_project_current_frame_get(test_project: george.TVPProject) -> None: + assert george.tv_project_current_frame_get() == 0 + + +@pytest.mark.parametrize("frame", [1, 25, 50, 100]) +def test_tv_project_current_frame_set(test_project: george.TVPProject, frame: int) -> None: + george.tv_project_current_frame_set(frame) + assert george.tv_project_current_frame_get() == frame + + +def test_tv_load_palette(tmp_path: Path) -> None: + palette_path = tmp_path / "palette.tvpx" + george.tv_save_palette(palette_path) + george.tv_load_palette(palette_path) + + +def test_tv_load_palette_wrong_path(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError): + george.tv_load_palette(tmp_path / "palette.tvpx") + + +def test_tv_save_palette_wrong_path(tmp_path: Path) -> None: + with pytest.raises(NotADirectoryError): + george.tv_save_palette(tmp_path / "out" / "palette.tvpx") + + +def test_tv_project_save_video_dependencies(test_project: george.TVPProject) -> None: + with pytest.raises(george.GeorgeError): + george.tv_project_save_video_dependencies("") + + assert george.tv_project_save_video_dependencies(test_project.id) == 0 + assert george.tv_project_save_video_dependencies(test_project.id, True, False) == 1 + assert george.tv_project_save_video_dependencies(test_project.id, True, True) == 1 + assert george.tv_project_save_video_dependencies(test_project.id, False, True) == 1 + + +def test_tv_project_save_audio_dependencies(test_project: george.TVPProject) -> None: + with pytest.raises(george.GeorgeError): + george.tv_project_save_audio_dependencies("") + + assert george.tv_project_save_audio_dependencies(test_project.id) == 0 + assert george.tv_project_save_audio_dependencies(test_project.id, True) == 1 + assert george.tv_project_save_audio_dependencies(test_project.id, False) == 1 + + +def test_tv_sound_project_info(test_project: george.TVPProject, wav_file: Path) -> None: + george.tv_sound_project_new(wav_file) + george.tv_sound_project_info(test_project.id, 0) + + +def test_tv_sound_project_info_wrong_project_id() -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_project_info("lo", 0) + + +def test_tv_sound_project_info_wrong_track_index(test_project: george.TVPProject) -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_project_info(test_project.id, 0) + + +def test_tv_sound_project_new_wrong_path(tmp_path: Path) -> None: + with pytest.raises(ValueError): + george.tv_sound_project_new(tmp_path / "sound.wav") + + +def test_tv_sound_project_remove(test_project: george.TVPProject, wav_file: Path) -> None: + george.tv_sound_project_new(wav_file) + george.tv_sound_project_info(test_project.id, 0) + george.tv_sound_project_remove(0) + + with pytest.raises(george.GeorgeError): + george.tv_sound_project_info(test_project.id, 0) + + +def test_tv_sound_project_remove_wrong_track_index(test_project: george.TVPProject) -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_project_remove(0) + + +def test_tv_sound_project_reload(test_project: george.TVPProject, wav_file: Path) -> None: + george.tv_sound_project_new(wav_file) + george.tv_sound_project_reload(test_project.id, 0) + + +def test_tv_sound_project_reload_wrong_project_id() -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_project_reload("lo", 0) + + +def test_tv_sound_project_reload_wrong_track_index(test_project: george.TVPProject) -> None: + with pytest.raises(george.GeorgeError): + george.tv_sound_project_reload(test_project.id, 0) + + +@pytest.mark.parametrize( + "args", + [ + (True, 5), + (False, 10), + (True,), + (True, 2, 5), + (False, 1.5, 0, 1, 4, 4, 4), + ], +) +def test_tv_sound_project_adjust( + test_project: george.TVPProject, + wav_file: Path, + args: tuple[Any, ...], +) -> None: + george.tv_sound_project_new(wav_file) + george.tv_sound_project_adjust(0, *args) + + attrs_check = [ + "mute", + "volume", + "offset", + "fade_in_start", + "fade_in_stop", + "fade_out_start", + "fade_out_stop", + "color_index", + ] + + sound = george.tv_sound_project_info(test_project.id, 0) + for attr, arg in zip(attrs_check, args): + current = getattr(sound, attr) + err_msg = f"Error checking {attr} (expected: {arg}, current: {current})" + assert current == arg, err_msg + + +def test_tv_project_header_info_get(test_project: george.TVPProject) -> None: + assert george.tv_project_header_info_get(test_project.id) == "" + + +def test_tv_project_header_info_get_wrong_id(test_project: george.TVPProject) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_project_header_info_get("ll") + + +@pytest.mark.parametrize("header", ["", "Hello", "This is a project header", "This is a project \nheader"]) +def test_tv_project_header_info_set(test_project: george.TVPProject, header: str) -> None: + george.tv_project_header_info_set(test_project.id, header) + assert george.tv_project_header_info_get(test_project.id) == header + + +def test_tv_project_header_info_set_wrong_id(test_project: george.TVPProject) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_project_header_info_set("ll", "header") + + +def test_tv_project_header_author_get(test_project: george.TVPProject) -> None: + import getpass + + assert george.tv_project_header_author_get(test_project.id) == getpass.getuser() + + +def test_tv_project_header_author_get_wrong_id(test_project: george.TVPProject) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_project_header_author_get("ll") + + +@pytest.mark.parametrize("author", ["l", "Hello", "This is a project author", "This is a project \nauthor"]) +def test_tv_project_header_author_set(test_project: george.TVPProject, author: str) -> None: + george.tv_project_header_author_set(test_project.id, author) + assert george.tv_project_header_author_get(test_project.id) == author + + +def test_tv_project_header_author_set_wrong_id(test_project: george.TVPProject) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_project_header_author_set("ll", "header") + + +def test_tv_project_header_notes_get(test_project: george.TVPProject) -> None: + assert george.tv_project_header_notes_get(test_project.id) == "" + + +def test_tv_project_header_notes_get_wrong_id(test_project: george.TVPProject) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_project_header_notes_get("ll") + + +@pytest.mark.parametrize("notes", ["l", "Hello", "This is a project note", r"This is a project \nnote"]) +def test_tv_project_header_notes_set(test_project: george.TVPProject, notes: str) -> None: + george.tv_project_header_notes_set(test_project.id, notes) + assert george.tv_project_header_notes_get(test_project.id) == notes + + +def test_tv_project_header_notes_set_wrong_id(test_project: george.TVPProject) -> None: + with pytest.raises(george.NoObjectWithIdError): + george.tv_project_header_notes_set("ll", "header") + + +def test_tv_start_frame_get(test_project: george.TVPProject) -> None: + george.tv_start_frame_set(12) + assert george.tv_start_frame_get() == 12 + + +@pytest.mark.parametrize("start", [0, 1, 50, 100]) +def test_tv_start_frame_set(test_project: george.TVPProject, start: int) -> None: + george.tv_start_frame_set(start) + assert george.tv_start_frame_get() == start diff --git a/tests/george/test_grg_scene.py b/tests/george/test_grg_scene.py index a61f5de..1a7d9bb 100644 --- a/tests/george/test_grg_scene.py +++ b/tests/george/test_grg_scene.py @@ -1,106 +1,97 @@ -# from __future__ import annotations -# -# from collections.abc import Iterator -# -# import pytest -# -# from pytvpaint.george.exceptions import GeorgeError -# from pytvpaint.george.grg_project import TVPProject -# from pytvpaint.george.grg_scene import ( -# tv_scene_close, -# tv_scene_current_id, -# tv_scene_duplicate, -# tv_scene_enum_id, -# tv_scene_move, -# tv_scene_new, -# ) -# from tests.conftest import test_project -# -# -# def test_tv_scene_enum_id(test_project: TVPProject) -> None: -# assert tv_scene_enum_id(0) -# -# -# @pytest.mark.parametrize("pos", [-1, 10]) -# def test_tv_scene_enum_id_wrong_pos(pos: int) -> None: -# with pytest.raises(GeorgeError): -# tv_scene_enum_id(-1) -# -# -# def test_tv_scene_current_id(test_project: TVPProject) -> None: -# assert tv_scene_current_id() -# -# -# def create_new_scene() -> int: -# """Creates a new scene and return its id""" -# tv_scene_new() -# return tv_scene_current_id() -# -# -# @pytest.mark.parametrize("pos", range(5)) -# def test_tv_scene_move(test_project: TVPProject, test_scene: int, pos: int) -> None: -# for _ in range(5): -# create_new_scene() -# tv_scene_move(test_scene, pos) -# assert tv_scene_enum_id(pos) == test_scene -# -# -# def test_tv_scene_new(test_project: TVPProject) -> None: -# previous = tv_scene_current_id() -# tv_scene_new() -# assert tv_scene_current_id() != previous -# -# -# def scenes_iterate() -> Iterator[tuple[int, int]]: -# pos = 0 -# while True: -# try: -# yield pos, tv_scene_enum_id(pos) -# except GeorgeError: -# break -# pos += 1 -# -# -# def get_scene_ids() -> Iterator[int]: -# for _, scene_id in scenes_iterate(): -# yield scene_id -# -# -# def get_scene_pos(scene_id: int) -> int: -# for pos, other_id in scenes_iterate(): -# if other_id == scene_id: -# return pos -# raise Exception("Scene doesn't exist") -# -# -# other_project = test_project -# -# -# def test_tv_scene_duplicate( -# test_project: TVPProject, -# test_scene: int, -# ) -> None: -# # Create another scene to test the behavior -# tv_scene_new() -# -# scenes_before = list(get_scene_ids()) -# test_scene_pos = get_scene_pos(test_scene) -# -# # Duplicate the scene -# tv_scene_duplicate(test_scene) -# dup_scene_pos = test_scene_pos + 1 -# dup_scene = tv_scene_enum_id(dup_scene_pos) -# -# # The duplicated scene is inserted after the test scene -# scenes_after = scenes_before -# scenes_after.insert(dup_scene_pos, dup_scene) -# -# assert list(get_scene_ids()) == scenes_after -# -# -# def test_tv_scene_close(test_project: TVPProject, test_scene: int) -> None: -# scenes_before = list(get_scene_ids()) -# tv_scene_close(test_scene) -# -# scenes_before.remove(test_scene) -# assert list(get_scene_ids()) == scenes_before +from __future__ import annotations + +from collections.abc import Iterator + +import pytest + +from pytvpaint import george +from tests.conftest import test_project + + +def test_tv_scene_enum_id(test_project: george.TVPProject) -> None: + assert george.tv_scene_enum_id(0) + + +@pytest.mark.parametrize("pos", [-1, 10]) +def test_tv_scene_enum_id_wrong_pos(pos: int) -> None: + with pytest.raises(george.GeorgeError): + george.tv_scene_enum_id(-1) + + +def test_tv_scene_current_id(test_project: george.TVPProject) -> None: + assert george.tv_scene_current_id() + + +def create_new_scene() -> int: + """Creates a new scene and return its id""" + george.tv_scene_new() + return george.tv_scene_current_id() + + +@pytest.mark.parametrize("pos", range(5)) +def test_tv_scene_move(test_project: george.TVPProject, test_scene: int, pos: int) -> None: + for _ in range(5): + create_new_scene() + george.tv_scene_move(test_scene, pos) + assert george.tv_scene_enum_id(pos) == test_scene + + +def test_tv_scene_new(test_project: george.TVPProject) -> None: + previous = george.tv_scene_current_id() + george.tv_scene_new() + assert george.tv_scene_current_id() != previous + + +def scenes_iterate() -> Iterator[tuple[int, int]]: + pos = 0 + while True: + try: + yield pos, george.tv_scene_enum_id(pos) + except george.GeorgeError: + break + pos += 1 + + +def get_scene_ids() -> Iterator[int]: + for _, scene_id in scenes_iterate(): + yield scene_id + + +def get_scene_pos(scene_id: int) -> int: + for pos, other_id in scenes_iterate(): + if other_id == scene_id: + return pos + raise Exception("Scene doesn't exist") + + +other_project = test_project + + +def test_tv_scene_duplicate( + test_project: george.TVPProject, + test_scene: int, +) -> None: + # Create another scene to test the behavior + george.tv_scene_new() + + scenes_before = list(get_scene_ids()) + test_scene_pos = get_scene_pos(test_scene) + + # Duplicate the scene + george.tv_scene_duplicate(test_scene) + dup_scene_pos = test_scene_pos + 1 + dup_scene = george.tv_scene_enum_id(dup_scene_pos) + + # The duplicated scene is inserted after the test scene + scenes_after = scenes_before + scenes_after.insert(dup_scene_pos, dup_scene) + + assert list(get_scene_ids()) == scenes_after + + +def test_tv_scene_close(test_project: george.TVPProject, test_scene: int) -> None: + scenes_before = list(get_scene_ids()) + george.tv_scene_close(test_scene) + + scenes_before.remove(test_scene) + assert list(get_scene_ids()) == scenes_before diff --git a/tests/george/test_utils.py b/tests/george/test_utils.py index b28f3e2..eb10458 100644 --- a/tests/george/test_utils.py +++ b/tests/george/test_utils.py @@ -1,11 +1,11 @@ -# import pytest -# -# from pytvpaint.project import Project -# from pytvpaint.utils import restore_current_frame -# -# -# @pytest.mark.parametrize("frame", [5, 2, 10]) -# def test_with_restore_current_frame(test_project_obj: Project, frame: int) -> None: -# frame = test_project_obj.current_frame -# with restore_current_frame(test_project_obj, frame): -# assert test_project_obj.current_frame == frame +import pytest + +from pytvpaint.project import Project +from pytvpaint.utils import restore_current_frame + + +@pytest.mark.parametrize("frame", [5, 2, 10]) +def test_with_restore_current_frame(test_project_obj: Project, frame: int) -> None: + frame = test_project_obj.current_frame + with restore_current_frame(test_project_obj, frame): + assert test_project_obj.current_frame == frame diff --git a/tests/test_camera.py b/tests/test_camera.py index 9fb6c86..fc61fb3 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -1,87 +1,91 @@ -# import pytest -# -# from pytvpaint import george -# from pytvpaint.camera import Camera, CameraPoint -# from pytvpaint.clip import Clip -# from pytvpaint.george.grg_base import FieldOrder -# from tests.conftest import FixtureYield -# -# -# @pytest.fixture -# def current_camera(test_clip_obj: Clip) -> FixtureYield[Camera]: -# yield Camera(test_clip_obj) -# -# -# def test_camera_init(test_clip_obj: Clip) -> None: -# camera_data = george.tv_camera_info_get() -# camera = Camera(test_clip_obj, data=camera_data) -# -# assert camera.clip == test_clip_obj -# -# assert camera.width == camera_data.width -# assert camera.height == camera_data.height -# assert camera.field_order == camera_data.field_order -# assert camera.fps == camera_data.frame_rate -# assert camera.pixel_aspect_ratio == camera_data.pixel_aspect_ratio -# assert camera.anti_aliasing == camera_data.anti_aliasing -# -# -# def test_camera_refresh(current_camera: Camera) -> None: -# current_camera.refresh() -# -# -# @pytest.mark.parametrize("width", [1, 50, 100, 1920]) -# def test_camera_width(current_camera: Camera, width: int) -> None: -# current_camera.width = width -# assert current_camera.width == width -# -# -# @pytest.mark.parametrize("height", [1, 50, 100, 1080]) -# def test_camera_height(current_camera: Camera, height: int) -> None: -# current_camera.height = height -# assert current_camera.height == height -# -# -# @pytest.mark.parametrize("fps", [5.0, 10.0, 24.0, 60.0]) -# def test_camera_fps(current_camera: Camera, fps: float) -> None: -# current_camera.fps = fps -# assert current_camera.fps == fps -# -# -# @pytest.mark.parametrize("aspect_ratio", [0.5, 1, 4]) -# def test_camera_pixel_aspect_ratio(current_camera: Camera, aspect_ratio: float) -> None: -# current_camera.pixel_aspect_ratio = aspect_ratio -# assert current_camera.pixel_aspect_ratio == aspect_ratio -# -# -# @pytest.mark.parametrize("field_order", FieldOrder) -# def test_camera_field_order(current_camera: Camera, field_order: FieldOrder) -> None: -# current_camera.field_order = field_order -# assert current_camera.field_order == field_order -# -# -# def test_camera_insert_point(current_camera: Camera) -> None: -# point = current_camera.insert_point(0, 50, 50, 34, 1.5) -# assert next(current_camera.points) == point -# -# -# def test_camera_current_points_data(current_camera: Camera) -> None: -# george.tv_camera_insert_point(0, 50, 50, 34, 1.5) -# point = george.tv_camera_enum_points(0) -# assert next(current_camera.points) == CameraPoint( -# 0, camera=current_camera, data=point -# ) -# -# -# def test_camera_get_point_data_at(current_camera: Camera) -> None: -# start = current_camera.insert_point(0, 50, 50, 10, 1.2) -# end = current_camera.insert_point(1, 10, 5, 48, 0.4) -# -# assert current_camera.get_point_data_at(0.0) == start.data -# assert current_camera.get_point_data_at(1.0) == end.data -# -# -# def test_camera_remove_point(current_camera: Camera) -> None: -# current_camera.insert_point(0, 50, 50, 34, 1.5) -# current_camera.remove_point(0) -# assert len(list(current_camera.points)) == 0 +import pytest + +from pytvpaint import george +from pytvpaint.camera import Camera, CameraPoint +from pytvpaint.clip import Clip +from tests.conftest import FixtureYield + +IS_NOT_TVP12 = not george.tv_version()[1].startswith("12") + + +@pytest.fixture +def current_camera(test_clip_obj: Clip) -> FixtureYield[Camera]: + yield Camera(test_clip_obj) + + +def test_camera_init(test_clip_obj: Clip) -> None: + camera_data = george.tv_camera_info_get() + camera = Camera(test_clip_obj, data=camera_data) + + assert camera.clip == test_clip_obj + + assert camera.width == camera_data.width + assert camera.height == camera_data.height + assert camera.field_order == camera_data.field_order + assert camera.fps == camera_data.frame_rate + assert camera.pixel_aspect_ratio == camera_data.pixel_aspect_ratio + assert camera.anti_aliasing == camera_data.anti_aliasing + + +def test_camera_refresh(current_camera: Camera) -> None: + current_camera.refresh() + + +@pytest.mark.parametrize("width", [1, 50, 100, 1920]) +def test_camera_width(current_camera: Camera, width: int) -> None: + current_camera.width = width + assert current_camera.width == width + + +@pytest.mark.parametrize("height", [1, 50, 100, 1080]) +def test_camera_height(current_camera: Camera, height: int) -> None: + current_camera.height = height + assert current_camera.height == height + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="Setting the camera fps is no longer possible in TVP12.") +@pytest.mark.parametrize("fps", [5.0, 10.0, 24.0, 60.0]) +def test_camera_fps(current_camera: Camera, fps: float) -> None: + current_camera.fps = fps + assert current_camera.fps == fps + + +@pytest.mark.parametrize("aspect_ratio", [0.5, 1, 4]) +def test_camera_pixel_aspect_ratio(current_camera: Camera, aspect_ratio: float) -> None: + current_camera.pixel_aspect_ratio = aspect_ratio + assert current_camera.pixel_aspect_ratio == aspect_ratio + + +@pytest.mark.parametrize("field_order", george.FieldOrder) +def test_camera_field_order(current_camera: Camera, field_order: george.FieldOrder) -> None: + current_camera.field_order = field_order + assert current_camera.field_order == field_order + + +def test_camera_insert_point(current_camera: Camera) -> None: + point = current_camera.insert_point(0, 50, 50, 34, 1.5) + assert next(current_camera.points) == point + + +def test_camera_current_points_data(current_camera: Camera) -> None: + george.tv_camera_insert_point(0, 50, 50, 34, 1.5) + point = george.tv_camera_enum_points(0) + assert next(current_camera.points) == CameraPoint(0, camera=current_camera, data=point) + + +@pytest.mark.skipif( + not IS_NOT_TVP12, + reason="tv_camera_interpolation no longer works properly in TVP 12", +) +def test_camera_get_point_data_at(current_camera: Camera) -> None: + start = current_camera.insert_point(0, 50, 50, 10, 1.2) + end = current_camera.insert_point(1, 10, 5, 48, 0.4) + + assert current_camera.get_point_data_at(0.0).data == start.data + assert current_camera.get_point_data_at(1.0).data == end.data + + +def test_camera_remove_point(current_camera: Camera) -> None: + current_camera.insert_point(0, 50, 50, 34, 1.5) + current_camera.remove_point(0) + assert len(list(current_camera.points)) == 0 diff --git a/tests/test_clip.py b/tests/test_clip.py index bc49a00..eaa865e 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -1,531 +1,529 @@ -# from __future__ import annotations -# -# import random -# from pathlib import Path -# -# import pytest -# from fileseq.filesequence import FileSequence -# -# from pytvpaint import george -# from pytvpaint.clip import Clip -# from pytvpaint.george import RGBColor -# from pytvpaint.layer import Layer, LayerFolder, LayerColor, LayerInstance -# from pytvpaint.project import Project -# from pytvpaint.scene import Scene -# from tests.conftest import FixtureYield -# from tests.george.test_grg_clip import TEST_TEXTS -# -# -# IS_NOT_TVP12 = not george.tv_version()[1].startswith('12') -# -# -# def test_clip_init(test_project_obj: Project, test_clip_obj: Clip) -> None: -# assert Clip(test_clip_obj.id, test_project_obj) == test_clip_obj -# -# -# def test_clip_refresh(test_clip_obj: Clip) -> None: -# test_clip_obj.refresh() -# -# test_clip_obj.remove() -# -# with pytest.raises(ValueError): -# test_clip_obj.refresh() -# -# -# def test_clip_is_current(test_clip_obj: Clip, create_some_clips: list[Clip]) -> None: -# assert not test_clip_obj.is_current -# test_clip_obj.make_current() -# assert test_clip_obj.is_current -# -# -# def test_clip_scene(test_scene_obj: Scene, test_clip_obj: Clip) -> None: -# assert test_clip_obj.scene == test_scene_obj -# -# -# def test_clip_scene_set(create_some_scenes: list[Scene], test_clip_obj: Clip) -> None: -# for scene in create_some_scenes: -# test_clip_obj.scene = scene -# assert test_clip_obj.scene == scene -# -# -# def test_clip_camera(test_clip_obj: Clip) -> None: -# assert test_clip_obj.camera -# -# -# def test_clip_position(create_some_clips: list[Clip]) -> None: -# clip = Clip.current_clip() -# -# for i in range(5): -# clip.position = i -# assert clip.position == i -# -# -# @pytest.mark.parametrize("name", ["l", "0o", "hello world", "_dP-"]) -# def test_clip_name(test_clip_obj: Clip, name: str) -> None: -# test_clip_obj.name = name -# assert test_clip_obj.name == name -# -# -# def test_clip_start(test_project_obj: Project, test_clip_obj: Clip) -> None: -# test_project_obj.start_frame = 5 -# assert test_clip_obj.start == 0 + test_project_obj.start_frame -# -# -# def test_clip_end(test_project_obj: Project, test_clip_obj: Clip) -> None: -# start_frame = 5 -# end_frame = 12 -# test_project_obj.start_frame = start_frame -# -# george.tv_layer_copy() -# george.tv_project_current_frame_set(end_frame - start_frame + 1) -# george.tv_layer_paste() -# -# assert test_clip_obj.end == end_frame -# -# -# @pytest.mark.parametrize( -# "mark_in, mark_out, end, expected", -# [(None, None, 4, 4), (2, None, 4, 3), (None, 7, 4, 7), (2, 6, 19, 5)], -# ) -# def test_clip_duration( -# test_clip_obj: Clip, -# mark_in: int | None, -# mark_out: int | None, -# end: int, -# expected: int, -# ) -> None: -# test_clip_obj.mark_in = mark_in -# test_clip_obj.mark_out = mark_out -# -# layer = test_clip_obj.add_layer("anim") -# layer.convert_to_anim_layer() -# layer.add_instance(end) -# -# assert test_clip_obj.duration == expected -# -# -# def test_clip_frame_count(test_clip_obj: Clip) -> None: -# assert test_clip_obj.frame_count == 1 -# george.tv_layer_insert_image(25, george.InsertDirection.AFTER) -# assert test_clip_obj.frame_count == 26 -# -# -# @pytest.mark.parametrize("index", range(5)) -# def test_clip_is_selected(create_some_clips: list[Clip], index: int) -> None: -# clip = create_some_clips[index] -# clip.is_selected = True -# -# assert clip.is_selected -# assert all(not c.is_selected for c in create_some_clips if c != clip) -# -# -# @pytest.mark.parametrize("index", range(5)) -# def test_clip_is_visible(create_some_clips: list[Clip], index: int) -> None: -# clip = create_some_clips[index] -# clip.is_visible = False -# -# assert not clip.is_visible -# assert all(c.is_visible for c in create_some_clips if c != clip) -# -# -# @pytest.mark.parametrize("index", range(26)) -# def test_clip_color_index(test_clip_obj: Clip, index: int) -> None: -# test_clip_obj.color_index = index -# assert test_clip_obj.color_index == index -# -# -# @pytest.mark.parametrize("text", [TEST_TEXTS[-1]]) -# def test_clip_action_text(test_clip_obj: Clip, text: str) -> None: -# test_clip_obj.action_text = text -# assert test_clip_obj.action_text == text -# -# -# @pytest.mark.parametrize("text", TEST_TEXTS) -# def test_clip_dialog_text(test_clip_obj: Clip, text: str) -> None: -# test_clip_obj.dialog_text = text -# assert test_clip_obj.dialog_text == text -# -# -# @pytest.mark.parametrize("text", TEST_TEXTS) -# def test_clip_note_text(test_clip_obj: Clip, text: str) -> None: -# test_clip_obj.note_text = text -# assert test_clip_obj.note_text == text -# -# -# def test_clip_current_id(test_clip_obj: Clip) -> None: -# assert Clip.current_clip_id() == test_clip_obj.id -# -# -# def test_clip_current_clip(test_clip_obj: Clip) -> None: -# assert Clip.current_clip() == test_clip_obj -# -# -# @pytest.mark.parametrize("frame", range(0, 10, 2)) -# def test_clip_current_frame(test_clip_obj: Clip, frame: int) -> None: -# test_clip_obj.project.start_frame = 5 -# test_clip_obj.current_frame = frame -# assert test_clip_obj.current_frame == frame -# -# -# @pytest.mark.parametrize("name", ["d", "un clip", "tset0"]) -# def test_clip_new(test_project_obj: Project, name: str) -> None: -# clip = Clip.new(name) -# assert clip.name == name -# -# -# def test_clip_new_unique_name(test_project_obj: Project) -> None: -# Clip.new("test") -# assert Clip.new("test").name == "test2" -# -# -# def test_clip_new_other_scene(test_project_obj: Project) -> None: -# first_scene = test_project_obj.current_scene -# other_scene = test_project_obj.add_scene() -# -# first_scene.make_current() -# -# new_clip = Clip.new("hello", scene=other_scene) -# assert new_clip.scene == other_scene -# -# -# def test_clip_duplicate(test_project_obj: Project) -> None: -# clip = Clip.new("test") -# dup = clip.duplicate() -# assert dup.name == "test2" -# -# -# def test_clip_remove(test_project_obj: Project) -> None: -# clip = Clip.new("cl") -# clip.remove() -# -# with pytest.raises(ValueError, match="Clip has been removed"): -# clip.name = "other" -# -# -# def test_clip_layer_ids(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: -# assert list(test_clip_obj.layer_ids) == [layer.id for layer in create_some_layers] -# -# -# def test_clip_layers(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: -# assert list(test_clip_obj.get_layers()) == create_some_layers -# -# -# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") -# def test_clip_layer_folders(test_clip_obj: Clip, create_some_layer_folders: list[LayerFolder]) -> None: -# assert list(test_clip_obj.layer_folders) == create_some_layer_folders -# -# -# def test_clip_current_layer(test_clip_obj: Clip, test_layer_obj: Layer) -> None: -# assert test_clip_obj.current_layer == test_layer_obj -# -# -# def test_clip_add_layer(test_clip_obj: Clip) -> None: -# layer = test_clip_obj.add_layer("test") -# assert test_clip_obj.current_layer == layer and isinstance(layer, Layer) -# -# -# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") -# def test_clip_add_layer_folder(test_clip_obj: Clip) -> None: -# layer = test_clip_obj.add_layer_folder("test") -# assert test_clip_obj.current_layer == layer and isinstance(layer, LayerFolder) -# -# -# def test_clip_selected_layers( -# test_clip_obj: Clip, create_some_layers: list[Layer] -# ) -> None: -# layer = create_some_layers[0] -# layer.is_selected = True -# assert list(test_clip_obj.selected_layers) == [layer] -# -# -# def test_clip_visible_layers( -# test_clip_obj: Clip, create_some_layers: list[Layer] -# ) -> None: -# layer = create_some_layers[0] -# layer.is_visible = False -# assert list(test_clip_obj.visible_layers) == create_some_layers[1:] -# -# -# def test_clip_load_media(test_clip_obj: Clip, ppm_sequence: list[Path]) -> None: -# layer = test_clip_obj.load_media(ppm_sequence[0], with_name="images") -# assert layer.name == "images" -# -# -# @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.0003.png", None, None, "render.0003.png"), -# ], -# ) -# def test_clip_render_single_img( -# test_clip_obj: Clip, -# count_up_generate: list[LayerInstance], -# tmp_path: Path, -# out: str, -# start: int | None, -# end: int | None, -# expected: str, -# ) -> None: -# test_clip_obj.render(tmp_path / out, start, end) -# -# 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_path.exists() -# -# -# @pytest.mark.parametrize("use_camera", [True, False]) -# @pytest.mark.parametrize( -# "out, start, end, expected, error", -# [ -# ("render.0010.png", 2, 5, "render.2-5#.png", None), -# ("render.#.png", 1, 5, "render.1-5#.png", None), -# ("render.png", 2, 7, "", ValueError), -# ("render.#.png", None, None, "render.1-5#.png", None), -# ("render.1-5#.png", None, None, "render.1-5#.png", None), -# ("render.2-4#.png", None, None, "render.2-4#.png", None), -# ("render.1-5#.png", 2, 5, "render.2-5#.png", None), -# ("render.1-5#.png", 2, None, "render.2-5#.png", None), -# ("render.1-5#.png", 2, 4, "render.2-4#.png", None), -# ("render.1-5#.png", 1, 7, "", ValueError), -# ("render.#.png", -6, 7, "", ValueError), -# ("render.1-5#.png", -6, 7, "", ValueError), -# ], -# ) -# def test_clip_render_sequence( -# test_clip_obj: Clip, -# count_up_generate: list[LayerInstance], -# tmp_path: Path, -# use_camera: bool, -# out: str, -# start: int | None, -# end: int | None, -# expected: str, -# error: type[Exception] | None, -# ) -> None: -# 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: -# 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("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, -# count_up_generate: list[LayerInstance], -# tmp_path: Path, -# use_camera: bool, -# out: str, -# start: int | None, -# end: int | None, -# expected: str, -# error: type[Exception] | None, -# ) -> None: -# 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( -# test_project_obj: Project, -# test_clip_obj: Clip, -# with_loaded_sequence: Layer, -# tmp_path: Path, -# ) -> None: -# out_tvp = tmp_path / "out.tvp" -# test_clip_obj.export_tvp(out_tvp) -# -# assert out_tvp.exists() -# -# loaded = test_project_obj.load(out_tvp) -# loaded.close() -# -# -# def test_clip_export_json( -# test_clip_obj: Clip, tmp_path: Path, with_loaded_sequence: Layer -# ) -> None: -# out_json = tmp_path / "out.json" -# -# test_clip_obj.export_json( -# out_json, -# george.SaveFormat.PNG, -# alpha_mode=george.AlphaSaveMode.NO_ALPHA, -# ) -# -# assert out_json.exists() -# -# -# def test_clip_export_psd( -# test_clip_obj: Clip, -# tmp_path: Path, -# with_loaded_sequence: Layer, -# ) -> None: -# out_psd = tmp_path / "out.psd" -# test_clip_obj.export_psd(out_psd, mode=george.PSDSaveMode.ALL) -# assert out_psd.exists() -# -# -# def test_clip_export_csv( -# test_clip_obj: Clip, -# tmp_path: Path, -# with_loaded_sequence: Layer, -# ) -> None: -# out_csv = tmp_path / "out.csv" -# test_clip_obj.export_csv(out_csv, george.SaveFormat.JPG) -# assert out_csv.exists() -# -# -# def test_clip_export_sprites( -# test_clip_obj: Clip, -# tmp_path: Path, -# with_loaded_sequence: Layer, -# ) -> None: -# out_sprite = tmp_path / "out.png" -# test_clip_obj.export_sprites(out_sprite) -# assert out_sprite.exists() -# -# -# def test_clip_export_flix( -# test_clip_obj: Clip, -# tmp_path: Path, -# with_loaded_sequence: Layer, -# ) -> None: -# out_flix = tmp_path / "out.xml" -# test_clip_obj.export_flix(out_flix) -# assert out_flix.exists() -# -# -# @pytest.mark.parametrize("mark_in", [None, 1, 10, 50, 100]) -# def test_clip_mark_in(test_clip_obj: Clip, mark_in: int | None) -> None: -# test_clip_obj.mark_in = mark_in -# assert test_clip_obj.mark_in == mark_in -# -# -# @pytest.mark.parametrize("mark_out", [None, 5, 10, 50, 100]) -# def test_clip_mark_out( -# test_project_obj: Project, test_clip_obj: Clip, mark_out: int | None -# ) -> None: -# test_project_obj.start_frame = 5 -# -# test_clip_obj.mark_out = mark_out -# assert test_clip_obj.mark_out == mark_out -# -# -# def test_clip_layer_colors(test_clip_obj: Clip) -> None: -# assert list(test_clip_obj.layer_colors) -# -# -# @pytest.fixture -# def random_color() -> RGBColor: -# return RGBColor( -# random.randint(0, 255), -# random.randint(0, 255), -# random.randint(0, 255), -# ) -# -# -# @pytest.mark.parametrize("index", range(1, 26)) -# def test_clip_set_layer_color( -# test_clip_obj: Clip, index: int, random_color: RGBColor -# ) -> None: -# expected = LayerColor(index, test_clip_obj) -# expected.color = random_color -# expected.name = "test" -# -# test_clip_obj.set_layer_color(expected) -# -# result = test_clip_obj.get_layer_color(by_index=index) -# assert result is not None -# assert result.name == "test" -# assert result.color == random_color -# -# -# @pytest.fixture -# def create_some_bookmarks(test_clip_obj: Clip) -> FixtureYield[list[int]]: -# bookmarks = [1, 50, 20, 34] -# for mark in bookmarks: -# test_clip_obj.add_bookmark(mark) -# yield sorted(bookmarks) -# -# -# def test_clip_bookmarks(test_clip_obj: Clip, create_some_bookmarks: list[int]) -> None: -# assert list(test_clip_obj.bookmarks) == create_some_bookmarks -# -# -# @pytest.mark.parametrize("mark", [1, 20, 50]) -# def test_clip_add_bookmark(test_clip_obj: Clip, mark: int) -> None: -# test_clip_obj.add_bookmark(mark) -# assert list(test_clip_obj.bookmarks) == [mark] -# -# -# @pytest.mark.parametrize("mark", [1, 20, 50]) -# def test_clip_remove_bookmark(test_clip_obj: Clip, mark: int) -> None: -# test_clip_obj.add_bookmark(mark) -# test_clip_obj.remove_bookmark(mark) -# assert list(test_clip_obj.bookmarks) == [] -# -# -# def test_clip_clear_bookmarks( -# test_clip_obj: Clip, create_some_bookmarks: list[int] -# ) -> None: -# test_clip_obj.clear_bookmarks() -# assert list(test_clip_obj.bookmarks) == [] -# -# -# def test_clip_go_to_previous_bookmark( -# test_clip_obj: Clip, create_some_bookmarks: list[int] -# ) -> None: -# for mark in reversed(create_some_bookmarks): -# test_clip_obj.go_to_previous_bookmark() -# assert test_clip_obj.current_frame == mark -# -# -# def test_clip_go_to_next_bookmark( -# test_clip_obj: Clip, create_some_bookmarks: list[int] -# ) -> None: -# test_clip_obj.current_frame = 0 -# -# for mark in create_some_bookmarks: -# test_clip_obj.go_to_next_bookmark() -# assert test_clip_obj.current_frame == mark -# -# -# def test_clip_sounds(test_clip_obj: Clip, wav_file: Path) -> None: -# sound = test_clip_obj.add_sound(wav_file) -# assert list(test_clip_obj.sounds) == [sound] +from __future__ import annotations + +import random +from pathlib import Path + +import pytest +from fileseq.filesequence import FileSequence +from fileseq.frameset import FrameSet + +from pytvpaint import george +from pytvpaint.clip import Clip +from pytvpaint.layer import Layer, LayerColor, LayerFolder, LayerInstance +from pytvpaint.project import Project +from pytvpaint.scene import Scene +from tests.conftest import FixtureYield + +IS_NOT_TVP12 = not george.tv_version()[1].startswith("12") + + +def test_clip_init(test_project_obj: Project, test_clip_obj: Clip) -> None: + assert Clip(test_clip_obj.id, test_project_obj) == test_clip_obj + + +def test_clip_refresh(test_clip_obj: Clip) -> None: + test_clip_obj.refresh() + + test_clip_obj.remove() + + with pytest.raises(ValueError): + test_clip_obj.refresh() + + +def test_clip_is_current(test_clip_obj: Clip, create_some_clips: list[Clip]) -> None: + assert not test_clip_obj.is_current + test_clip_obj.make_current() + assert test_clip_obj.is_current + + +def test_clip_scene(test_scene_obj: Scene, test_clip_obj: Clip) -> None: + assert test_clip_obj.scene == test_scene_obj + + +def test_clip_scene_set(create_some_scenes: list[Scene], test_clip_obj: Clip) -> None: + for scene in create_some_scenes: + test_clip_obj.scene = scene + assert test_clip_obj.scene == scene + + +def test_clip_camera(test_clip_obj: Clip) -> None: + assert test_clip_obj.camera + + +def test_clip_position(create_some_clips: list[Clip]) -> None: + clip = Clip.current_clip() + + for i in range(5): + clip.position = i + assert clip.position == i + + +@pytest.mark.parametrize("name", ["l", "0o", "hello world", "_dP-"]) +def test_clip_name(test_clip_obj: Clip, name: str) -> None: + test_clip_obj.name = name + assert test_clip_obj.name == name + + +def test_clip_start(test_project_obj: Project, test_clip_obj: Clip) -> None: + test_project_obj.start_frame = 5 + assert test_clip_obj.start == 0 + test_project_obj.start_frame + + +def test_clip_end(test_project_obj: Project, test_clip_obj: Clip) -> None: + start_frame = 5 + end_frame = 12 + test_project_obj.start_frame = start_frame + + george.tv_layer_copy() + george.tv_project_current_frame_set(end_frame - start_frame + 1) + george.tv_layer_paste() + + assert test_clip_obj.end == end_frame + + +@pytest.mark.parametrize( + "mark_in, mark_out, end, expected", + [(None, None, 4, 4), (2, None, 4, 3), (None, 7, 4, 7), (2, 6, 19, 5)], +) +def test_clip_duration( + test_clip_obj: Clip, + mark_in: int | None, + mark_out: int | None, + end: int, + expected: int, +) -> None: + test_clip_obj.mark_in = mark_in + test_clip_obj.mark_out = mark_out + + layer = test_clip_obj.add_layer("anim") + layer.convert_to_anim_layer() + layer.add_instance(end) + + assert test_clip_obj.duration == expected + + +def test_clip_frame_count(test_clip_obj: Clip) -> None: + assert test_clip_obj.frame_count == 1 + george.tv_layer_insert_image(25, george.InsertDirection.AFTER) + assert test_clip_obj.frame_count == 26 + + +@pytest.mark.parametrize("index", range(5)) +def test_clip_is_selected(create_some_clips: list[Clip], index: int) -> None: + clip = create_some_clips[index] + clip.is_selected = True + + assert clip.is_selected + assert all(not c.is_selected for c in create_some_clips if c != clip) + + +@pytest.mark.parametrize("index", range(5)) +def test_clip_is_visible(create_some_clips: list[Clip], index: int) -> None: + clip = create_some_clips[index] + clip.is_visible = False + + assert not clip.is_visible + assert all(c.is_visible for c in create_some_clips if c != clip) + + +@pytest.mark.parametrize("index", range(26)) +def test_clip_color_index(test_clip_obj: Clip, index: int) -> None: + test_clip_obj.color_index = index + assert test_clip_obj.color_index == index + + +TEST_TEXTS = ["", "l", "0", "ab", "a0l", "ap*", "a\nb"] + + +@pytest.mark.parametrize("text", TEST_TEXTS) +def test_clip_action_text(test_clip_obj: Clip, text: str) -> None: + test_clip_obj.action_text = text + assert test_clip_obj.action_text == text + + +@pytest.mark.parametrize("text", TEST_TEXTS) +def test_clip_dialog_text(test_clip_obj: Clip, text: str) -> None: + test_clip_obj.dialog_text = text + assert test_clip_obj.dialog_text == text + + +@pytest.mark.parametrize("text", TEST_TEXTS) +def test_clip_note_text(test_clip_obj: Clip, text: str) -> None: + test_clip_obj.note_text = text + assert test_clip_obj.note_text == text + + +def test_clip_current_id(test_clip_obj: Clip) -> None: + assert Clip.current_clip_id() == test_clip_obj.id + + +def test_clip_current_clip(test_clip_obj: Clip) -> None: + assert Clip.current_clip() == test_clip_obj + + +@pytest.mark.parametrize("frame", range(0, 10, 2)) +def test_clip_current_frame(test_clip_obj: Clip, frame: int) -> None: + test_clip_obj.project.start_frame = 0 + test_clip_obj.current_frame = frame + assert test_clip_obj.current_frame == frame + + +@pytest.mark.parametrize("name", ["d", "un clip", "test0"]) +def test_clip_new(test_project_obj: Project, name: str) -> None: + clip = Clip.new(name) + assert clip.name == name + + +def test_clip_new_unique_name(test_project_obj: Project) -> None: + Clip.new("test") + assert Clip.new("test").name == "test2" + + +def test_clip_new_other_scene(test_project_obj: Project) -> None: + first_scene = test_project_obj.current_scene + other_scene = test_project_obj.add_scene() + + first_scene.make_current() + + new_clip = Clip.new("hello", scene=other_scene) + assert new_clip.scene == other_scene + + +def test_clip_duplicate(test_project_obj: Project) -> None: + clip = Clip.new("test") + dup = clip.duplicate() + assert dup.name == "test2" + + +def test_clip_remove(test_project_obj: Project) -> None: + clip = Clip.new("cl") + clip.remove() + + with pytest.raises(ValueError, match="Clip has been removed"): + clip.name = "other" + + +def test_clip_layer_ids(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: + assert list(test_clip_obj.layer_ids) == [layer.id for layer in create_some_layers] + + +def test_clip_layers(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: + assert list(test_clip_obj.get_layers()) == create_some_layers + + +@pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +def test_clip_layer_folders(test_clip_obj: Clip, create_some_layer_folders: list[LayerFolder]) -> None: + assert list(test_clip_obj.folders) == create_some_layer_folders + + +def test_clip_current_layer(test_clip_obj: Clip, test_layer_obj: Layer) -> None: + assert test_clip_obj.current_layer == test_layer_obj + + +def test_clip_add_layer(test_clip_obj: Clip) -> None: + layer = test_clip_obj.add_layer("test") + assert test_clip_obj.current_layer == layer and isinstance(layer, Layer) + + +@pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +def test_clip_add_layer_folder(test_clip_obj: Clip) -> None: + layer = test_clip_obj.add_layer_folder("test") + assert test_clip_obj.current_layer == layer and isinstance(layer, LayerFolder) + + +def test_clip_selected_layers(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: + layer = create_some_layers[0] + layer.is_selected = True + assert list(test_clip_obj.selected_layers) == [layer] + + +def test_clip_visible_layers(test_clip_obj: Clip, create_some_layers: list[Layer]) -> None: + layer = create_some_layers[0] + layer.is_visible = False + assert list(test_clip_obj.visible_layers) == create_some_layers[1:] + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerRename` does not currently work in TVP12.") +def test_clip_load_media(test_clip_obj: Clip, png_sequence: list[Path]) -> None: + layer = test_clip_obj.load_media(png_sequence[0], with_name="images") + assert layer.name == "images" + + +@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.0003.png", None, None, "render.0003.png"), + ], +) +def test_clip_render_single_img( + test_clip_obj: Clip, + count_up_generate: list[LayerInstance], + tmp_path: Path, + out: str, + start: int | None, + end: int | None, + expected: str, +) -> None: + test_clip_obj.render(tmp_path / out, start, end) + + 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_path.exists() + + +@pytest.mark.parametrize("use_camera", [True, False]) +@pytest.mark.parametrize( + "out, start, end, frame_set, expected, error", + [ + ("render.0010.png", 2, 5, None, "render.2-5#.png", None), + ("render.#.png", 1, 5, None, "render.1-5#.png", None), + ("render.png", 2, 7, None, "", ValueError), + ("render.#.png", None, None, None, "render.1-5#.png", None), + ("render.1-5#.png", None, None, None, "render.1-5#.png", None), + ("render.2-4#.png", None, None, None, "render.2-4#.png", None), + ("render.1-5#.png", 2, 5, None, "render.2-5#.png", None), + ("render.1-5#.png", 2, None, None, "render.2-5#.png", None), + ("render.1-5#.png", 2, 4, None, "render.2-4#.png", None), + ("render.1-5#.png", 1, 7, None, "", ValueError), + ("render.#.png", -6, 7, None, "", ValueError), + ("render.1-5#.png", -6, 7, None, "", ValueError), + ("render.#.png", 1, 5, FrameSet("1-5"), "render.1-5#.png", None), + ("render.#.png", 1, 5, FrameSet("1-3"), "render.1-3#.png", None), + ("render.#.png", 1, 5, FrameSet([1, 3, 5]), "render.1-5x2#.png", None), + ("render.#.png", 1, 5, FrameSet([1, 3, 4]), "render.1,3-4#.png", None), + ("render.#.png", None, None, FrameSet([1, 3, 5]), "render.1-5x2#.png", None), + ], +) +def test_clip_render_sequence( + test_clip_obj: Clip, + count_up_generate: list[LayerInstance], + tmp_path: Path, + use_camera: bool, + out: str, + start: int | None, + end: int | None, + frame_set: FrameSet | None, + expected: str, + error: type[Exception] | None, +) -> None: + if error: + with pytest.raises(error): + test_clip_obj.render(tmp_path / out, start, end, frame_set, use_camera=use_camera) + else: + test_clip_obj.render(tmp_path / out, start, end, frame_set, use_camera=use_camera) + + if expected: + 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("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, + count_up_generate: list[LayerInstance], + tmp_path: Path, + use_camera: bool, + out: str, + start: int | None, + end: int | None, + expected: str, + error: type[Exception] | None, +) -> None: + 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( + test_project_obj: Project, + test_clip_obj: Clip, + with_loaded_sequence: Layer, + tmp_path: Path, +) -> None: + out_tvp = tmp_path / "out.tvp" + test_clip_obj.export_tvp(out_tvp) + + assert out_tvp.exists() + + loaded = test_project_obj.load(out_tvp) + loaded.close() + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="Skip since layer naming doesn't work in TVP12.") +def test_clip_export_json(test_clip_obj: Clip, tmp_path: Path, with_loaded_sequence: Layer) -> None: + out_json = tmp_path / "out.json" + + test_clip_obj.export_json( + out_json, + george.SaveFormat.PNG, + alpha_mode=george.AlphaSaveMode.NO_ALPHA, + ) + + assert out_json.exists() + + +def test_clip_export_psd( + test_clip_obj: Clip, + tmp_path: Path, + with_loaded_sequence: Layer, +) -> None: + out_psd = tmp_path / "out.psd" + test_clip_obj.export_psd(out_psd, mode=george.PSDSaveMode.ALL) + assert out_psd.exists() + + +def test_clip_export_csv( + test_clip_obj: Clip, + tmp_path: Path, + with_loaded_sequence: Layer, +) -> None: + out_csv = tmp_path / "out.csv" + test_clip_obj.export_csv(out_csv, george.SaveFormat.JPG) + assert out_csv.exists() + + +def test_clip_export_sprites( + test_clip_obj: Clip, + tmp_path: Path, + with_loaded_sequence: Layer, +) -> None: + out_sprite = tmp_path / "out.png" + test_clip_obj.export_sprites(out_sprite) + assert out_sprite.exists() + + +def test_clip_export_flix( + test_clip_obj: Clip, + tmp_path: Path, + with_loaded_sequence: Layer, +) -> None: + out_flix = tmp_path / "out.xml" + test_clip_obj.export_flix(out_flix) + assert out_flix.exists() + + +@pytest.mark.parametrize("mark_in", [None, 1, 10, 50, 100]) +def test_clip_mark_in(test_clip_obj: Clip, mark_in: int | None) -> None: + test_clip_obj.mark_in = mark_in + assert test_clip_obj.mark_in == mark_in + + +@pytest.mark.parametrize("mark_out", [None, 5, 10, 50, 100]) +def test_clip_mark_out(test_project_obj: Project, test_clip_obj: Clip, mark_out: int | None) -> None: + test_project_obj.start_frame = 5 + + test_clip_obj.mark_out = mark_out + assert test_clip_obj.mark_out == mark_out + + +def test_clip_layer_colors(test_clip_obj: Clip) -> None: + assert list(test_clip_obj.layer_colors) + + +@pytest.fixture +def random_color() -> george.RGBColor: + return george.RGBColor( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 255), + ) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_LayerColor setcolor` does not work when a name is provided.") +@pytest.mark.parametrize("index", range(1, 26)) +def test_clip_set_layer_color(test_clip_obj: Clip, index: int, random_color: george.RGBColor) -> None: + expected = LayerColor(index, test_clip_obj) + expected.color = random_color + expected.name = "test" + + test_clip_obj.set_layer_color(expected) + + result = test_clip_obj.get_layer_color(by_index=index) + assert result is not None + assert result.name == "test" + assert result.color == random_color + + +@pytest.fixture +def create_some_bookmarks(test_clip_obj: Clip) -> FixtureYield[list[int]]: + bookmarks = [2, 50, 20, 34] + for mark in bookmarks: + test_clip_obj.add_bookmark(mark) + yield sorted(bookmarks) + + +def test_clip_bookmarks(test_clip_obj: Clip, create_some_bookmarks: list[int]) -> None: + assert list(test_clip_obj.bookmarks) == create_some_bookmarks + + +@pytest.mark.parametrize("mark", [1, 20, 50]) +def test_clip_add_bookmark(test_clip_obj: Clip, mark: int) -> None: + test_clip_obj.add_bookmark(mark) + assert list(test_clip_obj.bookmarks) == [mark] + + +@pytest.mark.parametrize("mark", [1, 20, 50]) +def test_clip_remove_bookmark(test_clip_obj: Clip, mark: int) -> None: + test_clip_obj.add_bookmark(mark) + test_clip_obj.remove_bookmark(mark) + assert list(test_clip_obj.bookmarks) == [] + + +def test_clip_clear_bookmarks(test_clip_obj: Clip, create_some_bookmarks: list[int]) -> None: + test_clip_obj.clear_bookmarks() + assert list(test_clip_obj.bookmarks) == [] + + +def test_clip_go_to_previous_bookmark(test_clip_obj: Clip) -> None: + bookmarks = [1, 50, 20, 34] + for mark in bookmarks: + test_clip_obj.add_bookmark(mark) + + for mark in reversed(sorted(bookmarks)): + test_clip_obj.go_to_previous_bookmark() + assert test_clip_obj.current_frame == mark + + +def test_clip_go_to_next_bookmark(test_clip_obj: Clip, create_some_bookmarks: list[int]) -> None: + test_clip_obj.current_frame = 0 + + bookmarks = [2, 50, 20, 34] + for mark in bookmarks: + test_clip_obj.add_bookmark(mark) + + for mark in create_some_bookmarks: + test_clip_obj.go_to_next_bookmark() + assert test_clip_obj.current_frame == mark + + +def test_clip_sounds(test_clip_obj: Clip, wav_file: Path) -> None: + sound = test_clip_obj.add_sound(wav_file) + assert list(test_clip_obj.sounds) == [sound] diff --git a/tests/test_clip_sound.py b/tests/test_clip_sound.py index 067b03c..fb3f178 100644 --- a/tests/test_clip_sound.py +++ b/tests/test_clip_sound.py @@ -1,21 +1,21 @@ -# import pytest -# -# from pytvpaint.george.exceptions import GeorgeError -# from pytvpaint.sound import ClipSound -# -# -# def test_clip_sound_init(test_clip_sound: ClipSound) -> None: -# assert ClipSound(test_clip_sound.track_index) == test_clip_sound -# -# -# def test_clip_sound_remove(test_clip_sound: ClipSound) -> None: -# index = test_clip_sound.track_index -# test_clip_sound.remove() -# -# with pytest.raises(GeorgeError): -# ClipSound(track_index=index) -# -# -# @pytest.mark.skip("tv_sound_clip_reload doesn't work properly") -# def test_clip_sound_reload(test_clip_sound: ClipSound) -> None: -# test_clip_sound.reload() +import pytest + +from pytvpaint.george.exceptions import GeorgeError +from pytvpaint.sound import ClipSound + + +def test_clip_sound_init(test_clip_sound: ClipSound) -> None: + assert ClipSound(test_clip_sound.track_index) == test_clip_sound + + +def test_clip_sound_remove(test_clip_sound: ClipSound) -> None: + index = test_clip_sound.track_index + test_clip_sound.remove() + + with pytest.raises(GeorgeError): + ClipSound(track_index=index) + + +@pytest.mark.skip("tv_sound_clip_reload doesn't work properly") +def test_clip_sound_reload(test_clip_sound: ClipSound) -> None: + test_clip_sound.reload() diff --git a/tests/test_guideline.py b/tests/test_guideline.py new file mode 100644 index 0000000..0fa5bc6 --- /dev/null +++ b/tests/test_guideline.py @@ -0,0 +1,190 @@ +import contextlib +from collections.abc import Generator +from pathlib import Path + +import pytest + +from pytvpaint import george, guideline +from pytvpaint.project import Project + + +@pytest.fixture(autouse=True) +def clean_guidelines() -> Generator[None, None, None]: + """Wipe all guidelines before and after each test.""" + + def _remove_all() -> None: + for g_type in george.GuidelineType: + with contextlib.suppress(Exception): + george.tv_guideline_remove_all(g_type) + + _remove_all() + yield + _remove_all() + + +def test_guideline_base_properties(test_project_obj: Project) -> None: + """Test the common properties inherited from the base Guideline class.""" + guideline_obj = guideline.GuidelineLine.new(test_project_obj, x=10, y=10, angle=45) + + assert guideline_obj.position >= 0 + assert guideline_obj.project == test_project_obj + + # Test Name + guideline_obj.name = "test_line" + assert guideline_obj.name == "test_line" + + # Test Visibility + guideline_obj.is_visible = False + assert guideline_obj.is_visible is False + + # Test Color + test_color = george.RGBAColor(10, 20, 30, 255) + guideline_obj.color = test_color + assert guideline_obj.color == test_color + + # Test Snap + with pytest.raises(ValueError): + guideline_obj.snap = True + + guideline_obj.is_visible = True + guideline_obj.snap = True + assert guideline_obj.snap is True + + # Test Snap + + # Test Collapse + guideline_obj.collapse = True + assert guideline_obj.collapse is True + + # Test Remove / Removable mixin + assert not guideline_obj.is_removed + guideline_obj.remove() + assert guideline_obj.is_removed + + +def test_guideline_image(test_project_obj: Project, png_sequence: list[Path]) -> None: + """Test GuidelineImage specific properties.""" + # Grab the first image from the sequence fixture + test_img = png_sequence[0] + + guideline_obj = guideline.GuidelineImage.new( + test_project_obj, img_path=test_img, x=10, y=20, rotation=45, scale=2.0, flip=george.FlipDirection.X + ) + + assert guideline_obj.path == test_img + assert guideline_obj.x == 10 + assert guideline_obj.flip == george.FlipDirection.X + + # Modify property and verify + guideline_obj.rotation = 90 + assert guideline_obj.rotation == 90 + guideline_obj.scale = 1.5 + assert guideline_obj.scale == 1.5 + + +def test_guideline_line(test_project_obj: Project) -> None: + """Test GuidelineLine specific properties.""" + guideline_obj = guideline.GuidelineLine.new(test_project_obj, x=50, y=60, angle=15.5) + assert guideline_obj.x == 50 + assert guideline_obj.y == 60 + assert guideline_obj.angle == 15.5 + + guideline_obj.angle = 180 + assert guideline_obj.angle == 180 + + +def test_guideline_segment(test_project_obj: Project) -> None: + """Test GuidelineSegment specific properties.""" + guideline_obj = guideline.GuidelineSegment.new(test_project_obj, x1=10, y1=10, x2=100, y2=100) + assert guideline_obj.x1 == 10 + assert guideline_obj.x2 == 100 + + guideline_obj.y2 = 200 + assert guideline_obj.y2 == 200 + + +def test_guideline_circle(test_project_obj: Project) -> None: + """Test GuidelineCircle specific properties.""" + guideline_obj = guideline.GuidelineCircle.new(test_project_obj, x=200, y=200, radius=50) + assert guideline_obj.radius == 50 + + guideline_obj.radius = 75 + assert guideline_obj.radius == 75 + + +def test_guideline_ellipse(test_project_obj: Project) -> None: + """Test GuidelineEllipse specific properties.""" + guideline_obj = guideline.GuidelineEllipse.new(test_project_obj, x=100, y=100, radius_a=50, radius_b=25) + assert guideline_obj.radius_a == 50 + assert guideline_obj.radius_b == 25 + + guideline_obj.radius_b = 30 + assert guideline_obj.radius_b == 30 + + +def test_guideline_grid(test_project_obj: Project) -> None: + """Test GuidelineGrid specific properties.""" + guideline_obj = guideline.GuidelineGrid.new(test_project_obj, x=0, y=0, width=1920, height=1080) + assert guideline_obj.width == 1920 + assert guideline_obj.height == 1080 + + guideline_obj.width = 1280 + assert guideline_obj.width == 1280 + + +def test_guideline_marks(test_project_obj: Project) -> None: + """Test GuidelineMarks specific properties.""" + guideline_obj = guideline.GuidelineMarks.new(test_project_obj, count_x=4, count_y=3) + assert guideline_obj.count_x == 4 + + guideline_obj.count_y = 5 + + +def test_guideline_safe_area(test_project_obj: Project) -> None: + """Test GuidelineSafeArea specific properties.""" + guideline_obj = guideline.GuidelineSafeArea.new(test_project_obj, sf_out=10, sf_in=20) + assert guideline_obj.sf_out == 10 + + guideline_obj.sf_in = 25 + assert guideline_obj.sf_in == 25 + + +def test_guideline_vanish_point_1(test_project_obj: Project) -> None: + """Test GuidelineVanishPoint1 specific properties.""" + guideline_obj = guideline.GuidelineVanishPoint1.new(test_project_obj, x=100, y=100, grid=True) + assert guideline_obj.grid is True + + guideline_obj.grid = False + assert guideline_obj.grid is False + + +def test_guideline_vanish_point_2(test_project_obj: Project) -> None: + """Test GuidelineVanishPoint2 specific properties.""" + guideline_obj = guideline.GuidelineVanishPoint2.new(test_project_obj, x1=10, y1=10, x2=100, y2=100) + assert guideline_obj.x2 == 100 + + guideline_obj.x2 = 150 + assert guideline_obj.x2 == 150 + + +def test_guideline_vanish_point_3(test_project_obj: Project) -> None: + """Test GuidelineVanishPoint3 specific properties.""" + guideline_obj = guideline.GuidelineVanishPoint3.new(test_project_obj, x1=10, y1=10, x2=100, y2=100, x3=50, y3=50) + assert guideline_obj.x3 == 50 + assert guideline_obj.y3 == 50 + + guideline_obj.y3 = 60 + assert guideline_obj.y3 == 60 + + +def test_guideline_charts(test_project_obj: Project) -> None: + """Test chart guidelines (which generally don't have modify properties).""" + # Field Chart + g_field = guideline.GuidelineFieldChart.new(test_project_obj) + assert g_field.position >= 0 + assert g_field.TYPE == george.GuidelineType.FIELD_CHART + + # Animator Field + g_anim = guideline.GuidelineAnimatorField.new(test_project_obj) + assert g_anim.position >= 0 + assert g_anim.TYPE == george.GuidelineType.ANIMATOR_FIELD diff --git a/tests/test_layer.py b/tests/test_layer.py index 700da80..6c5ea17 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -1,372 +1,390 @@ -# from __future__ import annotations -# -# from pathlib import Path -# -# import pytest -# -# from pytvpaint import george -# from pytvpaint.clip import Clip -# from pytvpaint.george import BlendingMode, StencilMode -# from pytvpaint.george.grg_layer import ( -# LayerBehavior, -# LayerTransparency, -# LayerType, -# TVPLayer, -# ) -# from pytvpaint.layer import Layer, LayerFolder, LayerColor, LayerInstance -# from pytvpaint.project import Project -# from pytvpaint.scene import Scene -# from tests.conftest import FixtureYield -# -# -# IS_NOT_TVP12 = not george.tv_version()[1].startswith('12') -# -# -# def test_layer_init(test_layer_obj: Layer) -> None: -# assert Layer(test_layer_obj.id) == test_layer_obj -# -# -# def test_layer_refresh(test_layer_obj: Layer) -> None: -# test_layer_obj.refresh() -# -# -# def test_layer_attributes( -# test_project_obj: Project, -# test_scene_obj: Scene, -# test_clip_obj: Clip, -# test_layer_obj: Layer, -# ) -> None: -# assert test_layer_obj.id -# -# assert test_layer_obj.project == test_project_obj -# assert test_layer_obj.scene == test_scene_obj -# assert test_layer_obj.clip == test_clip_obj -# -# assert test_layer_obj.layer_type -# -# -# @pytest.mark.parametrize("position", range(5)) -# def test_layer_position( -# test_layer_obj: Layer, -# create_some_layers: None, -# position: int, -# ) -> None: -# test_layer_obj.position = position -# assert test_layer_obj.position == position -# -# -# @pytest.mark.parametrize("name", ["l", "a n", "a0", "8 _d"]) -# def test_layer_name(test_layer_obj: Layer, name: str) -> None: -# test_layer_obj.name = name -# assert test_layer_obj.name == name -# -# -# @pytest.mark.parametrize("opacity", [1, 50, 24, 100]) -# def test_layer_opacity(test_layer_obj: Layer, opacity: int) -> None: -# test_layer_obj.opacity = opacity -# assert test_layer_obj.opacity == opacity -# -# -# def test_layer_start(test_layer_obj: Layer) -> None: -# test_layer_obj.start -# -# -# def test_layer_end(test_layer_obj: Layer) -> None: -# test_layer_obj.end -# -# -# @pytest.mark.parametrize("color_index", range(1, 26, 4)) -# def test_layer_color(test_layer_obj: Layer, color_index: int) -> None: -# color = LayerColor(color_index) -# test_layer_obj.color = color -# assert test_layer_obj.color == color -# -# -# def test_layer_is_current( -# test_layer_obj: Layer, -# create_some_layers: list[Layer], -# ) -> None: -# assert not test_layer_obj.is_current -# -# test_layer_obj.make_current() -# assert test_layer_obj.is_current -# -# -# def test_layer_is_selected(test_layer_obj: Layer) -> None: -# assert not test_layer_obj.is_selected -# -# test_layer_obj.is_selected = True -# assert test_layer_obj.is_selected -# -# test_layer_obj.is_selected = False -# assert not test_layer_obj.is_selected -# -# -# def test_layer_is_visible(test_layer_obj: Layer) -> None: -# assert test_layer_obj.is_visible -# -# test_layer_obj.is_visible = False -# assert not test_layer_obj.is_visible -# -# test_layer_obj.is_visible = True -# assert test_layer_obj.is_visible -# -# -# def test_layer_is_locked(test_layer_obj: Layer) -> None: -# assert not test_layer_obj.is_locked -# -# test_layer_obj.is_locked = True -# assert test_layer_obj.is_locked -# -# test_layer_obj.is_locked = False -# assert not test_layer_obj.is_locked -# -# -# def test_layer_is_collapsed(test_layer_obj: Layer) -> None: -# assert not test_layer_obj.is_collapsed -# -# test_layer_obj.is_collapsed = True -# assert test_layer_obj.is_collapsed -# -# test_layer_obj.is_collapsed = False -# assert not test_layer_obj.is_collapsed -# -# -# @pytest.mark.parametrize("blending_mode", BlendingMode) -# def test_layer_blending_mode( -# test_layer_obj: Layer, -# blending_mode: BlendingMode, -# ) -> None: -# test_layer_obj.blending_mode = blending_mode -# assert test_layer_obj.blending_mode == blending_mode -# -# -# @pytest.mark.parametrize("stencil", StencilMode) -# def test_layer_stencil( -# test_layer_obj: Layer, -# stencil: StencilMode, -# ) -> None: -# test_layer_obj.stencil = stencil -# -# current = test_layer_obj.stencil -# if stencil == StencilMode.ON: -# assert current == StencilMode.NORMAL -# else: -# assert current == stencil -# -# -# @pytest.mark.parametrize("visible", [False, True]) -# def test_layer_thumbnails_visible(test_layer_obj: Layer, visible: bool) -> None: -# test_layer_obj.thumbnails_visible = visible -# assert test_layer_obj.thumbnails_visible == visible -# -# -# @pytest.mark.parametrize("value", [False, True]) -# def test_layer_auto_break_instance(test_anim_layer_obj: Layer, value: bool) -> None: -# test_anim_layer_obj.auto_break_instance = value -# assert test_anim_layer_obj.auto_break_instance == value -# -# -# def test_layer_auto_break_instance_not_anim_layer(test_layer_obj: Layer) -> None: -# with pytest.raises(Exception, match="it's not an animation layer"): -# test_layer_obj.auto_break_instance = True -# -# -# @pytest.mark.parametrize("value", [False, True]) -# def test_layer_auto_create_instance(test_layer_obj: Layer, value: bool) -> None: -# test_layer_obj.auto_create_instance = value -# assert test_layer_obj.auto_create_instance == value -# -# -# @pytest.mark.parametrize("behavior", LayerBehavior) -# def test_layer_pre_behavior(test_layer_obj: Layer, behavior: LayerBehavior) -> None: -# test_layer_obj.pre_behavior = behavior -# assert test_layer_obj.pre_behavior == behavior -# -# -# @pytest.mark.parametrize("behavior", LayerBehavior) -# def test_layer_post_behavior(test_layer_obj: Layer, behavior: LayerBehavior) -> None: -# test_layer_obj.post_behavior = behavior -# assert test_layer_obj.post_behavior == behavior -# -# -# @pytest.mark.parametrize("value", [False, True]) -# def test_layer_is_position_locked(test_layer_obj: Layer, value: bool) -> None: -# test_layer_obj.is_position_locked = value -# assert test_layer_obj.is_position_locked == value -# -# -# @pytest.mark.parametrize("transparency", LayerTransparency) -# def test_layer_preserve_transparency( -# test_layer_obj: Layer, transparency: LayerTransparency -# ) -> None: -# test_layer_obj.preserve_transparency = transparency -# -# transparency_map = { -# LayerTransparency.MINUS_1: LayerTransparency.ON, -# LayerTransparency.NONE: LayerTransparency.OFF, -# } -# -# current = test_layer_obj.preserve_transparency -# assert transparency_map.get(transparency, transparency) == current -# -# -# def test_layer_convert_to_anim_layer(test_layer_obj: Layer) -> None: -# test_layer_obj.convert_to_anim_layer() -# assert test_layer_obj.layer_type == LayerType.SEQUENCE -# -# -# def test_layer_load_dependencies(test_layer_obj: Layer) -> None: -# test_layer_obj.load_dependencies() -# -# -# def test_layer_current_layer_id(test_layer_obj: Layer) -> None: -# assert Layer.current_layer_id() == test_layer_obj.id -# -# -# def test_layer_current_layer(test_layer_obj: Layer) -> None: -# assert Layer.current_layer() == test_layer_obj -# -# -# @pytest.mark.parametrize("start", [0, 5, 10]) -# def test_layer_shift(test_layer_obj: Layer, start: int) -> None: -# test_layer_obj.shift(start) -# -# -# @pytest.mark.parametrize("name", ["l", "loid", "K4", "k 1"]) -# @pytest.mark.parametrize("color_index", [None, 5]) -# def test_layer_new( -# test_clip_obj: Clip, -# name: str, -# color_index: int | None, -# ) -> None: -# color = LayerColor(color_index) if color_index else None -# layer = Layer.new(name, color=color) -# assert layer.name == name -# -# if color_index: -# assert layer.color == LayerColor(color_index) -# -# -# def test_layer_new_anim_layer(test_clip_obj: Clip) -> None: -# layer = Layer.new_anim_layer("anim_layer") -# assert layer.layer_type == LayerType.SEQUENCE -# assert layer.thumbnails_visible -# -# -# def test_layer_new_background_layer(test_clip_obj: Clip) -> None: -# layer = Layer.new_background_layer("background_layer") -# assert layer.layer_type == LayerType.IMAGE -# assert layer.thumbnails_visible -# assert layer.pre_behavior == LayerBehavior.HOLD -# assert layer.post_behavior == LayerBehavior.HOLD -# -# -# @pytest.mark.parametrize("name", ["l", "loid", "K4", "k 1"]) -# def test_layer_duplicate(test_layer_obj: Layer, name: str) -> None: -# dup = test_layer_obj.duplicate(name) -# assert dup.name == name -# -# -# def test_layer_remove(test_clip_obj: Clip) -> None: -# layer = Layer.new("remove") -# layer.remove() -# -# with pytest.raises(ValueError, match="Layer has been removed"): -# layer.name = "other" -# -# -# @pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") -# def test_layer_folder_remove(test_clip_obj: Clip) -> None: -# layer = LayerFolder.new("remove") -# layer.remove() -# -# with pytest.raises(ValueError, match="LayerFolder has been removed"): -# 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]) -# -# -# def test_layer_render_frame(with_loaded_sequence: Layer, tmp_path: Path) -> None: -# with_loaded_sequence.render_frame(tmp_path / "out.jpg", frame=3) -# -# -# def test_layer_add_mark_not_anim_layer(test_layer_obj: Layer) -> None: -# with pytest.raises(Exception, match="not an animation layer"): -# test_layer_obj.add_mark(0, LayerColor(1)) -# -# -# def test_layer_get_mark_color(test_anim_layer_obj: Layer) -> None: -# test_anim_layer_obj.add_mark(1, LayerColor(6)) -# assert test_anim_layer_obj.get_mark_color(1) == LayerColor(color_index=6) -# -# -# def test_layer_remove_mark(test_anim_layer_obj: Layer) -> None: -# test_anim_layer_obj.add_mark(1, LayerColor(6)) -# test_anim_layer_obj.remove_mark(6) -# assert test_anim_layer_obj.get_mark_color(6) is None -# -# -# Mark = tuple[int, LayerColor] -# -# -# @pytest.fixture -# def with_images(test_layer: TVPLayer) -> FixtureYield[int]: -# images = 5 -# george.tv_layer_insert_image(count=images, direction=george.InsertDirection.AFTER) -# yield images + 1 -# -# -# @pytest.fixture -# def add_marks(test_anim_layer_obj: Layer, with_images: int) -> FixtureYield[list[Mark]]: -# marks = [(frame, LayerColor(frame)) for frame in range(1, 7)] -# -# for frame, color in marks: -# test_anim_layer_obj.add_mark(frame, color) -# -# yield marks -# -# -# def test_layer_marks(test_anim_layer_obj: Layer, add_marks: list[Mark]) -> None: -# assert list(test_anim_layer_obj.marks) == add_marks -# -# -# def test_layer_clear_marks(test_anim_layer_obj: Layer, add_marks: list[Mark]) -> None: -# assert len(list(test_anim_layer_obj.marks)) != 0 -# test_anim_layer_obj.clear_marks() -# assert len(list(test_anim_layer_obj.marks)) == 0 -# -# -# def test_layer_select_frames(test_layer_obj: Layer, with_images: int) -> None: -# test_layer_obj.convert_to_anim_layer() -# clip_start = test_layer_obj.clip.start -# -# test_layer_obj.select_frames(clip_start, with_images) -# assert test_layer_obj.selected_frames == list( -# range(clip_start, with_images + clip_start) -# ) -# -# -# def test_layer_instances( -# 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 -# -# instances = [ -# LayerInstance(test_anim_layer_obj, frame) -# for frame in range(start_frame, end_frame) -# ] -# -# real_instances = list(test_anim_layer_obj.instances) -# -# assert len(real_instances) == with_images -# assert real_instances == instances -# -# -# def test_layer_rename_instances(test_anim_layer_obj: Layer, with_images: int) -> None: -# test_anim_layer_obj.rename_instances(george.InstanceNamingMode.ALL, prefix="hello_") +from __future__ import annotations + +from pathlib import Path + +import pytest + +from pytvpaint import george +from pytvpaint.clip import Clip +from pytvpaint.layer import Layer, LayerColor, LayerFolder, LayerInstance +from pytvpaint.project import Project +from pytvpaint.scene import Scene +from tests.conftest import FixtureYield + +IS_NOT_TVP12 = not george.tv_version()[1].startswith("12") + + +def test_layer_init(test_layer_obj: Layer) -> None: + assert Layer(test_layer_obj.id) == test_layer_obj + + +def test_layer_refresh(test_layer_obj: Layer) -> None: + test_layer_obj.refresh() + + +def test_layer_attributes( + test_project_obj: Project, + test_scene_obj: Scene, + test_clip_obj: Clip, + test_layer_obj: Layer, +) -> None: + assert test_layer_obj.id + + assert test_layer_obj.project == test_project_obj + assert test_layer_obj.scene == test_scene_obj + assert test_layer_obj.clip == test_clip_obj + + assert test_layer_obj.layer_type + + +@pytest.mark.parametrize("position", range(5)) +def test_layer_position( + test_layer_obj: Layer, + create_some_layers: None, + position: int, +) -> None: + test_layer_obj.position = position + assert test_layer_obj.position == position + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="Skip since layer naming doesn't work in TVP12.") +@pytest.mark.parametrize("name", ["l", "a n", "a0", "8 _d"]) +def test_layer_name(test_layer_obj: Layer, name: str) -> None: + test_layer_obj.name = name + assert test_layer_obj.name == name + + +@pytest.mark.parametrize("opacity", [1, 50, 24, 100]) +def test_layer_opacity(test_layer_obj: Layer, opacity: int) -> None: + test_layer_obj.opacity = opacity + assert test_layer_obj.opacity == opacity + + +def test_layer_start(test_layer_obj: Layer) -> None: + test_layer_obj.start + + +def test_layer_end(test_layer_obj: Layer) -> None: + test_layer_obj.end + + +@pytest.mark.parametrize("color_index", range(1, 26, 4)) +def test_layer_color(test_layer_obj: Layer, color_index: int) -> None: + color = LayerColor(color_index) + test_layer_obj.color = color + assert test_layer_obj.color == color + + +def test_layer_is_current( + test_layer_obj: Layer, + create_some_layers: list[Layer], +) -> None: + assert not test_layer_obj.is_current + + test_layer_obj.make_current() + assert test_layer_obj.is_current + + +def test_layer_is_selected(test_layer_obj: Layer) -> None: + assert not test_layer_obj.is_selected + + test_layer_obj.is_selected = True + assert test_layer_obj.is_selected + + test_layer_obj.is_selected = False + assert not test_layer_obj.is_selected + + +def test_layer_is_visible(test_layer_obj: Layer) -> None: + assert test_layer_obj.is_visible + + test_layer_obj.is_visible = False + assert not test_layer_obj.is_visible + + test_layer_obj.is_visible = True + assert test_layer_obj.is_visible + + +def test_layer_is_locked(test_layer_obj: Layer) -> None: + assert not test_layer_obj.is_locked + + test_layer_obj.is_locked = True + assert test_layer_obj.is_locked + + test_layer_obj.is_locked = False + assert not test_layer_obj.is_locked + + +def test_layer_is_collapsed(test_layer_obj: Layer) -> None: + assert not test_layer_obj.is_collapsed + + test_layer_obj.is_collapsed = True + assert test_layer_obj.is_collapsed + + test_layer_obj.is_collapsed = False + assert not test_layer_obj.is_collapsed + + +@pytest.mark.parametrize("blending_mode", george.BlendingMode) +def test_layer_blending_mode( + test_layer_obj: Layer, + blending_mode: george.BlendingMode, +) -> None: + test_layer_obj.blending_mode = blending_mode + assert test_layer_obj.blending_mode == blending_mode + + +@pytest.mark.parametrize("stencil", george.StencilMode) +def test_layer_stencil( + test_layer_obj: Layer, + stencil: george.StencilMode, +) -> None: + test_layer_obj.stencil = stencil + + current = test_layer_obj.stencil + if stencil == george.StencilMode.ON: + assert current == george.StencilMode.NORMAL + else: + assert current == stencil + + +@pytest.mark.parametrize("visible", [False, True]) +def test_layer_thumbnails_visible(test_layer_obj: Layer, visible: bool) -> None: + test_layer_obj.thumbnails_visible = visible + assert test_layer_obj.thumbnails_visible == visible + + +@pytest.mark.parametrize("value", [False, True]) +def test_layer_auto_break_instance(test_anim_layer_obj: Layer, value: bool) -> None: + test_anim_layer_obj.auto_break_instance = value + assert test_anim_layer_obj.auto_break_instance == value + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="TVP12 create anim layers by default now.") +def test_layer_auto_break_instance_not_anim_layer(test_layer_obj: Layer) -> None: + with pytest.raises(Exception, match="it's not an animation layer"): + test_layer_obj.auto_break_instance = True + + +@pytest.mark.parametrize("value", [False, True]) +def test_layer_auto_create_instance(test_layer_obj: Layer, value: bool) -> None: + test_layer_obj.auto_create_instance = value + assert test_layer_obj.auto_create_instance == value + + +@pytest.mark.parametrize("behavior", george.LayerBehavior) +def test_layer_pre_behavior(test_layer_obj: Layer, behavior: george.LayerBehavior) -> None: + test_layer_obj.pre_behavior = behavior + assert test_layer_obj.pre_behavior == behavior + + +@pytest.mark.parametrize("behavior", george.LayerBehavior) +def test_layer_post_behavior(test_layer_obj: Layer, behavior: george.LayerBehavior) -> None: + test_layer_obj.post_behavior = behavior + assert test_layer_obj.post_behavior == behavior + + +@pytest.mark.parametrize("value", [False, True]) +def test_layer_is_position_locked(test_layer_obj: Layer, value: bool) -> None: + test_layer_obj.is_position_locked = value + assert test_layer_obj.is_position_locked == value + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="`tv_preserve_set` does not work in tvpaint 12.") +@pytest.mark.parametrize("transparency", george.LayerTransparency) +def test_layer_preserve_transparency(test_layer_obj: Layer, transparency: george.LayerTransparency) -> None: + test_layer_obj.preserve_transparency = transparency + + transparency_map = { + george.LayerTransparency.MINUS_1: george.LayerTransparency.ON, + george.LayerTransparency.NONE: george.LayerTransparency.OFF, + } + + current = test_layer_obj.preserve_transparency + assert transparency_map.get(transparency, transparency) == current + + +def test_layer_convert_to_anim_layer(test_layer_obj: Layer) -> None: + test_layer_obj.convert_to_anim_layer() + assert test_layer_obj.layer_type == george.LayerType.SEQUENCE + + +def test_layer_load_dependencies(test_layer_obj: Layer) -> None: + test_layer_obj.load_dependencies() + + +def test_layer_current_layer_id(test_layer_obj: Layer) -> None: + assert Layer.current_layer_id() == test_layer_obj.id + + +def test_layer_current_layer(test_layer_obj: Layer) -> None: + assert Layer.current_layer() == test_layer_obj + + +@pytest.mark.parametrize("start", [0, 5, 10]) +def test_layer_shift(test_layer_obj: Layer, start: int) -> None: + test_layer_obj.shift(start) + + +@pytest.mark.parametrize("name", ["l", "loid", "K4", "k 1"]) +@pytest.mark.parametrize("color_index", [None, 5]) +def test_layer_new( + test_clip_obj: Clip, + name: str, + color_index: int | None, +) -> None: + color = LayerColor(color_index) if color_index else None + layer = Layer.new(name, color=color) + assert layer.name == name + + if color_index: + assert layer.color == LayerColor(color_index) + + +def test_layer_new_anim_layer(test_clip_obj: Clip) -> None: + layer = Layer.new_anim_layer("anim_layer") + assert layer.layer_type == george.LayerType.SEQUENCE + assert layer.thumbnails_visible + + +def test_layer_new_background_layer(test_clip_obj: Clip) -> None: + layer = Layer.new_background_layer("background_layer") + assert layer.layer_type == (george.LayerType.IMAGE if IS_NOT_TVP12 else george.LayerType.SEQUENCE) + assert layer.thumbnails_visible + assert layer.pre_behavior == george.LayerBehavior.HOLD + assert layer.post_behavior == george.LayerBehavior.HOLD + + +@pytest.mark.parametrize("name", ["l", "loid", "K4", "k 1"]) +def test_layer_duplicate(test_layer_obj: Layer, name: str) -> None: + dup = test_layer_obj.duplicate(name) + assert dup.name == name + + +def test_layer_remove(test_clip_obj: Clip) -> None: + layer = Layer.new("remove") + layer.remove() + + with pytest.raises(ValueError, match="Layer has been removed"): + layer.name = "other" + + +@pytest.mark.skipif(IS_NOT_TVP12, reason="Requires TVP12 or higher") +def test_layer_folder_remove(test_clip_obj: Clip) -> None: + layer = LayerFolder.new("remove") + layer.remove() + + with pytest.raises(ValueError, match="LayerFolder has been removed"): + layer.name = "other" + + +def test_layer_load_image(test_layer_obj: Layer, png_sequence: list[Path]) -> None: + test_layer_obj.load_image(image_path=png_sequence[0]) + + +def test_layer_render_frame(with_loaded_sequence: Layer, tmp_path: Path) -> None: + with_loaded_sequence.render_frame(tmp_path / "out.jpg", frame=3) + + +@pytest.mark.skipif(not IS_NOT_TVP12, reason="TVP12 create anim layers by default now.") +def test_layer_add_mark_not_anim_layer(test_layer_obj: Layer) -> None: + with pytest.raises(Exception, match="not an animation layer"): + test_layer_obj.add_mark(0, LayerColor(1)) + + +def test_layer_get_mark_color(test_anim_layer_obj: Layer) -> None: + test_anim_layer_obj.add_mark(1, LayerColor(6)) + assert test_anim_layer_obj.get_mark_color(1) == LayerColor(color_index=6) + + +def test_layer_remove_mark(test_anim_layer_obj: Layer) -> None: + test_anim_layer_obj.add_mark(1, LayerColor(6)) + test_anim_layer_obj.remove_mark(6) + assert test_anim_layer_obj.get_mark_color(6) is None + + +Mark = tuple[int, LayerColor] + + +@pytest.fixture +def with_images(test_layer: george.TVPLayer) -> FixtureYield[int]: + images = 5 + george.tv_layer_insert_image(count=images, direction=george.InsertDirection.AFTER) + yield images + 1 + + +@pytest.fixture +def add_marks(test_anim_layer_obj: Layer, with_images: int) -> FixtureYield[list[Mark]]: + marks = [(frame, LayerColor(frame)) for frame in range(1, 7)] + + for frame, color in marks: + test_anim_layer_obj.add_mark(frame, color) + + yield marks + + +def test_layer_marks(test_anim_layer_obj: Layer, add_marks: list[Mark]) -> None: + assert list(test_anim_layer_obj.marks) == add_marks + + +def test_layer_clear_marks(test_anim_layer_obj: Layer, add_marks: list[Mark]) -> None: + assert len(list(test_anim_layer_obj.marks)) != 0 + test_anim_layer_obj.clear_marks() + assert len(list(test_anim_layer_obj.marks)) == 0 + + +def test_layer_select_frames(test_layer_obj: Layer, with_images: int) -> None: + test_layer_obj.convert_to_anim_layer() + clip_start = test_layer_obj.clip.start + + test_layer_obj.select_frames(clip_start, with_images) + assert test_layer_obj.selected_frames == list(range(clip_start, with_images + clip_start)) + + +def test_layer_copy_paste(test_clip_obj: Clip, with_loaded_sequence: Layer) -> None: + # Select and copy + test_layer = test_clip_obj.current_layer + test_layer.select_frames(0, 3) + test_layer.copy() + + # Paste into a new layer + new_layer = Layer.new_anim_layer("Paste_Layer", test_clip_obj) + new_layer.paste() + + assert len(list(new_layer.instances)) == 3 + + +def test_layer_cut_paste(test_clip_obj: Clip, with_loaded_sequence: Layer) -> None: + # Cut frames + test_layer = test_clip_obj.current_layer + original_instance_count = len(list(test_layer.instances)) + test_layer.select_frames(0, 3) + test_layer.cut() + + # Paste into a new layer + new_layer = Layer.new_anim_layer("Cut_Layer", test_clip_obj) + new_layer.paste() + + new_layer.refresh() + assert len(list(test_layer.instances)) != original_instance_count + assert len(list(new_layer.instances)) == 3 + + +def test_layer_instances( + 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 + + instances = [LayerInstance(test_anim_layer_obj, frame) for frame in range(start_frame, end_frame)] + + real_instances = list(test_anim_layer_obj.instances) + + assert len(real_instances) == with_images + assert real_instances == instances + + +def test_layer_rename_instances(test_anim_layer_obj: Layer, with_images: int) -> None: + test_anim_layer_obj.rename_instances(george.InstanceNamingMode.ALL, prefix="hello_") diff --git a/tests/test_layer_instance.py b/tests/test_layer_instance.py index d5643e1..170eca3 100644 --- a/tests/test_layer_instance.py +++ b/tests/test_layer_instance.py @@ -1,34 +1,34 @@ -# import pytest -# -# from pytvpaint.layer import Layer, LayerInstance -# from tests.conftest import FixtureYield -# -# -# @pytest.fixture -# def test_layer_instance(test_anim_layer_obj: Layer) -> FixtureYield[LayerInstance]: -# start_frame = test_anim_layer_obj.project.start_frame -# yield LayerInstance(test_anim_layer_obj, start_frame) -# -# -# def test_layer_instance_init(test_anim_layer_obj: Layer) -> None: -# start_frame = test_anim_layer_obj.project.start_frame -# instance = LayerInstance(test_anim_layer_obj, start_frame) -# -# assert instance.start == start_frame -# assert instance.layer == test_anim_layer_obj -# -# -# def test_layer_instance_init_wrong_frame(test_anim_layer_obj: Layer) -> None: -# with pytest.raises(ValueError, match="no instance at frame"): -# LayerInstance(test_anim_layer_obj, 67) -# -# -# @pytest.mark.parametrize("name", ["name", "l", "lo6", "8.2"]) -# def test_layer_instance_name(test_layer_instance: LayerInstance, name: str) -> None: -# test_layer_instance.name = name -# assert test_layer_instance.name == name -# -# -# def test_layer_instance_duplicate(test_layer_instance: LayerInstance) -> None: -# test_layer_instance.duplicate() -# assert test_layer_instance.layer.get_instance(test_layer_instance.start + 1) +import pytest + +from pytvpaint.layer import Layer, LayerInstance +from tests.conftest import FixtureYield + + +@pytest.fixture +def test_layer_instance(test_anim_layer_obj: Layer) -> FixtureYield[LayerInstance]: + start_frame = test_anim_layer_obj.project.start_frame + yield LayerInstance(test_anim_layer_obj, start_frame) + + +def test_layer_instance_init(test_anim_layer_obj: Layer) -> None: + start_frame = test_anim_layer_obj.project.start_frame + instance = LayerInstance(test_anim_layer_obj, start_frame) + + assert instance.start == start_frame + assert instance.layer == test_anim_layer_obj + + +def test_layer_instance_init_wrong_frame(test_anim_layer_obj: Layer) -> None: + with pytest.raises(ValueError, match="no instance at frame"): + LayerInstance(test_anim_layer_obj, 67) + + +@pytest.mark.parametrize("name", ["name", "l", "lo6", "8.2"]) +def test_layer_instance_name(test_layer_instance: LayerInstance, name: str) -> None: + test_layer_instance.name = name + assert test_layer_instance.name == name + + +def test_layer_instance_duplicate(test_layer_instance: LayerInstance) -> None: + test_layer_instance.duplicate() + assert test_layer_instance.layer.get_instance(test_layer_instance.start + 1) diff --git a/tests/test_project.py b/tests/test_project.py index 565a24d..fa16ef5 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,439 +1,516 @@ -# from pathlib import Path -# -# import pytest -# -# from pytvpaint import george -# from pytvpaint.clip import Clip -# from pytvpaint.george.grg_project import TVPProject -# from pytvpaint.project import Project -# from pytvpaint.scene import Scene -# from pytvpaint.sound import ProjectSound -# from tests.conftest import FixtureYield -# -# -# def test_project_init(test_project: TVPProject) -> None: -# project = Project(test_project.id) -# assert project.id == test_project.id -# -# -# def test_project_refresh(test_project_obj: Project) -> None: -# test_project_obj.refresh() -# -# -# def test_project_position(test_project_obj: Project) -> None: -# pos = test_project_obj.position -# assert george.tv_project_enum_id(pos) == test_project_obj.id -# -# -# def test_project_closed(test_project_obj: Project) -> None: -# assert not test_project_obj.is_closed -# george.tv_project_close(test_project_obj.id) -# assert test_project_obj.is_closed -# -# -# def test_project_exists_on_disk(test_project_obj: Project) -> None: -# assert not test_project_obj.exists -# george.tv_save_project(test_project_obj.path) -# assert test_project_obj.exists -# -# -# @pytest.fixture() -# def other_project(tmp_path: Path) -> FixtureYield[None]: -# other_project = george.tv_project_new( -# tmp_path / "other.tvpp", width=1234, height=567 -# ) -# yield -# george.tv_project_close(other_project) -# -# -# def test_project_is_current(test_project_obj: Project, other_project: None) -> None: -# assert not test_project_obj.is_current -# george.tv_project_select(test_project_obj.id) -# assert test_project_obj.is_current -# -# -# def test_project_make_current(test_project_obj: Project, other_project: None) -> None: -# test_project_obj.make_current() -# assert test_project_obj.is_current -# -# -# def test_project_path(test_project_obj: Project, tmp_path: Path) -> None: -# new_path = tmp_path / "new_location.tvpp" -# george.tv_save_project(new_path) -# assert test_project_obj.path == new_path -# -# -# def test_project_name(test_project_obj: Project, tmp_path: Path) -> None: -# new_path = tmp_path / "new_location.tvpp" -# george.tv_save_project(new_path) -# assert test_project_obj.name == new_path.stem -# -# -# def test_project_width_height(test_project_obj: Project, other_project: None) -> None: -# assert test_project_obj.width == 1920 -# assert test_project_obj.height == 1080 -# -# -# def test_project_resize_same_width_and_height(test_project_obj: Project) -> None: -# resized = test_project_obj.resize(test_project_obj.width, test_project_obj.height) -# assert resized == test_project_obj -# -# -# def test_project_resize( -# test_project_obj: Project, cleanup_current_project: None -# ) -> None: -# resized = test_project_obj.resize(100, 200) -# -# # The resized project is a new project -# assert resized != test_project_obj -# -# assert resized.width == 100 -# assert resized.height == 200 -# -# # It closes the project but it still exists on disk -# assert test_project_obj.is_closed -# -# -# def test_project_resize_overwrite( -# test_project_obj: Project, cleanup_current_project: None -# ) -> None: -# origin_path = test_project_obj.path -# -# resized = test_project_obj.resize(100, 200, overwrite=True) -# resized.close() -# -# # Verify that the original file is overwritten -# resized_load = Project.load(origin_path) -# assert resized == resized_load -# assert resized.width == resized_load.width -# assert resized.height == resized_load.height -# -# -# def test_project_fps(test_project_obj: Project) -> None: -# test_project_obj.set_fps(54) -# assert test_project_obj.fps == 54 -# -# -# def test_project_fps_preview(test_project_obj: Project) -> None: -# test_project_obj.set_fps(54, preview=True) -# assert test_project_obj.playback_fps == 54 -# -# -# @pytest.mark.parametrize("field_order", george.FieldOrder) -# def test_project_field_order( -# tmp_path: Path, -# field_order: george.FieldOrder, -# cleanup_current_project: None, -# ) -> None: -# proj = Project.new(tmp_path, field_order=field_order) -# assert proj.field_order == field_order -# -# -# @pytest.mark.parametrize("aspect_ratio", [0.5, 1, 4]) -# def test_project_pixel_aspect_ratio( -# tmp_path: Path, -# aspect_ratio: float, -# cleanup_current_project: None, -# ) -> None: -# proj = Project.new(tmp_path, pixel_aspect_ratio=aspect_ratio) -# assert proj.pixel_aspect_ratio == aspect_ratio -# -# -# @pytest.mark.parametrize("start_frame", [1, 2, 10, 100]) -# def test_project_start_frame(test_project_obj: Project, start_frame: int) -> None: -# test_project_obj.start_frame = start_frame -# assert test_project_obj.start_frame == start_frame -# -# -# def test_project_end_frame_clip_simple(test_project_obj: Project) -> None: -# test_project_obj.start_frame = 1 -# -# clip = test_project_obj.current_clip -# layer = clip.add_layer("anim") -# layer.convert_to_anim_layer() -# -# layer.add_instance(10) -# -# assert test_project_obj.end_frame == 10 -# -# -# def test_project_end_frame_clip_mark_out(test_project_obj: Project) -> None: -# test_project_obj.start_frame = 3 -# -# clip = test_project_obj.current_clip -# clip.mark_in = 4 -# clip.mark_out = 8 -# -# layer = clip.add_layer("anim") -# layer.convert_to_anim_layer() -# -# # The instance exceeds the mark out so it's not taken into account in clip duration -# layer.add_instance(10) -# -# assert test_project_obj.end_frame == 7 -# -# -# @pytest.mark.parametrize("mark_in", [1, 2, 10, 100]) -# @pytest.mark.parametrize("current_frame", [0, 1, 5, 50]) -# @pytest.mark.parametrize("start_frame", [2, 5, 20]) -# def test_project_current_frame( -# test_project_obj: Project, mark_in: int, current_frame: int, start_frame: int -# ) -> None: -# test_project_obj.start_frame = start_frame -# test_project_obj.current_clip.mark_in = mark_in -# test_project_obj.current_frame = current_frame -# -# assert test_project_obj.current_frame == current_frame -# -# -# def test_project_clear_background(test_project_obj: Project) -> None: -# test_project_obj.clear_background() -# -# mode, colors = george.tv_background_get() -# assert mode == george.BackgroundMode.NONE -# assert colors is None -# -# -# @pytest.mark.parametrize( -# "color", -# [ -# george.RGBColor(0, 255, 0), -# george.RGBColor(255, 255, 0), -# george.RGBColor(0, 255, 255), -# ], -# ) -# def test_project_set_background_solid_color( -# test_project_obj: Project, color: george.RGBColor -# ) -> None: -# test_project_obj.background_mode = george.BackgroundMode.COLOR -# test_project_obj.background_colors = color -# -# mode, colors = george.tv_background_get() -# assert test_project_obj.background_mode == mode -# assert test_project_obj.background_colors == colors -# -# -# @pytest.mark.parametrize( -# "colors", -# [ -# (george.RGBColor(255, 255, 255), george.RGBColor(0, 0, 0)), -# (george.RGBColor(0, 255, 0), george.RGBColor(0, 255, 255)), -# ], -# ) -# def test_project_set_background_checker_colors( -# test_project_obj: Project, colors: tuple[george.RGBColor, george.RGBColor] -# ) -> None: -# test_project_obj.background_mode = george.BackgroundMode.CHECK -# test_project_obj.background_colors = colors -# -# mode, actual_colors = george.tv_background_get() -# assert test_project_obj.background_mode == mode -# assert test_project_obj.background_colors == actual_colors -# -# -# @pytest.mark.parametrize("header", ["", "Hello", "THis is a project header"]) -# def test_project_header_info(test_project_obj: Project, header: str) -> None: -# test_project_obj.header_info = header -# assert test_project_obj.header_info == header -# -# -# @pytest.mark.parametrize("author", ["a", "Hello", "THis is a project author"]) -# def test_project_author(test_project_obj: Project, author: str) -> None: -# test_project_obj.author = author -# assert test_project_obj.author == author -# -# -# @pytest.mark.parametrize("notes", ["a", "Hello", "THis is a project notes"]) -# def test_project_notes(test_project_obj: Project, notes: str) -> None: -# test_project_obj.notes = notes -# assert test_project_obj.notes == notes -# -# -# def test_project_get_project(test_project_obj: Project) -> None: -# assert Project.get_project(by_id=test_project_obj.id) == test_project_obj -# assert Project.get_project(by_name=test_project_obj.name) == test_project_obj -# -# -# def test_project_get_project_wrong_id(test_project_obj: Project) -> None: -# res = Project.get_project(by_id="unknown") -# assert res is None -# -# -# def test_project_get_project_wrong_name(test_project_obj: Project) -> None: -# res = Project.get_project(by_name="name") -# assert res is None -# -# -# def test_project_current_scene_ids( -# test_project_obj: Project, -# create_some_scenes: list[Scene], -# ) -> None: -# ids = list(test_project_obj.current_scene_ids()) -# assert ids == [s.id for s in create_some_scenes] -# -# -# def test_project_current_scene( -# test_project_obj: Project, test_scene_obj: Scene -# ) -> None: -# assert test_project_obj.current_scene == test_scene_obj -# -# -# def test_project_scenes( -# test_project_obj: Project, -# create_some_scenes: list[Scene], -# ) -> None: -# assert list(test_project_obj.scenes) == create_some_scenes -# -# -# def test_project_get_scene(test_project_obj: Project, test_scene_obj: Scene) -> None: -# test_project_obj.get_scene(by_id=test_scene_obj.id) -# -# -# def test_project_add_scene(test_project_obj: Project) -> None: -# scene = test_project_obj.add_scene() -# assert test_project_obj.current_scene == scene -# -# -# @pytest.mark.parametrize("index", range(5)) -# def test_project_current_clip( -# test_project_obj: Project, create_some_clips: list[Clip], index: int -# ) -> None: -# clip = create_some_clips[index] -# clip.make_current() -# assert test_project_obj.current_clip == clip -# -# -# def test_project_clips( -# test_project_obj: Project, create_some_clips: list[Clip] -# ) -> None: -# clips = [clip for scene in test_project_obj.scenes for clip in scene.clips] -# assert list(test_project_obj.clips) == clips -# -# -# def test_project_get_clip_by_id(test_project_obj: Project, test_clip_obj: Clip) -> None: -# assert test_project_obj.get_clip(by_id=test_clip_obj.id) == test_clip_obj -# -# -# def test_project_get_clip_by_name( -# test_project_obj: Project, -# test_clip_obj: Clip, -# ) -> None: -# assert test_project_obj.get_clip(by_name=test_clip_obj.name) == test_clip_obj -# -# -# def test_project_get_clip_by_id_scene_id( -# test_project_obj: Project, -# test_scene_obj: Scene, -# test_clip_obj: Clip, -# ) -> None: -# clip = test_project_obj.get_clip( -# by_id=test_clip_obj.id, -# scene_id=test_scene_obj.id, -# ) -# assert clip == test_clip_obj -# -# -# @pytest.mark.parametrize("name", ["l", "hello", "this is my clip"]) -# def test_project_add_clip( -# test_project_obj: Project, test_scene_obj: Scene, name: str -# ) -> None: -# clip = test_project_obj.add_clip(name, test_scene_obj) -# assert clip.scene == test_scene_obj -# assert clip.name == name -# -# -# @pytest.mark.parametrize("name", ["l", "hello", "this is my clip"]) -# def test_project_add_clip_current_scene(test_project_obj: Project, name: str) -> None: -# clip = test_project_obj.add_clip(name, scene=None) -# assert clip.scene == test_project_obj.current_scene -# assert clip.name == name -# -# -# def test_project_sounds( -# test_project_obj: Project, -# create_some_project_sounds: list[ProjectSound], -# ) -> None: -# assert list(test_project_obj.sounds) == create_some_project_sounds -# -# -# def test_project_add_sound(test_project_obj: Project, wav_file: Path) -> None: -# test_project_obj.add_sound(wav_file) -# -# -# def test_project_current_project_id(test_project_obj: Project) -> None: -# assert Project.current_project_id() == test_project_obj.id -# -# -# def test_project_current_project(test_project_obj: Project) -> None: -# assert Project.current_project() == test_project_obj -# -# -# def test_project_opened_project_ids(create_some_projects: list[Project]) -> None: -# expected_ids = [p.id for p in create_some_projects] -# assert expected_ids == list(Project.open_projects_ids()) -# -# -# def test_project_opened_projects(create_some_projects: list[Project]) -> None: -# assert create_some_projects == list(Project.open_projects()) -# -# -# @pytest.mark.parametrize("mark_in", [1, 2, 10, 100]) -# def test_project_mark_in(test_project_obj: Project, mark_in: int) -> None: -# test_project_obj.mark_in = mark_in -# assert test_project_obj.mark_in == mark_in -# -# -# @pytest.mark.parametrize("mark_out", [1, 2, 10, 100]) -# def test_project_mark_out(test_project_obj: Project, mark_out: int) -> None: -# test_project_obj.mark_in = mark_out -# assert test_project_obj.mark_in == mark_out -# -# -# def test_project_new(tmp_path: Path, cleanup_current_project: None) -> None: -# proj = Project.new(tmp_path / "project.tvpp") -# assert Project.current_project() == proj -# -# -# def test_project_new_from_camera(test_project_obj: Project) -> None: -# george.tv_camera_insert_point(0, 0, 0, 0, 1) -# george.tv_camera_insert_point(5, 100, 150, 45, 1) -# -# test_project_obj.new_from_camera() -# -# -# def test_project_duplicate( -# test_project_obj: Project, -# ) -> None: -# dup = test_project_obj.duplicate() -# assert dup != test_project_obj -# dup.close() -# -# -# def test_project_close(test_project_obj: Project) -> None: -# test_project_obj.close() -# assert test_project_obj.is_closed -# -# # The project can't be refreshed -# with pytest.raises(ValueError): -# test_project_obj.refresh() -# -# -# def test_project_close_all(create_some_projects: list[Project]) -> None: -# Project.close_all() -# assert all(p.is_closed for p in create_some_projects) -# -# -# def test_project_load(test_project_obj: Project) -> None: -# test_project_obj.save() -# assert Project.load(test_project_obj.path) == test_project_obj -# -# -# def test_project_save(test_project_obj: Project, tmp_path: Path) -> None: -# test_project_obj.save(tmp_path / "save.tvpp") -# -# -# def test_project_save_destination_does_not_exist( -# test_project_obj: Project, tmp_path: Path -# ) -> None: -# with pytest.raises(ValueError, match="folder does not exist"): -# test_project_obj.save(tmp_path / "lo" / "save.tvpp") +from pathlib import Path + +import pytest + +from pytvpaint import george, guideline +from pytvpaint.clip import Clip +from pytvpaint.george.grg_project import TVPProject +from pytvpaint.project import Project +from pytvpaint.scene import Scene +from pytvpaint.sound import ProjectSound +from tests.conftest import FixtureYield + + +def test_project_init(test_project: TVPProject) -> None: + project = Project(test_project.id) + assert project.id == test_project.id + + +def test_project_refresh(test_project_obj: Project) -> None: + test_project_obj.refresh() + + +def test_project_position(test_project_obj: Project) -> None: + pos = test_project_obj.position + assert george.tv_project_enum_id(pos) == test_project_obj.id + + +def test_project_closed(test_project_obj: Project) -> None: + assert not test_project_obj.is_closed + george.tv_project_close(test_project_obj.id) + assert test_project_obj.is_closed + + +def test_project_exists_on_disk(test_project_obj: Project) -> None: + assert not test_project_obj.exists + george.tv_save_project(test_project_obj.path) + assert test_project_obj.exists + + +@pytest.fixture() +def other_project(tmp_path: Path) -> FixtureYield[None]: + other_project = george.tv_project_new(tmp_path / "other.tvpp", width=1234, height=567) + yield + george.tv_project_close(other_project) + + +def test_project_is_current(test_project_obj: Project, other_project: None) -> None: + assert not test_project_obj.is_current + george.tv_project_select(test_project_obj.id) + assert test_project_obj.is_current + + +def test_project_make_current(test_project_obj: Project, other_project: None) -> None: + test_project_obj.make_current() + assert test_project_obj.is_current + + +def test_project_path(test_project_obj: Project, tmp_path: Path) -> None: + new_path = tmp_path / "new_location.tvpp" + george.tv_save_project(new_path) + assert test_project_obj.path == new_path + + +def test_project_name(test_project_obj: Project, tmp_path: Path) -> None: + new_path = tmp_path / "new_location.tvpp" + george.tv_save_project(new_path) + assert test_project_obj.name == new_path.stem + + +def test_project_width_height(test_project_obj: Project, other_project: None) -> None: + assert test_project_obj.width == 1920 + assert test_project_obj.height == 1080 + + +def test_project_resize_same_width_and_height(test_project_obj: Project) -> None: + resized = test_project_obj.resize(test_project_obj.width, test_project_obj.height) + assert resized == test_project_obj + + +def test_project_resize(test_project_obj: Project, cleanup_current_project: None) -> None: + resized = test_project_obj.resize(100, 200) + + # The resized project is a new project + assert resized != test_project_obj + + assert resized.width == 100 + assert resized.height == 200 + + # It closes the project but it still exists on disk + assert test_project_obj.is_closed + + +def test_project_resize_overwrite(test_project_obj: Project, cleanup_current_project: None) -> None: + origin_path = test_project_obj.path + + resized = test_project_obj.resize(100, 200, overwrite=True) + resized.close() + + # Verify that the original file is overwritten + resized_load = Project.load(origin_path) + assert resized == resized_load + assert resized.width == resized_load.width + assert resized.height == resized_load.height + + +def test_project_fps(test_project_obj: Project) -> None: + test_project_obj.set_fps(54) + assert test_project_obj.fps == 54 + + +def test_project_fps_preview(test_project_obj: Project) -> None: + test_project_obj.set_fps(54, preview=True) + assert test_project_obj.playback_fps == 54 + + +@pytest.mark.parametrize("field_order", george.FieldOrder) +def test_project_field_order( + tmp_path: Path, + field_order: george.FieldOrder, + cleanup_current_project: None, +) -> None: + proj = Project.new(tmp_path, field_order=field_order) + assert proj.field_order == field_order + + +@pytest.mark.parametrize("aspect_ratio", [0.5, 1, 4]) +def test_project_pixel_aspect_ratio( + tmp_path: Path, + aspect_ratio: float, + cleanup_current_project: None, +) -> None: + proj = Project.new(tmp_path, pixel_aspect_ratio=aspect_ratio) + assert proj.pixel_aspect_ratio == aspect_ratio + + +@pytest.mark.parametrize("start_frame", [1, 2, 10, 100]) +def test_project_start_frame(test_project_obj: Project, start_frame: int) -> None: + test_project_obj.start_frame = start_frame + assert test_project_obj.start_frame == start_frame + + +def test_project_end_frame_clip_simple(test_project_obj: Project) -> None: + test_project_obj.start_frame = 1 + + clip = test_project_obj.current_clip + layer = clip.add_layer("anim") + layer.convert_to_anim_layer() + + layer.add_instance(10) + + assert test_project_obj.end_frame == 10 + + +def test_project_end_frame_clip_mark_out(test_project_obj: Project) -> None: + test_project_obj.start_frame = 3 + + clip = test_project_obj.current_clip + clip.mark_in = 4 + clip.mark_out = 8 + + layer = clip.add_layer("anim") + layer.convert_to_anim_layer() + + # The instance exceeds the mark out so it's not taken into account in clip duration + layer.add_instance(10) + + assert test_project_obj.end_frame == 7 + + +@pytest.mark.parametrize("mark_in", [1, 2, 10, 100]) +@pytest.mark.parametrize("current_frame", [0, 1, 5, 50]) +@pytest.mark.parametrize("start_frame", [2, 5, 20]) +def test_project_current_frame(test_project_obj: Project, mark_in: int, current_frame: int, start_frame: int) -> None: + test_project_obj.start_frame = start_frame + test_project_obj.current_clip.mark_in = mark_in + test_project_obj.current_frame = current_frame + + assert test_project_obj.current_frame == current_frame + + +def test_project_clear_background(test_project_obj: Project) -> None: + test_project_obj.clear_background() + + mode, colors = george.tv_background_get() + assert mode == george.BackgroundMode.NONE + assert colors is None + + +@pytest.mark.parametrize( + "color", + [ + george.RGBColor(0, 255, 0), + george.RGBColor(255, 255, 0), + george.RGBColor(0, 255, 255), + ], +) +def test_project_set_background_solid_color(test_project_obj: Project, color: george.RGBColor) -> None: + test_project_obj.background_mode = george.BackgroundMode.COLOR + test_project_obj.background_colors = color + + mode, colors = george.tv_background_get() + assert test_project_obj.background_mode == mode + assert test_project_obj.background_colors == colors + + +@pytest.mark.parametrize( + "colors", + [ + (george.RGBColor(255, 255, 255), george.RGBColor(0, 0, 0)), + (george.RGBColor(0, 255, 0), george.RGBColor(0, 255, 255)), + ], +) +def test_project_set_background_checker_colors( + test_project_obj: Project, colors: tuple[george.RGBColor, george.RGBColor] +) -> None: + test_project_obj.background_mode = george.BackgroundMode.CHECK + test_project_obj.background_colors = colors + + mode, actual_colors = george.tv_background_get() + assert test_project_obj.background_mode == mode + assert test_project_obj.background_colors == actual_colors + + +@pytest.mark.parametrize("header", ["", "Hello", "This is a project header", "This is a project \nheader"]) +def test_project_header_info(test_project_obj: Project, header: str) -> None: + test_project_obj.header_info = header + assert test_project_obj.header_info == header + + +@pytest.mark.parametrize("author", ["a", "Hello", "This is a project author", "This is a project \nauthor"]) +def test_project_author(test_project_obj: Project, author: str) -> None: + test_project_obj.author = author + assert test_project_obj.author == author + + +@pytest.mark.parametrize("notes", ["a", "Hello", "This is a project note", "This is a project \nnote"]) +def test_project_notes(test_project_obj: Project, notes: str) -> None: + test_project_obj.notes = notes + assert test_project_obj.notes == notes + + +def test_project_get_project(test_project_obj: Project) -> None: + assert Project.get_project(by_id=test_project_obj.id) == test_project_obj + assert Project.get_project(by_name=test_project_obj.name) == test_project_obj + + +def test_project_get_project_wrong_id(test_project_obj: Project) -> None: + res = Project.get_project(by_id="unknown") + assert res is None + + +def test_project_get_project_wrong_name(test_project_obj: Project) -> None: + res = Project.get_project(by_name="name") + assert res is None + + +def test_project_current_scene_ids( + test_project_obj: Project, + create_some_scenes: list[Scene], +) -> None: + ids = list(test_project_obj.current_scene_ids()) + assert ids == [s.id for s in create_some_scenes] + + +def test_project_current_scene(test_project_obj: Project, test_scene_obj: Scene) -> None: + assert test_project_obj.current_scene == test_scene_obj + + +def test_project_scenes( + test_project_obj: Project, + create_some_scenes: list[Scene], +) -> None: + assert list(test_project_obj.scenes) == create_some_scenes + + +def test_project_get_scene(test_project_obj: Project, test_scene_obj: Scene) -> None: + test_project_obj.get_scene(scene_id=test_scene_obj.id) + + +def test_project_add_scene(test_project_obj: Project) -> None: + scene = test_project_obj.add_scene() + assert test_project_obj.current_scene == scene + + +@pytest.mark.parametrize("index", range(5)) +def test_project_current_clip(test_project_obj: Project, create_some_clips: list[Clip], index: int) -> None: + clip = create_some_clips[index] + clip.make_current() + assert test_project_obj.current_clip == clip + + +def test_project_clips(test_project_obj: Project, create_some_clips: list[Clip]) -> None: + clips = [clip for scene in test_project_obj.scenes for clip in scene.clips] + assert list(test_project_obj.clips) == clips + + +def test_project_get_clip_by_id(test_project_obj: Project, test_clip_obj: Clip) -> None: + assert test_project_obj.get_clip(by_id=test_clip_obj.id) == test_clip_obj + + +def test_project_get_clip_by_name( + test_project_obj: Project, + test_clip_obj: Clip, +) -> None: + assert test_project_obj.get_clip(by_name=test_clip_obj.name) == test_clip_obj + + +def test_project_get_clip_by_id_scene_id( + test_project_obj: Project, + test_scene_obj: Scene, + test_clip_obj: Clip, +) -> None: + clip = test_project_obj.get_clip( + by_id=test_clip_obj.id, + scene_id=test_scene_obj.id, + ) + assert clip == test_clip_obj + + +@pytest.mark.parametrize("name", ["l", "hello", "this is my clip"]) +def test_project_add_clip(test_project_obj: Project, test_scene_obj: Scene, name: str) -> None: + clip = test_project_obj.add_clip(name, test_scene_obj) + assert clip.scene == test_scene_obj + assert clip.name == name + + +@pytest.mark.parametrize("name", ["l", "hello", "this is my clip"]) +def test_project_add_clip_current_scene(test_project_obj: Project, name: str) -> None: + clip = test_project_obj.add_clip(name, scene=None) + assert clip.scene == test_project_obj.current_scene + assert clip.name == name + + +def test_project_sounds( + test_project_obj: Project, + create_some_project_sounds: list[ProjectSound], +) -> None: + assert list(test_project_obj.sounds) == create_some_project_sounds + + +def test_project_add_sound(test_project_obj: Project, wav_file: Path) -> None: + test_project_obj.add_sound(wav_file) + + +def test_project_current_project_id(test_project_obj: Project) -> None: + assert Project.current_project_id() == test_project_obj.id + + +def test_project_current_project(test_project_obj: Project) -> None: + assert Project.current_project() == test_project_obj + + +def test_project_opened_project_ids(create_some_projects: list[Project]) -> None: + expected_ids = [p.id for p in create_some_projects] + assert expected_ids == list(Project.open_projects_ids()) + + +def test_project_opened_projects(create_some_projects: list[Project]) -> None: + assert create_some_projects == list(Project.open_projects()) + + +@pytest.mark.parametrize("mark_in", [1, 2, 10, 100]) +def test_project_mark_in(test_project_obj: Project, mark_in: int) -> None: + test_project_obj.mark_in = mark_in + assert test_project_obj.mark_in == mark_in + + +@pytest.mark.parametrize("mark_out", [1, 2, 10, 100]) +def test_project_mark_out(test_project_obj: Project, mark_out: int) -> None: + test_project_obj.mark_in = mark_out + assert test_project_obj.mark_in == mark_out + + +def test_project_add_guideline_line(test_project_obj: Project) -> None: + guideline_line = test_project_obj.add_guideline_line(x=10, y=20, angle=45) + assert isinstance(guideline_line, guideline.GuidelineLine) + assert guideline_line.x == 10 + assert guideline_line.y == 20 + assert guideline_line.angle == 45 + + +def test_project_add_guideline_segment(test_project_obj: Project) -> None: + guideline_segment = test_project_obj.add_guideline_segment(x1=0, y1=0, x2=100, y2=100) + assert isinstance(guideline_segment, guideline.GuidelineSegment) + assert guideline_segment.x1 == 0 + assert guideline_segment.y1 == 0 + assert guideline_segment.x2 == 100 + assert guideline_segment.y2 == 100 + + +def test_project_add_guideline_circle(test_project_obj: Project) -> None: + guideline_circle = test_project_obj.add_guideline_circle(x=50, y=50, radius=25) + assert isinstance(guideline_circle, guideline.GuidelineCircle) + assert guideline_circle.x == 50 + assert guideline_circle.y == 50 + assert guideline_circle.radius == 25 + + +def test_project_add_guideline_ellipse(test_project_obj: Project) -> None: + guideline_ellipse = test_project_obj.add_guideline_ellipse(x=10, y=10, radius_a=20, radius_b=10) + assert isinstance(guideline_ellipse, guideline.GuidelineEllipse) + assert guideline_ellipse.x == 10 + assert guideline_ellipse.y == 10 + assert guideline_ellipse.radius_a == 20 + assert guideline_ellipse.radius_b == 10 + + +def test_project_add_guideline_grid(test_project_obj: Project) -> None: + guideline_grid = test_project_obj.add_guideline_grid(x=0, y=0, width=1920, height=1080) + assert isinstance(guideline_grid, guideline.GuidelineGrid) + assert guideline_grid.x == 0 + assert guideline_grid.y == 0 + assert guideline_grid.width == 1920 + assert guideline_grid.height == 1080 + + +def test_project_add_guideline_marks(test_project_obj: Project) -> None: + guideline_marks = test_project_obj.add_guideline_marks(count_x=4, count_y=3) + assert isinstance(guideline_marks, guideline.GuidelineMarks) + assert guideline_marks.count_x == 4 + assert guideline_marks.count_y == 3 + + +def test_project_add_guideline_field_chart(test_project_obj: Project) -> None: + guideline_field = test_project_obj.add_guideline_field_chart() + assert isinstance(guideline_field, guideline.GuidelineFieldChart) + # (No internal metrics to assert on creation for field charts) + + +def test_project_add_guideline_animator_field(test_project_obj: Project) -> None: + guideline_anim = test_project_obj.add_guideline_animator_field() + assert isinstance(guideline_anim, guideline.GuidelineAnimatorField) + # (No internal metrics to assert on creation for animator fields) + + +def test_project_add_guideline_safe_area(test_project_obj: Project) -> None: + guideline_safe = test_project_obj.add_guideline_safe_area(sf_out=10, sf_in=20) + assert isinstance(guideline_safe, guideline.GuidelineSafeArea) + assert guideline_safe.sf_out == 10 + assert guideline_safe.sf_in == 20 + + +def test_project_add_guideline_vanishing_point1(test_project_obj: Project) -> None: + guideline_vp1 = test_project_obj.add_guideline_vanishing_point1(x=100, y=100, grid=True) + assert isinstance(guideline_vp1, guideline.GuidelineVanishPoint1) + assert guideline_vp1.x == 100 + assert guideline_vp1.y == 100 + assert guideline_vp1.grid is True + + +def test_project_add_guideline_vanishing_point2(test_project_obj: Project) -> None: + guideline_vp2 = test_project_obj.add_guideline_vanishing_point2(x1=10, y1=10, x2=100, y2=100) + assert isinstance(guideline_vp2, guideline.GuidelineVanishPoint2) + assert guideline_vp2.x1 == 10 + assert guideline_vp2.y1 == 10 + assert guideline_vp2.x2 == 100 + assert guideline_vp2.y2 == 100 + + +def test_project_add_guideline_vanishing_point3(test_project_obj: Project) -> None: + guideline_vp3 = test_project_obj.add_guideline_vanishing_point3(x1=10, y1=10, x2=100, y2=100, x3=50, y3=50) + assert isinstance(guideline_vp3, guideline.GuidelineVanishPoint3) + assert guideline_vp3.x1 == 10 + assert guideline_vp3.y1 == 10 + assert guideline_vp3.x2 == 100 + assert guideline_vp3.y2 == 100 + assert guideline_vp3.x3 == 50 + assert guideline_vp3.y3 == 50 + + +def test_project_new(tmp_path: Path, cleanup_current_project: None) -> None: + proj = Project.new(tmp_path / "project.tvpp") + assert Project.current_project() == proj + + +def test_project_new_from_camera(test_project_obj: Project) -> None: + george.tv_camera_insert_point(0, 0, 0, 0, 1) + george.tv_camera_insert_point(5, 100, 150, 45, 1) + + test_project_obj.new_from_camera() + + +def test_project_duplicate( + test_project_obj: Project, +) -> None: + dup = test_project_obj.duplicate() + assert dup != test_project_obj + dup.close() + + +def test_project_close(test_project_obj: Project) -> None: + test_project_obj.close() + assert test_project_obj.is_closed + + # The project can't be refreshed + with pytest.raises(ValueError): + test_project_obj.refresh() + + +def test_project_close_all(create_some_projects: list[Project]) -> None: + Project.close_all() + assert all(p.is_closed for p in create_some_projects) + + +def test_project_load(test_project_obj: Project) -> None: + test_project_obj.save() + assert Project.load(test_project_obj.path) == test_project_obj + + +def test_project_save(test_project_obj: Project, tmp_path: Path) -> None: + test_project_obj.save(tmp_path / "save.tvpp") + + +def test_project_save_destination_does_not_exist(test_project_obj: Project, tmp_path: Path) -> None: + with pytest.raises(ValueError, match="folder does not exist"): + test_project_obj.save(tmp_path / "lo" / "save.tvpp") diff --git a/tests/test_project_sound.py b/tests/test_project_sound.py index c235eb2..20950c5 100644 --- a/tests/test_project_sound.py +++ b/tests/test_project_sound.py @@ -1,21 +1,21 @@ -# import pytest -# -# from pytvpaint.george.exceptions import GeorgeError -# from pytvpaint.sound import ProjectSound -# -# -# def test_project_sound_init(test_project_sound: ProjectSound) -> None: -# assert ProjectSound(track_index=0) == test_project_sound -# -# -# def test_project_sound_remove(test_project_sound: ProjectSound) -> None: -# index = test_project_sound.track_index -# test_project_sound.remove() -# -# # The project sound doesn't exist anymore -# with pytest.raises(GeorgeError): -# ProjectSound(track_index=index) -# -# -# def test_project_sound_reload(test_project_sound: ProjectSound) -> None: -# test_project_sound.reload() +import pytest + +from pytvpaint.george.exceptions import GeorgeError +from pytvpaint.sound import ProjectSound + + +def test_project_sound_init(test_project_sound: ProjectSound) -> None: + assert ProjectSound(track_index=0) == test_project_sound + + +def test_project_sound_remove(test_project_sound: ProjectSound) -> None: + index = test_project_sound.track_index + test_project_sound.remove() + + # The project sound doesn't exist anymore + with pytest.raises(GeorgeError): + ProjectSound(track_index=index) + + +def test_project_sound_reload(test_project_sound: ProjectSound) -> None: + test_project_sound.reload() diff --git a/tests/test_scene.py b/tests/test_scene.py index f3d9f3c..860f628 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -1,88 +1,88 @@ -# import pytest -# -# from pytvpaint import george -# from pytvpaint.clip import Clip -# from pytvpaint.project import Project -# from pytvpaint.scene import Scene -# -# -# def test_scene_init(test_project_obj: Project, test_scene: int) -> None: -# scene = Scene(test_scene, test_project_obj) -# assert scene.id == test_scene -# assert scene.project == test_project_obj -# assert scene.position == 1 # It's the second scene -# -# -# def test_scene_new_current_project(test_project_obj: Project) -> None: -# scene = Scene.new() -# # The scene's project is the current project -# assert scene.project == test_project_obj -# -# -# def test_scene_new_other_project(test_project_obj: Project) -> None: -# scene = Scene.new(project=test_project_obj) -# # The scene's project is the given project -# assert scene.project == test_project_obj -# -# -# def test_scene_current_scene_id_static(test_scene: int) -> None: -# assert Scene.current_scene_id() == test_scene -# -# -# def test_scene_current_scene_static(test_scene_obj: Scene) -> None: -# assert Scene.current_scene() == test_scene_obj -# -# -# def test_scene_is_current(test_project_obj: Project, test_scene_obj: Scene) -> None: -# assert test_scene_obj.is_current -# george.tv_scene_new() -# assert not test_scene_obj.is_current -# -# -# def test_scene_make_current(test_scene_obj: Scene) -> None: -# george.tv_scene_new() -# -# # Because we created another scene which is current -# assert not test_scene_obj.is_current -# -# test_scene_obj.make_current() -# assert test_scene_obj.is_current -# -# -# @pytest.mark.parametrize("pos", range(5)) -# def test_scene_position_getter( -# test_scene_obj: Scene, -# create_some_scenes: None, -# pos: int, -# ) -> None: -# george.tv_scene_move(test_scene_obj.id, pos) -# assert pos == test_scene_obj.position -# -# -# @pytest.mark.parametrize("pos", range(5)) -# def test_scene_position_setter( -# test_scene_obj: Scene, -# create_some_scenes: None, -# pos: int, -# ) -> None: -# test_scene_obj.position = pos -# assert test_scene_obj.position == pos -# -# -# def test_scene_clips( -# test_scene_obj: Scene, -# create_some_clips: list[Clip], -# ) -> None: -# assert list(Scene.current_scene().clips) == create_some_clips -# -# -# def test_scene_duplicate(test_scene_obj: Scene) -> None: -# dup = test_scene_obj.duplicate() -# assert test_scene_obj != dup -# -# -# def test_scene_remove(test_scene_obj: Scene) -> None: -# test_scene_obj.remove() -# -# with pytest.raises(ValueError, match="has been removed"): -# test_scene_obj.id +import pytest + +from pytvpaint import george +from pytvpaint.clip import Clip +from pytvpaint.project import Project +from pytvpaint.scene import Scene + + +def test_scene_init(test_project_obj: Project, test_scene: int) -> None: + scene = Scene(test_scene, test_project_obj) + assert scene.id == test_scene + assert scene.project == test_project_obj + assert scene.position == 1 # It's the second scene + + +def test_scene_new_current_project(test_project_obj: Project) -> None: + scene = Scene.new() + # The scene's project is the current project + assert scene.project == test_project_obj + + +def test_scene_new_other_project(test_project_obj: Project) -> None: + scene = Scene.new(project=test_project_obj) + # The scene's project is the given project + assert scene.project == test_project_obj + + +def test_scene_current_scene_id_static(test_scene: int) -> None: + assert Scene.current_scene_id() == test_scene + + +def test_scene_current_scene_static(test_scene_obj: Scene) -> None: + assert Scene.current_scene() == test_scene_obj + + +def test_scene_is_current(test_project_obj: Project, test_scene_obj: Scene) -> None: + assert test_scene_obj.is_current + george.tv_scene_new() + assert not test_scene_obj.is_current + + +def test_scene_make_current(test_scene_obj: Scene) -> None: + george.tv_scene_new() + + # Because we created another scene which is current + assert not test_scene_obj.is_current + + test_scene_obj.make_current() + assert test_scene_obj.is_current + + +@pytest.mark.parametrize("pos", range(5)) +def test_scene_position_getter( + test_scene_obj: Scene, + create_some_scenes: None, + pos: int, +) -> None: + george.tv_scene_move(test_scene_obj.id, pos) + assert pos == test_scene_obj.position + + +@pytest.mark.parametrize("pos", range(5)) +def test_scene_position_setter( + test_scene_obj: Scene, + create_some_scenes: None, + pos: int, +) -> None: + test_scene_obj.position = pos + assert test_scene_obj.position == pos + + +def test_scene_clips( + test_scene_obj: Scene, + create_some_clips: list[Clip], +) -> None: + assert list(Scene.current_scene().clips) == create_some_clips + + +def test_scene_duplicate(test_scene_obj: Scene) -> None: + dup = test_scene_obj.duplicate() + assert test_scene_obj != dup + + +def test_scene_remove(test_scene_obj: Scene) -> None: + test_scene_obj.remove() + + with pytest.raises(ValueError, match="has been removed"): + test_scene_obj.id diff --git a/tests/test_utils.py b/tests/test_utils.py index ffb98aa..e860b59 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,25 +1,25 @@ -# from __future__ import annotations -# -# import pytest -# -# from pytvpaint.utils import get_unique_name -# -# -# @pytest.mark.parametrize( -# "test_case", -# [ -# (["cl001"], "cl", "cl002"), -# (["cl"], "cl", "cl2"), -# (["a01", "a001"], "a", "a002"), -# (["a001"], "a001b", "a001b"), -# (["a001"], "a001", "a002"), -# (["cl001", "cl005"], "cl", "cl006"), -# (["001", "003"], "003", "004"), -# (["l"], "tset0", "tset0"), -# ([], "tset0", "tset0"), -# (["cl0001", "cl005"], "cl", "cl0006"), -# ], -# ) -# def test_get_unique_name(test_case: tuple[list[str], str, str]) -> None: -# current_names, name, expected = test_case -# assert get_unique_name(current_names, name) == expected +from __future__ import annotations + +import pytest + +from pytvpaint.utils import get_unique_name + + +@pytest.mark.parametrize( + "test_case", + [ + (["cl001"], "cl", "cl002"), + (["cl"], "cl", "cl2"), + (["a01", "a001"], "a", "a002"), + (["a001"], "a001b", "a001b"), + (["a001"], "a001", "a002"), + (["cl001", "cl005"], "cl", "cl006"), + (["001", "003"], "003", "004"), + (["l"], "tset0", "tset0"), + ([], "tset0", "tset0"), + (["cl0001", "cl005"], "cl", "cl0006"), + ], +) +def test_get_unique_name(test_case: tuple[list[str], str, str]) -> None: + current_names, name, expected = test_case + assert get_unique_name(current_names, name) == expected From 2da263dc56f607430f92436796c35adcdc56ee38 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 24 Mar 2026 17:53:28 +0100 Subject: [PATCH 13/26] CLEAN code for ruff/mypy --- pytvpaint/clip.py | 6 +++--- pytvpaint/george/client/parse.py | 36 ++++++++++++++++---------------- pytvpaint/project.py | 6 +++--- pytvpaint/utils.py | 34 ++++++++++-------------------- tests/conftest.py | 20 ++++++++---------- 5 files changed, 44 insertions(+), 58 deletions(-) diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index 2d31793..e24e00d 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -574,12 +574,12 @@ def convert_range(x: int) -> int: end = max(clip_real_start, end) if frame_set is not None: - start = max(start, convert_range(frame_set.start())) - end = min(end, convert_range(frame_set.end())) + start = max(start, convert_range(int(frame_set.start()))) + end = min(end, convert_range(int(frame_set.end()))) if frame_set.isConsecutive(): frame_set = FrameSet(f"{start}-{end}") else: - frames = [convert_range(f) for f in frame_set.items] + [start, end] + frames = [convert_range(int(f)) for f in frame_set.items] + [start, end] frame_set = FrameSet(frames) else: frame_set = FrameSet(f"{start}-{end}") diff --git a/pytvpaint/george/client/parse.py b/pytvpaint/george/client/parse.py index 9c86795..0e63856 100644 --- a/pytvpaint/george/client/parse.py +++ b/pytvpaint/george/client/parse.py @@ -180,45 +180,45 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: # noqa: C901 clean_val = clean_val.strip("\"'") # Basic Primitives - if cast_type is str: - return cast(T, clean_val) - if cast_type is int: - return cast(T, int(float(clean_val))) - if cast_type is float: - return cast(T, float(clean_val)) - if cast_type is Path: - return cast(T, Path(clean_val)) - if cast_type is bool: - return cast(T, clean_val.lower() in ("true", "1", "yes", "on")) + if isinstance(cast_type, str): + return clean_val + if isinstance(cast_type, int): + return int(float(clean_val)) + if isinstance(cast_type, float): + return float(clean_val) + if isinstance(cast_type, Path): + return Path(clean_val) + if isinstance(cast_type, bool): + return clean_val.lower() in ("true", "1", "yes", "on") # Enums if isinstance(cast_type, type) and issubclass(cast_type, Enum): # By Name with suppress(KeyError): - return cast(T, cast_type[clean_val]) + return cast_type[clean_val] with suppress(KeyError): - return cast(T, cast_type[clean_val.lower()]) + return cast_type[clean_val.lower()] # By value with suppress(ValueError): - return cast(T, cast_type(clean_val)) + return cast_type(clean_val) with suppress(ValueError): - return cast(T, cast_type(clean_val.lower())) + return cast_type(clean_val.lower()) # Exhaustive case-insensitive search for names and string values clean_lower = clean_val.lower() for member in cast_type: if member.name.lower() == clean_lower: - return cast(T, member) + return member if isinstance(member.value, str) and member.value.lower() == clean_lower: - return cast(T, member) + return member # By int value (for "1" -> 1) with suppress(ValueError): - return cast(T, cast_type(int(float(clean_val)))) + return cast_type(int(float(clean_val))) # By Index (Position in definition) if clean_val.isdigit(): with suppress(IndexError): - return cast(T, list(cast_type)[int(clean_val)]) + return list(cast_type)[int(clean_val)] raise ValueError(f"'{clean_val}' is not a valid {cast_type.__name__}") diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 7380e96..0091e2b 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -632,12 +632,12 @@ def convert_range(x: int) -> int: end = convert_range(end) if frame_set is not None: - start = max(start, convert_range(frame_set.start())) - end = min(end, convert_range(frame_set.end())) + start = max(start, convert_range(int(frame_set.start()))) + end = min(end, convert_range(int(frame_set.end()))) if frame_set.isConsecutive(): frame_set = FrameSet(f"{start}-{end}") else: - frames = [convert_range(f) for f in frame_set.items] + [start, end] + frames = [convert_range(int(f)) for f in frame_set.items] + [start, end] frame_set = FrameSet(frames) else: frame_set = FrameSet(f"{start}-{end}") diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index aa7a9a0..511b5aa 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -7,13 +7,7 @@ from abc import ABC, abstractmethod from collections.abc import Generator, Iterable, Iterator from pathlib import Path -from typing import ( - TYPE_CHECKING, - Any, - Callable, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast, runtime_checkable from fileseq.filesequence import FileSequence from fileseq.frameset import FrameSet @@ -146,14 +140,14 @@ def _render( # noqa: C901 background_mode: george.BackgroundMode | None = None, format_opts: list[str] | None = None, ) -> None: - if start or end and not frame_set: + if (start or end) and not frame_set: log.warning("Use of `start` and `end` is deprecated, prefer using `fileseq.FrameSet()` instead.") if start and end and frame_set and any(f not in frame_set for f in (start, end)): log.warning("`start` and/or `end` outside of `frame_set` range, will prioritize FrameSet.") if frame_set is not None and not (start and end): - start = start if start is not None else frame_set.start() - end = end if end is not None else frame_set.end() + start = start if start is not None else int(frame_set.start()) + end = end if end is not None else int(frame_set.end()) # finds range if none provided or in path and clamps it to the correct context file_sequence, start, end, is_sequence, is_image = handle_output_range( @@ -419,31 +413,25 @@ def restore_current_frame(tvp_element: HasCurrentFrame, frame: int) -> Generator tvp_element.current_frame = previous_frame +@runtime_checkable class _TVPElement(Protocol): @property def id(self) -> int | str: ... - @property def name(self) -> str: ... -class _TVPElementWithPath(_TVPElement, Protocol): - - @property - def path(self) -> Path: ... - - -_TVPElementType = TypeVar("_TVPElementType", bound=_TVPElement) -_TVPElementWithPathType = TypeVar("_TVPElementWithPathType", bound=_TVPElementWithPath) +# We define a single TypeVar bound to the base protocol +TVPElementType = TypeVar("TVPElementType", bound=_TVPElement) def get_tvp_element( - tvp_elements: Iterator[_TVPElementType | _TVPElementWithPathType], + tvp_elements: Iterator[TVPElementType], by_id: int | str | None = None, by_name: str | None = None, by_regex: re.Pattern[str] | None = None, by_path: str | Path | None = None, -) -> _TVPElementType | _TVPElementWithPathType | None: +) -> TVPElementType | None: """Search for a TVPaint element by attributes. Args: @@ -514,8 +502,8 @@ def handle_output_range( # 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() + start = start or int(file_sequence.start()) + end = end or int(file_sequence.end()) # check characteristics of file sequence fseq_has_range = frame_set and len(frame_set) > 1 diff --git a/tests/conftest.py b/tests/conftest.py index 4e14b72..e4ffc61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -245,17 +245,15 @@ def wav_file(tmp_path_factory: pytest.TempPathFactory) -> Path: n_frames = framerate * duration max_amp = 2 ** (amp_width * 8 - 1) - 1 - wav = wave.open(str(wav_path), "wb") - wav.setnchannels(1) - wav.setsampwidth(amp_width) - wav.setframerate(framerate) - - for _ in range(n_frames): - value = randint(-max_amp, max_amp) - data = struct.pack(" Date: Tue, 7 Apr 2026 10:07:10 +0200 Subject: [PATCH 14/26] UPDATE documentation and pyproject.toml files --- .github/workflows/check.yml | 6 +-- .github/workflows/docs-deploy.yml | 6 +-- .gitignore | 2 +- docs/api/objects/guideline_line.md | 3 ++ docs/contributing/developer_setup.md | 81 +++++++++++++++++++--------- docs/cpp/building.md | 15 +++--- docs/installation.md | 6 --- docs/limitations.md | 55 +++++++++++++------ docs/usage/rendering.md | 4 +- docs/usage/usage.md | 3 +- mkdocs.yml | 4 +- pyproject.toml | 34 ++++++++---- pytvpaint/camera.py | 22 ++++++-- pytvpaint/clip.py | 9 ++-- pytvpaint/george/grg_clip.py | 2 +- pytvpaint/george/grg_guideline.py | 2 +- pytvpaint/george/grg_layer.py | 2 +- pytvpaint/guideline.py | 4 +- pytvpaint/layer.py | 33 +++++++----- pytvpaint/project.py | 9 ++-- pytvpaint/utils.py | 13 ++--- tests/conftest.py | 66 ++++++++++++++++++++--- tests/test_layer.py | 20 ++++++- 23 files changed, 283 insertions(+), 118 deletions(-) create mode 100644 docs/api/objects/guideline_line.md diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d75ca65..8cf2467 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,12 +19,12 @@ jobs: - name: Formatting check if: always() - run: hatch run black --check . + run: hatch run dev:black --check . - name: Linting if: always() - run: hatch run ruff check . + run: hatch run dev:ruff check . - name: Type checking if: always() - run: hatch run mypy . \ No newline at end of file + run: hatch run dev:mypy . \ No newline at end of file diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 2229f29..78b7b5f 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -45,14 +45,14 @@ jobs: # Build the docs on PRs - name: Verify Docs Build (on Pull Requests) if: github.event_name == 'pull_request' - run: hatch run docs:mkdocs build --strict + run: hatch run docs:build # Deploy Dev Documentation (on main) - name: Deploy Dev Documentation (on main) if: github.ref == 'refs/heads/main' - run: hatch run docs:mike deploy dev --push + run: hatch run docs:deploy-dev # Deploy Release Documentation (on tags) - name: Deploy Release Documentation (on tags) if: startsWith(github.ref, 'refs/tags/') - run: hatch run docs:mike deploy ${{ github.ref_name }} latest --update-aliases --push \ No newline at end of file + run: hatch run docs:deploy-release ${{ github.ref_name }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c934d60..fd68e37 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,7 @@ ENV/ env.bak/ venv.bak/ -# mkdocs documentation +# zensical documentation /site # mypy diff --git a/docs/api/objects/guideline_line.md b/docs/api/objects/guideline_line.md new file mode 100644 index 0000000..db788b4 --- /dev/null +++ b/docs/api/objects/guideline_line.md @@ -0,0 +1,3 @@ +# GuidelineImage class + +::: pytvpaint.guideline.GuidelineLine \ No newline at end of file diff --git a/docs/contributing/developer_setup.md b/docs/contributing/developer_setup.md index ffd5561..abee364 100644 --- a/docs/contributing/developer_setup.md +++ b/docs/contributing/developer_setup.md @@ -19,22 +19,33 @@ First clone the repository: ❯ git clone git@github.com:brunchstudio/pytvpaint.git ``` +then `cd` into the repo directory + +```shell +❯ cd /path/to/the/repo +``` + Install Hatch if needed: ```shell ❯ pip install hatch ``` -Then install the dependencies in a virtualenv with Hatch: -```shell -❯ hatch env create -``` +!!! info -Note that this will install the library and default development dependencies. To install optional [environments](https://hatch.pypa.io/latest/config/environment/overview/) (to build the documentation, etc...) you can specify the environment name: + You may create the env and install the dependencies in a virtualenv with Hatch if you want to, + but it is not necessary for hatch as it creates the environment on its own when needed: + + ```shell + ❯ hatch env create # create the venv + ❯ hatch shell # Enter the new venv shell + ``` -```shell -❯ hatch env create docs -``` + Note that this will install the library and default development dependencies. To install optional [environments](https://hatch.pypa.io/latest/config/environment/overview/) (to build the documentation, etc...) you can specify the environment name: + + ```shell + ❯ hatch env create docs + ``` ### Code formatting @@ -42,25 +53,35 @@ We use [Black](https://black.readthedocs.io/en/stable/) to ensure that the code ```shell # Will format all the .py files in the current directory -(venv) ❯ black . +❯ hatch run dev:black . # To only check if the format is correct -(venv) ❯ black --check . +❯ hatch run dev:black --check . ``` -!!! Tip +Or use the `pyproject.toml` shortcut : - Use `hatch shell` to enter a new shell in the virtualenv. In this page commands marked `(venv) ❯` can also be run with `hatch run ` +```shell +# this is the same as running `black .` +❯ hatch run dev:format +``` ### Linting We also use [Ruff](https://docs.astral.sh/ruff/) as a linter. It combines a lot of rules from other projects like Flake8, pyupgrade, pydocstyle, isort, etc... ```shell -(venv) ❯ ruff . +❯ hatch run dev:ruff check . # Will apply autofixes -(venv) ❯ ruff --fix . +❯ hatch run dev:ruff check --fix . +``` + +Or use the `pyproject.toml` shortcut : + +```shell +# this is the same as running `ruff check --fix .` +❯ hatch run dev:lint ``` ### Type checking @@ -68,7 +89,21 @@ We also use [Ruff](https://docs.astral.sh/ruff/) as a linter. It combines a lot 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 editing the codebase. ```shell -(venv) ❯ mypy . +❯ hatch run dev:mypy . +``` + +Or use the `pyproject.toml` shortcut : + +```shell +# this is the same as running `mypy .` +❯ hatch run dev:typecheck +``` + +You can also use this shortcut to run all the steps above combined by running: + +```shell +# This will run format then lint then typecheck. +❯ hatch run dev:all ``` !!! info @@ -77,18 +112,16 @@ Mypy is the go-to static type checker for Python. It ensures that variables and ### 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 provides the Material look as well as some other nice features. +The documentation is built using [Zensical](https://zensical.org/) which is a static site generator that uses Markdown as the source format. You can either run the development server or build the entire documentation: ```shell # Will serve the doc on [http://127.0.0.1:8000](http://127.0.0.1:8000) with hot reload -(venv) ❯ mkdocs serve +❯ hatch run docs:serve # Build the doc as static files -(venv) ❯ mkdocs build +❯ hatch run docs: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. @@ -120,14 +153,14 @@ To run them, use the following commands: ```shell # run all the tests -(venv) ❯ pytest +❯ hatch run test:pytest # Run with verbosity enabled (use PYTVPAINT_LOG_LEVEL to DEBUG) to see George commands -(venv) ❯ pytest -v -s +❯ hatch run test:pytest -v -s # Only run specific tests with pattern matching -(venv) ❯ pytest -k test_tv_clip +❯ hatch run test:pytest -k test_tv_clip # See the coverage statistics with pytest-cov -(venv) ❯ pytest --cov=pytvpaint +❯ hatch run test:pytest --cov=pytvpaint ``` \ No newline at end of file diff --git a/docs/cpp/building.md b/docs/cpp/building.md index e606bf0..a60987b 100644 --- a/docs/cpp/building.md +++ b/docs/cpp/building.md @@ -8,17 +8,18 @@ To install the build dependencies, we use [Conan](https://conan.io/) which is a C/C++ package manager. -To install it, use the virtualenv provided by [Poetry](https://python-poetry.org/): +To install it, use [Hatch](https://hatch.pypa.io/). + +If you do not have Hatch, you can install it via Python pip : ```shell -❯ hatch env create # create the venv -❯ hatch shell # Enter the new venv shell +❯ pip install hatch ``` Then configure your Conan compilation profile: ```shell -(venv) ❯ conan profile detect +❯ hatch run conan profile detect ``` ### Windows @@ -43,7 +44,7 @@ os=Windows To check if your profile is correct, use: ```shell -(venv) ❯ conan profile show +❯ hatch run conan profile show ``` #### Install the C++ dependencies @@ -51,10 +52,10 @@ To check if your profile is correct, use: Install the dependencies specified in [`conanfile.txt`](https://github.com/brunchstudio/tvpaint-rpc/blob/main/conanfile.txt): ```shell -(venv) ❯ conan install . --output-folder=build --build=missing +❯ hatch run conan install . --output-folder=build --build=missing ``` -The above command generates CMake build files that helps finding those libraries. +The above command generates CMake build files that helps to find those libraries. #### Build diff --git a/docs/installation.md b/docs/installation.md index c501422..1397a70 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -27,12 +27,6 @@ Simply install it with Pip: ❯ pip install pytvpaint ``` -or use [Poetry](https://python-poetry.org/): - -```console -❯ poetry add pytvpaint -``` - !!! success You are now ready to start coding in Python for TVPaint! diff --git a/docs/limitations.md b/docs/limitations.md index d980b63..b87d3f1 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -98,12 +98,12 @@ issues in the table below: | `tv_ProjectSaveAudioDependencies` and `tv_ProjectSaveVideoDependencies` | Missing arguments in documentation rendering the function useless, thankfully someone provided the correct details [here](http://tvpaint.net/forum/viewtopic.php?p=136709) | -## TVPaint 12 Bugs and Breaking Changes +## TVPaint 12 Bugs and Breaking Changes: -TVPaint 12 introduces a lot of welcome changes (especially for the artists) and some new needed function for -developers. However, it also introduces a lot of breaking changes and some new bugs. +TVPaint 12 introduces a lot of welcome changes (especially for the artists) and some new needed function for developers. +However, it also introduces a lot of breaking changes and some new bugs. -To deal with these changes as well as the new functions exclusive to TVP 12, we added a couple decorators and helper functions : +To deal with these changes as well as the new functions exclusive to TVP 12, we added a couple decorators and functions : | Method | Description | |:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| @@ -112,41 +112,62 @@ To deal with these changes as well as the new functions exclusive to TVP 12, we | [`is_tvp_version_below_12`](api/george/misc.md#pytvpaint.george.grg_base.is_tvp_version_below_12) | Returns True if the tvp instances version is below 12, False otherwise. | -Below is also a list of the current breaking changes and bugs we noticed during development +Below is also a list of the current breaking changes and bugs we noticed during development : ### C++ Plugin : * [BUG] plugin PIRF_HIDDEN_REQ doesn't seem to be working anymore, the plugin window is now visible and closing it kills the plugin with no way to restart it without restarting tvpaint. ### Project : * [DEPRECATED/BREAKING] [`tv_ProjectInfo`](api/george/project.md#pytvpaint.george.grg_project.tv_project_info) values of `field_order` have been removed so for now we provide it ourselves. +* [BUG] [`tv_project_save_sequence`](api/george/project.md#pytvpaint.george.grg_project.tv_project_save_sequence) does not render empty instances even if they exist in the timeline. ### Scene : * [BUG] [`tv_SceneCreate`](api/george/scene.md#pytvpaint.george.grg_scene.tv_scene_create) doesn't seem to work. +### Clip: +* [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json %fi does not work for folder structure (in v11 and v12) +* [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json seems to output low quality images, this may use the same function `george.tv_save_image`. + ### Layers : -* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually `tv_CTGGetSource` without the `s` at the end. +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually tv\_CTGGetSource without the `s` at the end. * [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) actually requires and returns layer Ids not names. -* [FEATURE] Some function now return a clear error message which is much appreciated (Ex: [`tv_CTGSource`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_source_add)) -* child layers have no way of knowing if they are in a folder layer or which one. -* Folder layer has no way of knowing which layers are inside the folder. +* Child layers have no way of knowing if they are in a folder layer or which one. +* Folder layer has no way of knowing which layers are its children. * [BUG] Creating a CTG layer from a folder crashes TVPaint. * [BUG] Moving a layer that is already in a folder in the same folder crashes TVPaint. -* [`tv_LayerMove`](api/george/layer.md#pytvpaint.george.grg_layer.tv_layer_move) can now move layers into folders but the position is relative to the root and not the folder, - this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers. -* Not providing a `FolderID` to [`tv_LayerMove`](api/george/layer.md#pytvpaint.george.grg_layer.tv_layer_move) doesn't - move the layer to the root, you just need to move outside the folder range for it to work, which again is pretty tough to do since we can't get the child layers of a folder and therefore their positions. +* `tv_LayerMove` can now move layers into folders but the position is relative to the root and not the folder, this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers in and out of folders. +* Not providing a `FolderID` to `tv_LayerMove` doesn't move the layer to the root, you just need to move outside the folder range for it to work, which again is pretty tough to do since we can't get the child layers of a folder and therefore their positions. * Moving a layer inside a folder can be done without providing a `FolderID`, just by moving the layer in the folder's range. * [BUG] UI doesn't always update when values are set/updated via code, you either have to wait a few seconds for a refresh, or click somewhere else, this might lead to errors when users are using the UI at the same time. * [BUG] CTG layer sometimes takes a while to update in the UI (a few seconds), which means they can be unintentionally edited or reset before the update. -* CameraLayer is not really a layer and most layer functions will ignore it, prefer use of PyTVPaint [`Camera`](api/objects/camera.md#pytvpaint.camera.Camera) object instead. -* [BUG] Selecting the Camera Layer in the UI now disables/grays out most layer related tools (this is a new behaviour), this causes a lot of errors when using pipeline tools or - TVPaint panels as TVPaint now raises an error prompt/message anytime you try to use a tool that is not camera related when the camera layer is selected. +* CameraLayer is not really a layer and most layer functions will ignore it, prefer use of PyTVPaint `Camera` object instead. +* [BUG] Selecting the Camera Layer in the UI now disables/grays out most layer related tools (this is a new behaviour), this causes a lot of errors when using pipeline tools or TVPaint panels as TVPaint now raises an error messages anytime you try to use a tool that is not camera related when the camera layer is selected. The problem is that the Camera layer is now also the default layer and selected by default when creating/opening any project. +* [BUG] `tv_LayerRename` does not work in TVP12 and will replace the name with an empty string, this is either a new bug or there is now a new argument that is needed but not documented. +* default layers name is now Anim\_X and when these layers are queried for their names they return an empty string. +* [BUG] `tv_layer_move` still sets position at (value-1) when value is superior to 0. +* [BUG] `tv_preserve_set` does not seem to work in TVP12 +* [BUG] `tv_LayerColor setcolor` will fail when the `name` argument is provided. +* [BUG] `tv_LayerColor setcolor` no longer returns -1 when given a bad color index. +* [BUG] `tv_layer_move` no longer returns -1 when given a bad position. +* [BUG] `tv_LayerCreate` should not be used when an empty string as it will consider the new variable `layer_type` as the name. +* [BUG] Many layer functions that used to return -1 when given a bad layer id or position no longer do so, this can lead to silent errors and breaking behaviour, this also breaks any way to check/validate these functions return values. The functions are : + * `tv_layer_set` + * `tv_LayerSelection` + * `tv_layer_kill` + * `tv_LayerBlendingMode` + * `tv_LayerStencil` + * `tv_LayerPreBehavior` + * `tv_LayerPostBehavior` + * `tv_LayerLockPosition` + * `tv_LayerMarkSet` ### Camera : -* [DEPRECATED/BREAKING] Camera.anti_aliasing always returns 1 +* [DEPRECATED/BREAKING] [`Camera.anti_aliasing`](api/objects/camera.md#pytvpaint.camera.Camera.anti_aliasing) now always returns 1. * [DEPRECATED/BREAKING] [`Camera.fps`](api/objects/camera.md#pytvpaint.camera.Camera.fps) no longer returns/sets fps, now only fps is Project fps. * [BUG] Most Camera values can still be edited/queried using [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) but they are not immediately reflected in the UI, and so they can be unintentionally edited or reset. * [DEPRECATED/BREAKING] [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) values of pixel aspect ratio and fps have been "swapped" (since camera fps is no longer provided). * [BUG] [`tv_CameraInterpolation`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_interpolation) doesn't work properly in TVP12 and always returns an "empty" point. +### Guidelines: +* `tv_GuidelineModify "marks"` doesn't seem to work, values are never changed, added function with a warning. \ No newline at end of file diff --git a/docs/usage/rendering.md b/docs/usage/rendering.md index 22e425f..e8e6524 100644 --- a/docs/usage/rendering.md +++ b/docs/usage/rendering.md @@ -63,9 +63,9 @@ as these have no range selection options. We will also ignore all the clip expor same way as `tv_SaveSequence` (which we will review below) when handling ranges. | Method | Description | Can Render Camera | -| :--------------------------------------------------------------------------------------------------------- | :---------------- | ----------------- | +|:-----------------------------------------------------------------------------------------------------------| :---------------- | ----------------- | | [`tv_ProjectSaveSequence`](../api/george/project.md#pytvpaint.george.grg_project.tv_project_save_sequence) | Renders a project | True | -| [`tv_SaveSequence`](../api/george/layer.md#pytvpaint.george.grg_clip.tv_save_sequence) | Renders a clip | False | +| [`tv_SaveSequence`](../api/george/clip.md#pytvpaint.george.grg_clip.tv_save_sequence) | Renders a clip | False | ### Setup diff --git a/docs/usage/usage.md b/docs/usage/usage.md index f7ab054..fbb3086 100644 --- a/docs/usage/usage.md +++ b/docs/usage/usage.md @@ -13,8 +13,9 @@ their George counterparts. These can be imported from `pytvpaint.george.*`. PyTVPaint can be configured using these variables: | 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_LOG_PATH` | `` | Log file output path, default is `` | | `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` | Whether or not PyTVPaint should automatically connect to the WebSocket server at startup (module import). Accepts 0 or 1. | diff --git a/mkdocs.yml b/mkdocs.yml index 1392524..58f553c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -108,7 +108,7 @@ plugins: handlers: python: paths: [.] - import: + inventories: - https://docs.python.org/3/objects.inv options: members_order: source @@ -120,6 +120,8 @@ plugins: filters: ["!^_"] show_signature_annotations: True merge_init_into_class: True + show_if_no_docstring: true + inherited_members: true markdown_extensions: - admonition diff --git a/pyproject.toml b/pyproject.toml index 2e0e28e..402cd61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,15 @@ dependencies = [ Repository = "https://github.com/brunchstudio/pytvpaint" Documentation = "https://brunchstudio.github.io/pytvpaint/" +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "pytvpaint/_version.py" + +[tool.hatch.build.targets.sdist] +exclude = ["/.github"] + [project.optional-dependencies] dev = [ "black>=24.2.0", @@ -39,27 +48,32 @@ test = [ "pytest-html>=4.0.0", ] docs = [ - "mkdocs-material>=9.5.10", + "zensical", "mkdocstrings[python]>=0.24.0", "mike", ] -[tool.hatch.version] -source = "vcs" +[tool.hatch.envs.default] +features = ["dev", "test"] -[tool.hatch.build.hooks.vcs] -version-file = "pytvpaint/_version.py" +[tool.hatch.envs.dev] +features = ["dev"] -[tool.hatch.build.targets.sdist] -exclude = ["/.github"] +[tool.hatch.envs.dev.scripts] +format = "black ." +lint = "ruff check --fix ." +typecheck = "mypy ." +all = ["format", "lint", "typecheck"] -[tool.hatch.envs.default] -features = ["dev", "test"] +[tool.hatch.envs.test] +features = ["test"] [tool.hatch.envs.docs] features = ["docs"] [tool.hatch.envs.docs.scripts] +build = "zensical build" +serve = "zensical serve" deploy-dev = "mike deploy dev --push" deploy-release = "mike deploy {args} latest --update-aliases --push" @@ -120,7 +134,7 @@ disallow_any_generics = true disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true -disallow_untyped_decorators = true +disallow_untyped_decorators = false disallow_untyped_defs = true exclude = "package.py" follow_imports = "normal" diff --git a/pytvpaint/camera.py b/pytvpaint/camera.py index c2ffd44..72cfb25 100644 --- a/pytvpaint/camera.py +++ b/pytvpaint/camera.py @@ -58,6 +58,7 @@ def width(self) -> int: @width.setter @set_as_current def width(self, value: int) -> None: + """The sensor width of the camera.""" george.tv_camera_info_set(width=value, height=self.height) @refreshed_property @@ -69,6 +70,7 @@ def height(self) -> int: @height.setter @set_as_current def height(self, value: int) -> None: + """The sensor height of the camera.""" george.tv_camera_info_set(height=value, width=self.width) @refreshed_property @@ -93,6 +95,12 @@ def fps(self) -> float: "For now, in TVP 12, it always sets 1.0 but will be removed in future versions." ) def fps(self, value: float) -> None: + """The framerate of the camera. + + Warning: + DEPRECATED: Property `Camera.fps` is not recommended for use in TVP 12, use Project.fps instead. + For now, in TVP 12, it always returns 1.0 but will be removed in future versions. + """ george.tv_camera_info_set( self.width, self.height, @@ -109,6 +117,7 @@ def pixel_aspect_ratio(self) -> float: @pixel_aspect_ratio.setter @set_as_current def pixel_aspect_ratio(self, value: float) -> None: + """The pixel aspect ratio of the camera.""" george.tv_camera_info_set( self.width, self.height, @@ -141,6 +150,7 @@ def field_order(self) -> george.FieldOrder: @field_order.setter @set_as_current def field_order(self, value: george.FieldOrder) -> None: + """The field order of the camera.""" george.tv_camera_info_set(self.width, self.height, field_order=value) @property @@ -267,6 +277,7 @@ def x(self) -> float: @x.setter def x(self, value: float) -> None: + """The x coordinate of the point.""" current_data = george.tv_camera_enum_points(self.index) george.tv_camera_set_point( self.index, @@ -283,6 +294,7 @@ def y(self) -> float: @y.setter def y(self, value: float) -> None: + """The y coordinate of the point.""" current_data = george.tv_camera_enum_points(self.index) george.tv_camera_set_point( self.index, @@ -299,6 +311,7 @@ def angle(self) -> float: @angle.setter def angle(self, value: float) -> None: + """The angle of the camera at the point.""" current_data = george.tv_camera_enum_points(self.index) george.tv_camera_set_point( self.index, @@ -315,6 +328,7 @@ def scale(self) -> float: @scale.setter def scale(self, value: float) -> None: + """The scale of the camera at the point.""" current_data = george.tv_camera_enum_points(self.id) george.tv_camera_set_point( self.id, @@ -326,12 +340,12 @@ def scale(self, value: float) -> None: @property def width(self) -> float: - """The scale of the camera at the point.""" + """The width of the camera at the point.""" return self.camera.clip.project.width * (self.scale * 0.01) @property def height(self) -> float: - """The scale of the camera at the point.""" + """The height of the camera at the point.""" return self.camera.clip.project.height * (self.scale * 0.01) @classmethod @@ -401,7 +415,7 @@ def remove(self) -> None: the FrameCameraPoint instance is read-only and cannot be removed as it doesn't really exist """ log.warning( - "Read-Only InterpolationCameraPoint cannot be deleted as it doesn't really exist, " "ignoring request." + "Read-Only InterpolationCameraPoint cannot be deleted as it doesn't really exist, ignoring request." ) return @@ -448,5 +462,5 @@ def remove(self) -> None: Warning: the FrameCameraPoint instance is read-only and cannot be removed as it doesn't really exist """ - log.warning("Read-Only FrameCameraPoint cannot be deleted as it doesn't really exist, " "ignoring request.") + log.warning("Read-Only FrameCameraPoint cannot be deleted as it doesn't really exist, ignoring request.") return diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index e24e00d..e284177 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -579,7 +579,7 @@ def convert_range(x: int) -> int: if frame_set.isConsecutive(): frame_set = FrameSet(f"{start}-{end}") else: - frames = [convert_range(int(f)) for f in frame_set.items] + [start, end] + frames = [convert_range(int(f)) for f in frame_set.items if start <= convert_range(int(f)) <= end] frame_set = FrameSet(frames) else: frame_set = FrameSet(f"{start}-{end}") @@ -598,7 +598,7 @@ def render( alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = None, format_opts: list[str] | None = None, - ) -> None: + ) -> Path | FileSequence: """Render the clip to a single frame or frame sequence or movie. Args: @@ -625,11 +625,14 @@ def render( Warning: Even though pytvpaint does a pretty good job of correcting the frame ranges for rendering, we're still encountering some weird edge cases where TVPaint will consider the range invalid for seemingly no reason. + + Returns: + the output file path or sequence """ default_start = self.mark_in or self.start default_end = self.mark_out or self.end - self._render( + return self._render( output_path=output_path, default_start=default_start, default_end=default_end, diff --git a/pytvpaint/george/grg_clip.py b/pytvpaint/george/grg_clip.py index cf89059..c7f58b8 100644 --- a/pytvpaint/george/grg_clip.py +++ b/pytvpaint/george/grg_clip.py @@ -279,7 +279,7 @@ def tv_save_sequence( if not export_path.parent.exists(): raise NotADirectoryError( - "Can't save the sequence because parent" f"folder does not exist: {export_path.parent.as_posix()}" + f"Can't save the sequence because parentfolder does not exist: {export_path.parent.as_posix()}" ) args: list[Any] = [export_path.as_posix()] diff --git a/pytvpaint/george/grg_guideline.py b/pytvpaint/george/grg_guideline.py index f04be0c..10a0679 100644 --- a/pytvpaint/george/grg_guideline.py +++ b/pytvpaint/george/grg_guideline.py @@ -892,7 +892,7 @@ def tv_guideline_modify_marks_set( ) -> None: """Set info for the marks guideline at the given position. - Warnings: + Warning: function `tv_GuidelineModify` doesn't seem to work in tvpaint, values are never changed. """ diff --git a/pytvpaint/george/grg_layer.py b/pytvpaint/george/grg_layer.py index bc6009e..cc8af9c 100644 --- a/pytvpaint/george/grg_layer.py +++ b/pytvpaint/george/grg_layer.py @@ -1278,7 +1278,7 @@ def tv_exposure_prev() -> int: def tv_save_image(export_path: Path | str) -> None: """Save the current image of the current layer. - Warnings: + Warning: This function outputs very low quality images, we recommend using other rendering functions. Raises: diff --git a/pytvpaint/guideline.py b/pytvpaint/guideline.py index 12c054a..efe7e20 100644 --- a/pytvpaint/guideline.py +++ b/pytvpaint/guideline.py @@ -628,7 +628,7 @@ def count_x(self) -> int: def count_x(self, value: int) -> None: """The number of vertical marks. - Warnings: + Warning: function GuidelineMarks.x doesn't seem to work in tvpaint, values are never changed. """ george.tv_guideline_modify_marks_set(self.position, count_x=value) @@ -642,7 +642,7 @@ def count_y(self) -> int: def count_y(self, value: int) -> None: """The number of vertical marks. - Warnings: + Warning: function GuidelineMarks.y doesn't seem to work in tvpaint, values are never changed. """ george.tv_guideline_modify_marks_set(self.position, count_y=value) diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index f444dd0..fab09bb 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -904,17 +904,19 @@ def render( output_path: Path | str | FileSequence, start: int | None = None, end: int | None = None, + frame_set: FrameSet | None = None, use_camera: bool = False, alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = None, format_opts: list[str] | None = None, - ) -> None: + ) -> Path | FileSequence: """Render the layer to a single frame or frame sequence or movie. Args: output_path: a single file or file sequence pattern start: the start frame to render the layer's start if None. Defaults to None. end: the end frame to render or the layer's end if None. Defaults to None. + frame_set: a FrameSet with the frames/range to render. Defaults to None. use_camera: use the camera for rendering, otherwise render the whole canvas. Defaults to False. alpha_mode: the alpha mode for rendering. Defaults to george.AlphaSaveMode.PREMULTIPLY. background_mode: the background mode for rendering. Defaults to None. @@ -933,11 +935,14 @@ def render( Warning: Even tough pytvpaint does a pretty good job of correcting the frame ranges for rendering, we're still encountering some weird edge cases where TVPaint will consider the range invalid for seemingly no reason. + + Returns: + the output file path or sequence """ start = self.start if start is None else start end = self.end if end is None else end - frame_set = FrameSet(f"{start}-{end}") - self.clip.render( + frame_set = frame_set or FrameSet(f"{start}-{end}") + return self.clip.render( output_path=output_path, frame_set=frame_set, use_camera=use_camera, @@ -986,25 +991,23 @@ def render_frame( background_mode=background_mode, format_opts=format_opts, ) - return export_path + return Path(export_path) @set_as_current def render_instances( self, export_path: Path | str | FileSequence, - start: int | None = None, - end: int | None = None, + frame_set: FrameSet | None = None, alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = None, format_opts: list[str] | None = None, use_camera: bool = False, - ) -> None: + ) -> Path | FileSequence: """Render all layer instances in the provided range for the current layer. Args: export_path: the export path (the extension determines the output format) - start: the start frame to render the layer's start if None. Defaults to None. - end: the end frame to render or the layer's end if None. Defaults to None. + frame_set: Render only the instances within the provided frameset. Defaults to None. alpha_mode: the render alpha mode background_mode: the render background mode format_opts: custom output format options to pass when rendering @@ -1019,10 +1022,15 @@ def render_instances( FileSequence: instances output sequence """ frames = [layer_instance.start for layer_instance in self.instances] + if frame_set is not None: + frame_set = FrameSet([f for f in frame_set.items if f in frames]) + else: + frame_set = FrameSet(frames) or frame_set - self.clip.render( + print("frame_set ===> ", frame_set) + return self.clip.render( output_path=export_path, - frame_set=FrameSet(frames), + frame_set=frame_set, use_camera=use_camera, layer_selection=[self], alpha_mode=alpha_mode, @@ -1281,8 +1289,7 @@ def add_instance( if start and self.get_instance(start): raise ValueError( - "An instance already exists at the designated frame range. " - "Edit or delete it before adding a new one." + "An instance already exists at the designated frame range. Edit or delete it before adding a new one." ) start = start if start is not None else self.clip.current_frame diff --git a/pytvpaint/project.py b/pytvpaint/project.py index 0091e2b..a5a5ba2 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -637,7 +637,7 @@ def convert_range(x: int) -> int: if frame_set.isConsecutive(): frame_set = FrameSet(f"{start}-{end}") else: - frames = [convert_range(int(f)) for f in frame_set.items] + [start, end] + frames = [convert_range(int(f)) for f in frame_set.items if start <= convert_range(int(f)) <= end] frame_set = FrameSet(frames) else: frame_set = FrameSet(f"{start}-{end}") @@ -655,7 +655,7 @@ def render( alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = None, format_opts: list[str] | None = None, - ) -> None: + ) -> Path | FileSequence: """Render the project to a single frame or frame sequence or movie. Args: @@ -681,11 +681,14 @@ def render( Warning: Even tough pytvpaint does a pretty good job of correcting the frame ranges for rendering, we're still encountering some weird edge cases where TVPaint will consider the range invalid for seemingly no reason. + + Returns: + the output file path or sequence """ default_start = self.mark_in or self.start_frame default_end = self.mark_out or self.end_frame - self._render( + return self._render( output_path=output_path, default_start=default_start, default_end=default_end, diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index 511b5aa..ed90792 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -139,7 +139,7 @@ def _render( # noqa: C901 alpha_mode: george.AlphaSaveMode = george.AlphaSaveMode.PREMULTIPLY, background_mode: george.BackgroundMode | None = None, format_opts: list[str] | None = None, - ) -> None: + ) -> Path | FileSequence: if (start or end) and not frame_set: log.warning("Use of `start` and `end` is deprecated, prefer using `fileseq.FrameSet()` instead.") if start and end and frame_set and any(f not in frame_set for f in (start, end)): @@ -166,7 +166,7 @@ def _render( # noqa: C901 raise ValueError("TVPaint will not render a movie that contains a single frame") # get first frame, tvp doesn't understand vfx padding `#` - if is_image or file_sequence.padding(): + if is_image and is_sequence: first_frame = Path(file_sequence.frame(file_sequence.start())) else: first_frame = Path(str(output_path)) @@ -209,11 +209,12 @@ def _render( # noqa: C901 # not all frames found 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}" + f"Not all frames found, missing frames ({missing_frames}) in sequence : {output_path}" ) - else: - if not first_frame.exists(): - raise FileNotFoundError(f"Could not find output at : {first_frame.as_posix()}") + return file_sequence + if not first_frame.exists(): + raise FileNotFoundError(f"Could not find output at : {first_frame.as_posix()}") + return first_frame def get_unique_name(names: Iterable[str], stub: str) -> str: diff --git a/tests/conftest.py b/tests/conftest.py index e4ffc61..3359391 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -182,27 +182,76 @@ def count_up_generate(test_clip_obj: Clip) -> None: george.tv_update_undo() -def png_generate(path: Path, width: int, height: int) -> None: +# 5x7 Bitmap font definition for digits 0-9 +FONT_BITMAPS = { + "0": [" 000 ", "0 0", "0 0", "0 0", "0 0", "0 0", " 000 "], + "1": [" 1 ", " 11 ", " 1 ", " 1 ", " 1 ", " 1 ", " 111 "], + "2": [" 222 ", "2 2", " 2", " 22 ", " 2 ", "2 ", "22222"], + "3": [" 333 ", "3 3", " 3", " 33 ", " 3", "3 3", " 333 "], + "4": [" 4 ", " 44 ", " 4 4 ", "4 4 ", "44444", " 4 ", " 4 "], + "5": ["55555", "5 ", "5555 ", " 5", " 5", "5 5", " 555 "], + "6": [" 666 ", "6 ", "6666 ", "6 6", "6 6", "6 6", " 666 "], + "7": ["77777", " 7", " 7 ", " 7 ", " 7 ", "7 ", "7 "], + "8": [" 888 ", "8 8", "8 8", " 888 ", "8 8", "8 8", " 888 "], + "9": [" 999 ", "9 9", "9 9", " 9999", " 9", " 9", " 999 "], +} + + +def png_generate_with_index(path: Path, width: int, height: int, index: int) -> None: """ Generates a valid grayscale PNG image file using pure Python. - No external dependencies required. + Renders the numeric index in the center of the image. """ signature = b"\x89PNG\r\n\x1a\n" def make_chunk(chunk_type: bytes, data: bytes) -> bytes: - """Helper to create a standard PNG chunk with length and CRC32.""" length = struct.pack(">I", len(data)) crc = struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF) return length + chunk_type + data + crc + # IHDR: Bit depth 8, Color Type 0 (Grayscale) ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 0, 0, 0, 0) ihdr = make_chunk(b"IHDR", ihdr_data) + # Initialize canvas with a dark gray background + row_size = width + 1 raw_data = bytearray() for _ in range(height): raw_data.append(0) # Filter type 0 - for _ in range(width): - raw_data.append(randint(0, 255)) + raw_data.extend([40] * width) + + # Geometric calculation for text centering + index_str = str(index) + scale = 8 # Magnification multiplier for the 5x7 font + char_w, char_h = 5, 7 + spacing = 1 + + total_w = (len(index_str) * char_w + (len(index_str) - 1) * spacing) * scale + total_h = char_h * scale + + start_x = max(0, (width - total_w) // 2) + start_y = max(0, (height - total_h) // 2) + + # Rasterize font onto the 1D buffer + current_x = start_x + for char in index_str: + bitmap = FONT_BITMAPS.get(char, FONT_BITMAPS["0"]) + for row_idx, row in enumerate(bitmap): + for col_idx, pixel in enumerate(row): + if pixel != " ": + # Apply magnification scaling + for sy in range(scale): + for sx in range(scale): + px = current_x + col_idx * scale + sx + py = start_y + row_idx * scale + sy + + # Boundary condition check + if 0 <= px < width and 0 <= py < height: + # 1D Array Mapping: y * row_width + 1 (filter offset) + x + buf_idx = py * row_size + 1 + px + raw_data[buf_idx] = 255 # White pixel + + current_x += (char_w + spacing) * scale idat_data = zlib.compress(raw_data) idat = make_chunk(b"IDAT", idat_data) @@ -224,8 +273,9 @@ def png_sequence(tmp_path_factory: pytest.TempPathFactory) -> Generator[list[Pat images: list[Path] = [] for i in range(5): - png_path = images_dir / f"image.{(i + 1):03d}.png" - png_generate(png_path, 200, 200) + idx = i + 1 + png_path = images_dir / f"image.{idx:03d}.png" + png_generate_with_index(png_path, 512, 512, idx) images.append(png_path) yield images @@ -234,7 +284,7 @@ def png_sequence(tmp_path_factory: pytest.TempPathFactory) -> Generator[list[Pat @pytest.fixture(scope="session") def wav_file(tmp_path_factory: pytest.TempPathFactory) -> Path: """Create a test WAV sound file with random data""" - sounds_dir = tmp_path_factory.mktemp("sounds") + sounds_dir = Path(tmp_path_factory.mktemp("sounds")) import uuid wav_path = sounds_dir / f"{uuid.uuid4()}.wav" diff --git a/tests/test_layer.py b/tests/test_layer.py index 6c5ea17..7aa3299 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -3,6 +3,8 @@ from pathlib import Path import pytest +from fileseq.filesequence import FileSequence +from fileseq.frameset import FrameSet from pytvpaint import george from pytvpaint.clip import Clip @@ -283,7 +285,23 @@ def test_layer_load_image(test_layer_obj: Layer, png_sequence: list[Path]) -> No def test_layer_render_frame(with_loaded_sequence: Layer, tmp_path: Path) -> None: - with_loaded_sequence.render_frame(tmp_path / "out.jpg", frame=3) + out_img = with_loaded_sequence.render_frame(tmp_path / "out.jpg", frame=3) + assert out_img and out_img.exists() and out_img == (tmp_path / "out.jpg") + + +@pytest.mark.parametrize("frame_set", [None, FrameSet([1, 3, 5]), FrameSet("1-15")]) +def test_layer_render_instances(with_loaded_sequence: Layer, tmp_path: Path, frame_set: FrameSet) -> None: + out_seq = with_loaded_sequence.render_instances(tmp_path / "out.#.jpg", frame_set=frame_set) + found_sequence = FileSequence.findSequenceOnDisk(str(out_seq)) + + frame_set = frame_set or FrameSet(f"{with_loaded_sequence.start}-{with_loaded_sequence.end}") + check_frame_set = FrameSet( + [f for f in frame_set.items if with_loaded_sequence.start <= f <= with_loaded_sequence.end] + ) + + assert found_sequence and found_sequence.frameSet() and found_sequence.frameSet().items # type: ignore[union-attr] + assert check_frame_set and check_frame_set.items + assert found_sequence.frameSet().items == check_frame_set.items # type: ignore[union-attr] @pytest.mark.skipif(not IS_NOT_TVP12, reason="TVP12 create anim layers by default now.") From 84a757a71f213a771a023e65705e6058a96e5c9a Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Thu, 9 Apr 2026 18:49:41 +0200 Subject: [PATCH 15/26] UPDATE doc build on commit/tag --- .github/workflows/docs-deploy.yml | 26 +++++++++++--------------- pyproject.toml | 3 +-- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 78b7b5f..8215c4c 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -2,10 +2,11 @@ name: Publish documentation on: push: + # Trigger on all branches and all tags branches: - - main + - "**" tags: - - "*" + - "**" pull_request: branches: - main @@ -17,7 +18,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -26,7 +27,7 @@ jobs: git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.9" @@ -35,24 +36,19 @@ jobs: - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: key: mkdocs-material-${{ env.cache_id }} path: .cache restore-keys: | mkdocs-material- - # Build the docs on PRs + # Build the docs on PRs to verify they don't break - name: Verify Docs Build (on Pull Requests) if: github.event_name == 'pull_request' run: hatch run docs:build - # Deploy Dev Documentation (on main) - - name: Deploy Dev Documentation (on main) - if: github.ref == 'refs/heads/main' - run: hatch run docs:deploy-dev - - # Deploy Release Documentation (on tags) - - name: Deploy Release Documentation (on tags) - if: startsWith(github.ref, 'refs/tags/') - run: hatch run docs:deploy-release ${{ github.ref_name }} \ No newline at end of file + # Deploy Documentation dynamically for EVERY branch and tag + - name: Deploy Documentation + if: github.event_name == 'push' + run: hatch run docs:deploy ${{ github.ref_name }} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 402cd61..9d0649d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,8 +74,7 @@ features = ["docs"] [tool.hatch.envs.docs.scripts] build = "zensical build" serve = "zensical serve" -deploy-dev = "mike deploy dev --push" -deploy-release = "mike deploy {args} latest --update-aliases --push" +deploy = "mike deploy {args} latest --update-aliases --push" [tool.pytest.ini_options] addopts = [ From cf5c6dbdc5c1caec63e97ef2f63352ea12fdd499 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Thu, 9 Apr 2026 18:54:13 +0200 Subject: [PATCH 16/26] UPDATE docs ci to use zensical --- .github/workflows/docs-deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 8215c4c..73154d3 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -38,10 +38,10 @@ jobs: - uses: actions/cache@v5 with: - key: mkdocs-material-${{ env.cache_id }} + key: zensical-${{ env.cache_id }} path: .cache restore-keys: | - mkdocs-material- + zensical- # Build the docs on PRs to verify they don't break - name: Verify Docs Build (on Pull Requests) From 42799463c67d18c3d1b1f5358da5f646c263b0af Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Thu, 9 Apr 2026 19:31:47 +0200 Subject: [PATCH 17/26] UPDATE docs pyproject.toml and ci --- .github/workflows/docs-deploy.yml | 15 +++++++---- docs/contributing/developer_setup.md | 2 +- pyproject.toml | 37 +++++++++++----------------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 73154d3..a9c15fb 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -43,12 +43,17 @@ jobs: restore-keys: | zensical- - # Build the docs on PRs to verify they don't break + # Test build docs on PRs - name: Verify Docs Build (on Pull Requests) if: github.event_name == 'pull_request' run: hatch run docs:build - # Deploy Documentation dynamically for EVERY branch and tag - - name: Deploy Documentation - if: github.event_name == 'push' - run: hatch run docs:deploy ${{ github.ref_name }} \ No newline at end of file + # Deploy dev documentation + - name: Deploy Branch Documentation + if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') + run: hatch run docs:deploy ${{ github.ref_name }} + + # Deploy release documentation + - name: Deploy Release Documentation + if: startsWith(github.ref, 'refs/tags/') + run: hatch run docs:deploy ${{ github.ref_name }} latest \ No newline at end of file diff --git a/docs/contributing/developer_setup.md b/docs/contributing/developer_setup.md index abee364..8002ccd 100644 --- a/docs/contributing/developer_setup.md +++ b/docs/contributing/developer_setup.md @@ -124,7 +124,7 @@ You can either run the development server or build the entire documentation: ❯ hatch run docs: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. +The [Python API documentation](https://brunchstudio.github.io/pytvpaint/api/objects/project/) is auto-generated from the docstrings in the code by using [Zensical](https://github.com/zensical/zensical). We use the [Google style](https://mkdocstrings.github.io/griffe/docstrings/#google-style) for docstrings. For example: diff --git a/pyproject.toml b/pyproject.toml index 9d0649d..ade01e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,30 +35,12 @@ version-file = "pytvpaint/_version.py" [tool.hatch.build.targets.sdist] exclude = ["/.github"] -[project.optional-dependencies] -dev = [ +[tool.hatch.envs.dev] +dependencies = [ "black>=24.2.0", "ruff>=0.2.2", "mypy>=1.9.0", ] -test = [ - "pytest>=7.0.0", - "pytest-mock>=3.0.0", - "pytest-cov>=4.0.0", - "pytest-html>=4.0.0", -] -docs = [ - "zensical", - "mkdocstrings[python]>=0.24.0", - "mike", -] - -[tool.hatch.envs.default] -features = ["dev", "test"] - -[tool.hatch.envs.dev] -features = ["dev"] - [tool.hatch.envs.dev.scripts] format = "black ." lint = "ruff check --fix ." @@ -66,15 +48,24 @@ typecheck = "mypy ." all = ["format", "lint", "typecheck"] [tool.hatch.envs.test] -features = ["test"] +dependencies = [ + "pytest>=7.0.0", + "pytest-mock>=3.0.0", + "pytest-cov>=4.0.0", + "pytest-html>=4.0.0", +] [tool.hatch.envs.docs] -features = ["docs"] +dependencies = [ + "zensical", + "mkdocstrings[python]>=0.24.0", + "mike @ git+https://github.com/squidfunk/mike.git", +] [tool.hatch.envs.docs.scripts] build = "zensical build" serve = "zensical serve" -deploy = "mike deploy {args} latest --update-aliases --push" +deploy = "mike deploy {args} --update-aliases --push" [tool.pytest.ini_options] addopts = [ From bdf01b5933dceb81c3144aa5eff53275104da5ff Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Thu, 9 Apr 2026 20:53:46 +0200 Subject: [PATCH 18/26] HOTFIX range issues with non-consecutive frame sets --- pytvpaint/clip.py | 9 +++++---- pytvpaint/layer.py | 3 +-- pytvpaint/project.py | 9 +++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pytvpaint/clip.py b/pytvpaint/clip.py index e284177..a191131 100644 --- a/pytvpaint/clip.py +++ b/pytvpaint/clip.py @@ -574,14 +574,15 @@ def convert_range(x: int) -> int: end = max(clip_real_start, end) if frame_set is not None: - start = max(start, convert_range(int(frame_set.start()))) - end = min(end, convert_range(int(frame_set.end()))) + frames = sorted(frame_set.items) + start = max(start, convert_range(int(frames[0]))) if frames else start + end = min(end, convert_range(int(frames[-1]))) if frames else end if frame_set.isConsecutive(): frame_set = FrameSet(f"{start}-{end}") else: - frames = [convert_range(int(f)) for f in frame_set.items if start <= convert_range(int(f)) <= end] + frames = [convert_range(int(f)) for f in frames if start <= convert_range(int(f)) <= end] frame_set = FrameSet(frames) - else: + if frame_set is None or not frame_set.items: frame_set = FrameSet(f"{start}-{end}") return start, end, frame_set diff --git a/pytvpaint/layer.py b/pytvpaint/layer.py index fab09bb..a35f5a4 100644 --- a/pytvpaint/layer.py +++ b/pytvpaint/layer.py @@ -1021,13 +1021,12 @@ def render_instances( Returns: FileSequence: instances output sequence """ - frames = [layer_instance.start for layer_instance in self.instances] + frames = sorted([layer_instance.start for layer_instance in self.instances]) if frame_set is not None: frame_set = FrameSet([f for f in frame_set.items if f in frames]) else: frame_set = FrameSet(frames) or frame_set - print("frame_set ===> ", frame_set) return self.clip.render( output_path=export_path, frame_set=frame_set, diff --git a/pytvpaint/project.py b/pytvpaint/project.py index a5a5ba2..9618ac2 100644 --- a/pytvpaint/project.py +++ b/pytvpaint/project.py @@ -632,14 +632,15 @@ def convert_range(x: int) -> int: end = convert_range(end) if frame_set is not None: - start = max(start, convert_range(int(frame_set.start()))) - end = min(end, convert_range(int(frame_set.end()))) + frames = sorted(frame_set.items) + start = max(start, convert_range(int(frames[0]))) if frames else start + end = min(end, convert_range(int(frames[-1]))) if frames else end if frame_set.isConsecutive(): frame_set = FrameSet(f"{start}-{end}") else: - frames = [convert_range(int(f)) for f in frame_set.items if start <= convert_range(int(f)) <= end] + frames = [convert_range(int(f)) for f in frames if start <= convert_range(int(f)) <= end] frame_set = FrameSet(frames) - else: + if frame_set is None or not frame_set.items: frame_set = FrameSet(f"{start}-{end}") return start, end, frame_set From d586495c8257b87bc03d4f6df70c2bdb3e8c245a Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Fri, 10 Apr 2026 12:23:57 +0200 Subject: [PATCH 19/26] HOTFIX parsing and output handling and tests --- pyproject.toml | 5 +++- pytvpaint/george/client/parse.py | 15 ++++++----- pytvpaint/utils.py | 44 ++++++++++++++++++++------------ tests/conftest.py | 36 +++++--------------------- tests/test_clip.py | 3 ++- tests/test_layer.py | 2 -- 6 files changed, 48 insertions(+), 57 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ade01e9..2dcac97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,9 @@ version-file = "pytvpaint/_version.py" [tool.hatch.build.targets.sdist] exclude = ["/.github"] +[tool.hatch.envs.default] +installer = "uv" + [tool.hatch.envs.dev] dependencies = [ "black>=24.2.0", @@ -76,7 +79,7 @@ addopts = [ "--html=tests/reports/test_report.html", "--self-contained-html", "-vv", - "--maxfail=1" +# "--maxfail=1" ] cache_dir = "tests/.pytest_cache" log_cli = true diff --git a/pytvpaint/george/client/parse.py b/pytvpaint/george/client/parse.py index 0e63856..3855035 100644 --- a/pytvpaint/george/client/parse.py +++ b/pytvpaint/george/client/parse.py @@ -178,21 +178,22 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: # noqa: C901 return cast(T, container_cls(casted_items)) - clean_val = clean_val.strip("\"'") + # clean_val = clean_val.strip("\"'") # Basic Primitives - if isinstance(cast_type, str): + target_type = cast_type if isinstance(cast_type, type) else type(cast_type) + if target_type is str: return clean_val - if isinstance(cast_type, int): + if target_type is int: return int(float(clean_val)) - if isinstance(cast_type, float): + if target_type is float: return float(clean_val) - if isinstance(cast_type, Path): + if target_type is Path: return Path(clean_val) - if isinstance(cast_type, bool): + if target_type is bool: return clean_val.lower() in ("true", "1", "yes", "on") # Enums - if isinstance(cast_type, type) and issubclass(cast_type, Enum): + if issubclass(cast_type, Enum): # By Name with suppress(KeyError): return cast_type[clean_val] diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index ed90792..74d3670 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -146,8 +146,9 @@ def _render( # noqa: C901 log.warning("`start` and/or `end` outside of `frame_set` range, will prioritize FrameSet.") if frame_set is not None and not (start and end): - start = start if start is not None else int(frame_set.start()) - end = end if end is not None else int(frame_set.end()) + frames = sorted(frame_set.items) + start = start if start is not None or not frames else int(frames[0]) + end = end if end is not None or not frames else int(frames[-1]) # finds range if none provided or in path and clamps it to the correct context file_sequence, start, end, is_sequence, is_image = handle_output_range( @@ -165,12 +166,18 @@ def _render( # noqa: C901 if not is_image and start == end: raise ValueError("TVPaint will not render a movie that contains a single frame") - # get first frame, tvp doesn't understand vfx padding `#` - if is_image and is_sequence: - first_frame = Path(file_sequence.frame(file_sequence.start())) - else: - first_frame = Path(str(output_path)) - first_frame.parent.mkdir(exist_ok=True, parents=True) + if is_sequence or ( + not is_sequence and FileSequence(str(output_path)).padding() + ): # get first frame, tvp doesn't understand vfx padding `#` + first_frame = ( + sorted(file_sequence.frameSet().items)[0] + if len(file_sequence.frameSet()) >= 1 + else file_sequence.start() + ) + first_frame_file = Path(file_sequence.frame(first_frame)) + else: # if movie or single image + first_frame_file = Path(str(output_path)) + first_frame_file.parent.mkdir(exist_ok=True, parents=True) save_format = george.SaveFormat.from_extension(file_sequence.extension().lower()) @@ -179,7 +186,7 @@ def _render( # noqa: C901 with render_context(alpha_mode, background_mode, save_format, format_opts, layer_selection): if frame_set.isConsecutive(): george.tv_project_save_sequence( - first_frame, + first_frame_file, start=start, end=end, use_camera=use_camera, @@ -203,7 +210,7 @@ def _render( # noqa: C901 file_sequence_frame_set = file_sequence.frameSet() if file_sequence_frame_set is None or frame_set is None: - raise Exception("Should have frame set") + raise Exception("Sequence Should have a frame set !") if not frame_set.issuperset(file_sequence_frame_set): # not all frames found @@ -212,9 +219,9 @@ def _render( # noqa: C901 f"Not all frames found, missing frames ({missing_frames}) in sequence : {output_path}" ) return file_sequence - if not first_frame.exists(): - raise FileNotFoundError(f"Could not find output at : {first_frame.as_posix()}") - return first_frame + if not first_frame_file.exists(): + raise FileNotFoundError(f"Could not find output at : {first_frame_file.as_posix()}") + return first_frame_file def get_unique_name(names: Iterable[str], stub: str) -> str: @@ -475,8 +482,8 @@ def handle_output_range( ) -> tuple[FileSequence, int, int, bool, bool]: """Handle the different options for output paths and range. - Whether the user provides 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 + Whether the user provides a range (start-end) or a frame set 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 @@ -503,8 +510,9 @@ def handle_output_range( # 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 int(file_sequence.start()) - end = end or int(file_sequence.end()) + frames = sorted(frame_set.items) + start = start or int(frames[0]) + end = end or int(frames[-1]) # check characteristics of file sequence fseq_has_range = frame_set and len(frame_set) > 1 @@ -529,6 +537,8 @@ def handle_output_range( if not file_sequence.padding() and is_image and len(frame_set) > 1: file_sequence.setPadding("#") + if not is_sequence and "#" not in str(file_sequence): + is_sequence = True # we should have a range by now, set it in the sequence if (is_image and not is_single_image) or file_sequence.padding(): diff --git a/tests/conftest.py b/tests/conftest.py index 3359391..684a4bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -154,34 +154,6 @@ def test_guideline_obj(test_project_obj: Project) -> FixtureYield[GuidelineLine] yield guideline_obj -@pytest.fixture -def count_up_generate(test_clip_obj: Clip) -> None: - """Create 5 frames with a text in the middle of the screen for each frame. Useful for debugging render tests.""" - text_pos = ( - int(test_clip_obj.project.width / 2), - int(test_clip_obj.project.height / 2), - ) - - test_clip_obj.current_frame = 1 - test_layer = Layer.new_anim_layer("count_up", test_clip_obj) - test_layer.make_current() - - for i in range(1, 6): - if i != 1: - test_layer.add_instance(i) - test_clip_obj.current_frame = i - - # write the frame number in the middle of the image - george.tv_set_a_pen_rgba(george.RGBColor(0, 0, 0), 255) # set the pen color - send_cmd("tv_TextTool2", "size", 200) # set text size - george.tv_text_brush(str(i)) # set the brush text - george.tv_set_active_shape(george.TVPShape.FREE_HAND_LINE, size=200) # set the shape and it's size - # write a line with the text brush, having the start-end pos being the same will fake a single click - george.tv_line(text_pos, text_pos) - # update undo stack otherwise edits to last image are not saved (-_-)" - george.tv_update_undo() - - # 5x7 Bitmap font definition for digits 0-9 FONT_BITMAPS = { "0": [" 000 ", "0 0", "0 0", "0 0", "0 0", "0 0", " 000 "], @@ -275,12 +247,18 @@ def png_sequence(tmp_path_factory: pytest.TempPathFactory) -> Generator[list[Pat for i in range(5): idx = i + 1 png_path = images_dir / f"image.{idx:03d}.png" - png_generate_with_index(png_path, 512, 512, idx) + png_generate_with_index(png_path, 1920, 1080, idx) images.append(png_path) yield images +@pytest.fixture +def count_up_generate(test_clip_obj: Clip, png_sequence: list[Path]) -> None: + """Load 5 frames with a text in the middle of the screen for each frame. Useful for debugging render tests.""" + test_clip_obj.load_media(png_sequence[0], start_count=[0, 5], stretch=True, preload=True, with_name="count_up") + + @pytest.fixture(scope="session") def wav_file(tmp_path_factory: pytest.TempPathFactory) -> Path: """Create a test WAV sound file with random data""" diff --git a/tests/test_clip.py b/tests/test_clip.py index eaa865e..5acd0a0 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -267,7 +267,8 @@ def test_clip_render_single_img( end: int | None, expected: str, ) -> None: - test_clip_obj.render(tmp_path / out, start, end) + frame_set = FrameSet(f"{start}-{end}") if start is not None and end is not None else None + test_clip_obj.render(output_path=tmp_path / out, frame_set=frame_set) expected_path = tmp_path.joinpath(expected) if "#" in expected_path.stem: diff --git a/tests/test_layer.py b/tests/test_layer.py index 7aa3299..f9a6048 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -88,8 +88,6 @@ def test_layer_is_current( def test_layer_is_selected(test_layer_obj: Layer) -> None: - assert not test_layer_obj.is_selected - test_layer_obj.is_selected = True assert test_layer_obj.is_selected From 52a4acb7287724cbb94e6a1a82d8fa9b7048fca0 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Fri, 10 Apr 2026 12:40:09 +0200 Subject: [PATCH 20/26] HOTFIX lint and type check issues --- pytvpaint/george/client/parse.py | 11 +++++------ pytvpaint/utils.py | 4 ++-- tests/conftest.py | 3 +-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pytvpaint/george/client/parse.py b/pytvpaint/george/client/parse.py index 3855035..d19274a 100644 --- a/pytvpaint/george/client/parse.py +++ b/pytvpaint/george/client/parse.py @@ -178,19 +178,18 @@ def tv_cast_to_type(value: str, cast_type: type[T]) -> T: # noqa: C901 return cast(T, container_cls(casted_items)) - # clean_val = clean_val.strip("\"'") # Basic Primitives target_type = cast_type if isinstance(cast_type, type) else type(cast_type) if target_type is str: - return clean_val + return cast(T, clean_val) if target_type is int: - return int(float(clean_val)) + return cast(T, int(float(clean_val))) if target_type is float: - return float(clean_val) + return cast(T, float(clean_val)) if target_type is Path: - return Path(clean_val) + return cast(T, Path(clean_val)) if target_type is bool: - return clean_val.lower() in ("true", "1", "yes", "on") + return cast(T, clean_val.lower() in ("true", "1", "yes", "on")) # Enums if issubclass(cast_type, Enum): diff --git a/pytvpaint/utils.py b/pytvpaint/utils.py index 74d3670..cdeeea5 100644 --- a/pytvpaint/utils.py +++ b/pytvpaint/utils.py @@ -170,8 +170,8 @@ def _render( # noqa: C901 not is_sequence and FileSequence(str(output_path)).padding() ): # get first frame, tvp doesn't understand vfx padding `#` first_frame = ( - sorted(file_sequence.frameSet().items)[0] - if len(file_sequence.frameSet()) >= 1 + sorted(file_sequence.frameSet().items)[0] # type: ignore[union-attr] + if file_sequence.frameSet() and len(file_sequence.frameSet()) >= 1 # type: ignore[arg-type] else file_sequence.start() ) first_frame_file = Path(file_sequence.frame(first_frame)) diff --git a/tests/conftest.py b/tests/conftest.py index 684a4bd..81a360c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,6 @@ from pytvpaint import george from pytvpaint.clip import Clip -from pytvpaint.george.client import send_cmd from pytvpaint.guideline import GuidelineLine from pytvpaint.layer import Layer from pytvpaint.project import Project @@ -256,7 +255,7 @@ def png_sequence(tmp_path_factory: pytest.TempPathFactory) -> Generator[list[Pat @pytest.fixture def count_up_generate(test_clip_obj: Clip, png_sequence: list[Path]) -> None: """Load 5 frames with a text in the middle of the screen for each frame. Useful for debugging render tests.""" - test_clip_obj.load_media(png_sequence[0], start_count=[0, 5], stretch=True, preload=True, with_name="count_up") + test_clip_obj.load_media(png_sequence[0], start_count=(0, 5), stretch=True, preload=True, with_name="count_up") @pytest.fixture(scope="session") From c065729f6171e9779fe239478d26c659993760f6 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Thu, 30 Apr 2026 18:14:12 +0200 Subject: [PATCH 21/26] UPDATE doc and limitations.md --- docs/limitations.md | 26 ++++++++++++++++---------- docs/usage/usage.md | 1 + 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/limitations.md b/docs/limitations.md index b87d3f1..0835e51 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -92,6 +92,7 @@ issues in the table below: | [`tv_Ratio`](api/george/project.md#pytvpaint.george.grg_project.tv_ratio) | Always returns an empty string (`""`) | | [`tv_InstanceName`](api/george/layer.md#pytvpaint.george.grg_layer.tv_instance_name) | Crashes if we provided with an invalid `layer_id` | | `tv_CameraEnumPoints` | Only returns the first point, no matter how many points there are. | +| `tv_AlphaSaveMode` | Extremely inconsistant, seems to only work sometimes. | | `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` seems to work for the current clip | | `tv_LayerSelectInfo` | Does not select frames as stated in the documentation and will also return non selected frames if attribute `full` is set to True | @@ -103,7 +104,7 @@ issues in the table below: TVPaint 12 introduces a lot of welcome changes (especially for the artists) and some new needed function for developers. However, it also introduces a lot of breaking changes and some new bugs. -To deal with these changes as well as the new functions exclusive to TVP 12, we added a couple decorators and functions : +To deal with these changes as well as the new functions exclusive to TVPaint 12, we added a couple decorators and functions : | Method | Description | |:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| @@ -114,8 +115,14 @@ To deal with these changes as well as the new functions exclusive to TVP 12, we Below is also a list of the current breaking changes and bugs we noticed during development : +### George Documentation : +* George documentation is still not up to date and still contains many errors (seems stuck at some older TVP11 version), for now prefer the PyTVPaint documentation whenever possible. + +### General : +* UI doesn't always update when values are set/updated via code, you either have to wait a few seconds for a refresh, or click somewhere else. This might lead to errors when users are using the UI at the same time. + ### C++ Plugin : -* [BUG] plugin PIRF_HIDDEN_REQ doesn't seem to be working anymore, the plugin window is now visible and closing it kills the plugin with no way to restart it without restarting tvpaint. +* [BUG] plugin PIRF_HIDDEN_REQ and FILTERREQ_NO_TBAR don't seem to be working anymore, the plugin window is now visible and closing it kills the plugin with no way to restart it without restarting tvpaint. ### Project : * [DEPRECATED/BREAKING] [`tv_ProjectInfo`](api/george/project.md#pytvpaint.george.grg_project.tv_project_info) values of `field_order` have been removed so for now we provide it ourselves. @@ -129,28 +136,27 @@ Below is also a list of the current breaking changes and bugs we noticed during * [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json seems to output low quality images, this may use the same function `george.tv_save_image`. ### Layers : -* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually tv\_CTGGetSource without the `s` at the end. +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually `tv_CTGGetSource` without the `s` at the end. * [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) actually requires and returns layer Ids not names. * Child layers have no way of knowing if they are in a folder layer or which one. -* Folder layer has no way of knowing which layers are its children. +* A Folder layer has no way of knowing which layers are its children. * [BUG] Creating a CTG layer from a folder crashes TVPaint. * [BUG] Moving a layer that is already in a folder in the same folder crashes TVPaint. * `tv_LayerMove` can now move layers into folders but the position is relative to the root and not the folder, this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers in and out of folders. * Not providing a `FolderID` to `tv_LayerMove` doesn't move the layer to the root, you just need to move outside the folder range for it to work, which again is pretty tough to do since we can't get the child layers of a folder and therefore their positions. * Moving a layer inside a folder can be done without providing a `FolderID`, just by moving the layer in the folder's range. -* [BUG] UI doesn't always update when values are set/updated via code, you either have to wait a few seconds for a refresh, or click somewhere else, this might lead to errors when users are using the UI at the same time. * [BUG] CTG layer sometimes takes a while to update in the UI (a few seconds), which means they can be unintentionally edited or reset before the update. * CameraLayer is not really a layer and most layer functions will ignore it, prefer use of PyTVPaint `Camera` object instead. -* [BUG] Selecting the Camera Layer in the UI now disables/grays out most layer related tools (this is a new behaviour), this causes a lot of errors when using pipeline tools or TVPaint panels as TVPaint now raises an error messages anytime you try to use a tool that is not camera related when the camera layer is selected. The problem is that the Camera layer is now also the default layer and selected by default when creating/opening any project. +* [BUG] Selecting the Camera Layer in the UI now disables/grays out most layer related tools (this is a new behavior), this causes a lot of errors when using pipeline tools or TVPaint panels as TVPaint now raises an error messages anytime you try to use a tool that is not camera related when the camera layer is selected. * [BUG] `tv_LayerRename` does not work in TVP12 and will replace the name with an empty string, this is either a new bug or there is now a new argument that is needed but not documented. -* default layers name is now Anim\_X and when these layers are queried for their names they return an empty string. +* [BREAKING] default layers name is now `Anim_X` and when these layers are queried for their names they return an empty string. * [BUG] `tv_layer_move` still sets position at (value-1) when value is superior to 0. * [BUG] `tv_preserve_set` does not seem to work in TVP12 * [BUG] `tv_LayerColor setcolor` will fail when the `name` argument is provided. * [BUG] `tv_LayerColor setcolor` no longer returns -1 when given a bad color index. * [BUG] `tv_layer_move` no longer returns -1 when given a bad position. * [BUG] `tv_LayerCreate` should not be used when an empty string as it will consider the new variable `layer_type` as the name. -* [BUG] Many layer functions that used to return -1 when given a bad layer id or position no longer do so, this can lead to silent errors and breaking behaviour, this also breaks any way to check/validate these functions return values. The functions are : +* [BUG] Many layer functions that used to return -1 when given a bad layer id or position no longer do so, this can lead to silent errors and breaking behavior, this also breaks any way to check/validate these functions return values. The functions are : * `tv_layer_set` * `tv_LayerSelection` * `tv_layer_kill` @@ -162,7 +168,7 @@ Below is also a list of the current breaking changes and bugs we noticed during * `tv_LayerMarkSet` ### Camera : -* [DEPRECATED/BREAKING] [`Camera.anti_aliasing`](api/objects/camera.md#pytvpaint.camera.Camera.anti_aliasing) now always returns 1. +* [DEPRECATED/BREAKING] [`Camera.anti_aliasing`](api/objects/camera.md#pytvpaint.camera.Camera.anti_aliasing) no longer returns anti_aliasing, for now it always returns 1. * [DEPRECATED/BREAKING] [`Camera.fps`](api/objects/camera.md#pytvpaint.camera.Camera.fps) no longer returns/sets fps, now only fps is Project fps. * [BUG] Most Camera values can still be edited/queried using [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) but they are not immediately reflected in the UI, and so they can be unintentionally edited or reset. @@ -170,4 +176,4 @@ Below is also a list of the current breaking changes and bugs we noticed during * [BUG] [`tv_CameraInterpolation`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_interpolation) doesn't work properly in TVP12 and always returns an "empty" point. ### Guidelines: -* `tv_GuidelineModify "marks"` doesn't seem to work, values are never changed, added function with a warning. \ No newline at end of file +* `tv_GuidelineModify "marks"` doesn't seem to work, values are never changed, added function with a warning.g_layer.tv_layer_density) now returns a float instead of an integer which broke the python cast, this is now fixed. \ No newline at end of file diff --git a/docs/usage/usage.md b/docs/usage/usage.md index fbb3086..b6c9fd5 100644 --- a/docs/usage/usage.md +++ b/docs/usage/usage.md @@ -20,6 +20,7 @@ PyTVPaint can be configured using these variables: | `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` | Whether or not PyTVPaint should automatically connect to the WebSocket server at startup (module import). Accepts 0 or 1. | | `PYTVPAINT_WS_TIMEOUT` | `60` seconds | The timeout after which we stop reconnecting at startup or if the connection was lost. | +| `TVP_RPC_LOG_PATH` | `` | C++ plugin log file output path, default is `` ## Automatic client connection From 2e68a272fef333e1da8409655e5899ab3e719a6a Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 12 May 2026 12:26:09 +0200 Subject: [PATCH 22/26] UPDATE RPC signals, timeout and retries setup --- pytvpaint/george/client/__init__.py | 29 +++++----- pytvpaint/george/client/rpc.py | 84 ++++++++++++++--------------- 2 files changed, 57 insertions(+), 56 deletions(-) diff --git a/pytvpaint/george/client/__init__.py b/pytvpaint/george/client/__init__.py index 3c48401..b722738 100644 --- a/pytvpaint/george/client/__init__.py +++ b/pytvpaint/george/client/__init__.py @@ -18,18 +18,19 @@ from pytvpaint.george.client.rpc import JSONRPCClient from pytvpaint.george.exceptions import GeorgeError +DEFAULT_HOST = "ws://localhost" +DEFAULT_PORT = 3000 +DEFAULT_TIMEOUT = 60 -def _connect_client(host: str = "ws://localhost", port: int = 3000, timeout: int = 60) -> JSONRPCClient: - host = os.getenv("PYTVPAINT_WS_HOST", host) - port = int(os.getenv("PYTVPAINT_WS_PORT", port)) - startup_connect = bool(int(os.getenv("PYTVPAINT_WS_STARTUP_CONNECT", 1))) - timeout = int(os.getenv("PYTVPAINT_WS_TIMEOUT", timeout)) - rpc_client = JSONRPCClient(f"{host}:{port}", timeout) +def _connect_client( + host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, timeout: int = DEFAULT_TIMEOUT, startup_connect: bool = True +) -> JSONRPCClient: + _rpc_client = JSONRPCClient(f"{host}:{port}", timeout) if not startup_connect: log.debug("Auto Connect Disabled, RPC client is not connected") - return rpc_client + return _rpc_client start_time = time() wait_duration = 5 @@ -39,7 +40,7 @@ def _connect_client(host: str = "ws://localhost", port: int = 3000, timeout: int if timeout and (time() - start_time) > timeout: break with contextlib.suppress(ConnectionRefusedError): - rpc_client.connect() + _rpc_client.connect() connection_successful = True break @@ -48,18 +49,22 @@ def _connect_client(host: str = "ws://localhost", port: int = 3000, timeout: int if not connection_successful: # Connection could not be established after timeout - if rpc_client.is_connected: - rpc_client.disconnect() + if _rpc_client.is_connected: + _rpc_client.disconnect() raise ConnectionRefusedError("Could not establish connection with a tvpaint instance before timeout !") if connection_successful: log.info(f"Connected to TVPaint on port {port}") - return rpc_client + return _rpc_client -rpc_client = _connect_client() +_rpc_host = os.getenv("PYTVPAINT_WS_HOST", DEFAULT_HOST) +_rpc_port = int(os.getenv("PYTVPAINT_WS_PORT", DEFAULT_PORT)) +_rpc_timeout = int(os.getenv("PYTVPAINT_WS_TIMEOUT", DEFAULT_TIMEOUT)) +_rpc_startup_connect = bool(int(os.getenv("PYTVPAINT_WS_STARTUP_CONNECT", 1))) +rpc_client = _connect_client(_rpc_host, _rpc_port, _rpc_timeout, _rpc_startup_connect) T = TypeVar("T", bound=Callable[..., Any]) diff --git a/pytvpaint/george/client/rpc.py b/pytvpaint/george/client/rpc.py index 85c3b69..664b24c 100644 --- a/pytvpaint/george/client/rpc.py +++ b/pytvpaint/george/client/rpc.py @@ -2,17 +2,14 @@ from __future__ import annotations -import contextlib import json +import select +import socket import sys -import threading -from time import time from typing import Any, Union, cast from typing_extensions import NotRequired, TypedDict -from websocket import WebSocket, WebSocketException - -from pytvpaint import log +from websocket import WebSocket JSONValueType = Union[str, int, float, bool, None] @@ -60,73 +57,72 @@ def __init__(self, error: JSONRPCError) -> None: class JSONRPCClient: - """Simple JSON-RPC 2.0 client over websockets with automatic reconnection. + """Simple JSON-RPC 2.0 client over websockets. See: https://www.jsonrpc.org/specification#notification """ - def __init__(self, url: str, timeout: int = 60, version: str = "2.0") -> None: + def __init__(self, url: str, timeout: int = 60, max_retries: int = 5, version: str = "2.0") -> None: """Initialize a new JSON-RPC client with a WebSocket url endpoint. Args: url: the WebSocket url endpoint - timeout: the reconnection timeout + timeout: the socket operation timeout + max_retries: the maximum socket connection retries version: The JSON-RPC version. Defaults to "2.0". """ self.ws_handle = WebSocket() self.url = url self.rpc_id = 0 self.timeout = timeout + self.max_retries = max_retries self.jsonrpc_version = version - self.stop_ping = threading.Event() - self.run_forever = False - self.ping_thread: threading.Thread | None = None - self._ping_start_time: float = 0 - - 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): - try: - self.ws_handle.ping() - continue - except (WebSocketException, ConnectionError): - self.ws_handle.close() - - 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 !") - def __del__(self) -> None: """Called when the client goes out of scope.""" self.disconnect() @property def is_connected(self) -> bool: - """Returns True if the client is connected.""" - return self.ws_handle.connected + """Returns True if the client is connected and the socket is active.""" + if not self.ws_handle.connected or self.ws_handle.sock is None: + return False + + try: + # check if the socket is readable. + readable_sockets, _, _ = select.select([self.ws_handle.sock], [], [], 0.0) + if readable_sockets: + # MSG_PEEK reads data without consuming it from the buffer. + # If recv returns 0 bytes on a readable socket, the peer has disconnected. + data = self.ws_handle.sock.recv(1, socket.MSG_PEEK) + if not data: + return False + except (BlockingIOError, InterruptedError): + pass # Normal non-blocking behavior + except Exception: + return False # Socket error indicates disconnection + + return True def connect(self, timeout: float | None = None) -> None: - """Connects to the WebSocket endpoint.""" + """Connects to the WebSocket endpoint and configures TCP keepalive.""" self.ws_handle.connect(self.url, timeout=timeout) - if not self.ping_thread: - self._ping_start_time = time() - self.ping_thread = threading.Thread(target=self._auto_reconnect, daemon=True) - self.run_forever = True - self.ping_thread.start() + if self.ws_handle.sock: + sock = self.ws_handle.sock + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + # Apply OS-specific keepalive configurations if available + if hasattr(socket, "TCP_KEEPIDLE"): # linux and windows specific + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, self.timeout) + if hasattr(socket, "TCP_KEEPINTVL"): + probe_interval = max(1, self.timeout // 6) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, probe_interval) + if hasattr(socket, "TCP_KEEPCNT"): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, self.max_retries) def disconnect(self) -> None: """Disconnects from the server.""" - self.run_forever = False - if self.ping_thread: - self.ping_thread.join() - self.ws_handle.close() def increment_rpc_id(self) -> None: From 29773340ccc6afa33f90605f857cf6558841f117 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Tue, 12 May 2026 18:59:33 +0200 Subject: [PATCH 23/26] IMPROVE rpc connect and socket check --- pytvpaint/george/client/rpc.py | 4 ++-- tests/george/client/test_rpc.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pytvpaint/george/client/rpc.py b/pytvpaint/george/client/rpc.py index 664b24c..da26e97 100644 --- a/pytvpaint/george/client/rpc.py +++ b/pytvpaint/george/client/rpc.py @@ -104,7 +104,7 @@ def is_connected(self) -> bool: return True - def connect(self, timeout: float | None = None) -> None: + def connect(self, timeout: int = 0) -> None: """Connects to the WebSocket endpoint and configures TCP keepalive.""" self.ws_handle.connect(self.url, timeout=timeout) @@ -116,7 +116,7 @@ def connect(self, timeout: float | None = None) -> None: if hasattr(socket, "TCP_KEEPIDLE"): # linux and windows specific sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, self.timeout) if hasattr(socket, "TCP_KEEPINTVL"): - probe_interval = max(1, self.timeout // 6) + probe_interval = max(1, max(self.timeout, 1) // 6) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, probe_interval) if hasattr(socket, "TCP_KEEPCNT"): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, self.max_retries) diff --git a/tests/george/client/test_rpc.py b/tests/george/client/test_rpc.py index ca82305..ed850bd 100644 --- a/tests/george/client/test_rpc.py +++ b/tests/george/client/test_rpc.py @@ -1,5 +1,6 @@ from __future__ import annotations +import socket from typing import Any import pytest @@ -11,8 +12,11 @@ @pytest.fixture def json_rpc_client(mocker: MockFixture) -> JSONRPCClient: + mocker.patch("select.select", return_value=([], [], [])) + def connect(w: WebSocket, url: str, **options: Any) -> None: w.connected = True + w.sock = mocker.MagicMock(spec=socket.socket) mocker.patch.object(WebSocket, "connect", connect) return JSONRPCClient("ws://localhost:3000") From 0b78eae73f7f20d886d4dfea0a097947dfd9874c Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Wed, 13 May 2026 10:22:47 +0200 Subject: [PATCH 24/26] ADD CHANGELOG.md and changelog page in docs --- CHANGELOG.md | 275 ++++++++++++++++++++++++++++++++++++++++++++++ docs/changelog.md | 275 ++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 551 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 docs/changelog.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a65a9cb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,275 @@ +# Changelog + +## [1.1.0] - 2024-05-13 +### Features and Updates + +* Improved parsing logic and added a new features to handle tvpaint not escaping characters +* Added option to set aliases in the dataclass fields to have more pythonic variable names +* Updated `Layer.render...` functions to use the parent clips render instead of `george.tv_save_image` +* Updated `render` functions to use `Fileseq.FrameSet` instead of a `(start, end)` this allows rendering of non consecutive ranges and makes range handling more streamlined. +* Improved `utils.get_tvp_element` functions and added searching `by_regex` as well +* Updated all tests for compatibility with tvpaint 12 +* Improved documentation and fixed some typos +* Move project from `poetry` to `hatch` + +### George + +* Added dataclass `george.RGBAColor` +* Added function `george.tv_pick_color` +* Added warning to `george.tv_save_image` as it outputs low quality images +* added `to_extension` in SaveFormat + +### Project + +* Added Support for Guidelines in george functions and python classes + * new module `george/grg_guidelines.py` + * new module `guideline.py` with classes : + * `guideline.GuidelineImage` + * `guideline.GuidelineLine` + * `guideline.GuidelineSegment` + * `guideline.GuidelineCircle` + * `guideline.GuidelineEllipse` + * `guideline.GuidelineGrid` + * `guideline.GuidelineMarks` + * `guideline.GuidelineSafeArea` + * `guideline.GuidelineFieldChart` + * `guideline.GuidelineAnimatorField` + * `guideline.GuidelineVanishPoint1` + * `guideline.GuidelineVanishPoint2` + * `guideline.GuidelineVanishPoint3` +* Added `Project.guidelines` and `Project.add_guideline_...` functions to `Project` class + +### Scene + +* Added new `Scene.split()` function to `Scene` based on the new function `tv_SceneSplit`. +* Added new george function `tv_SceneCreate`, however this functions doesn't seem to work for now, check bugs and breaking changes section. + +### Clip + +* Added `Clip.ctg_layers()` property to `Clip`. +* Added `Clip.folders()` property to `Clip`. +* Added `Clip.camera_layer()` property to `Clip`. +* Added `Clip.get_layers()` property to `Clip` which returns the clip's layers using filters (use this instead of `Clip.layers` when using TVP 12 or above) .Added `Clip.camera_layer()` property to `Clip`. +* [DEPRECATED] `Clip.layers()` is deprecated and only returns animation layers (this remains unchanged from before) and ignores all new Folder, Camera and CTG layers. Prefer `Clip.get_layers()` instead. +* Added support for new unescape logic to : + * `Clip.action_text` + * `Clip.dialog_text` + * `Clip.note_text` + * `Project.header_info` + * `Project.author` + * `Project.notes` + +### Layers + +* `tv_LayerMove` now takes a `folder_id` argument, more on this in the bugs sections. +* `tv_LayerCreate` now takes a `layer_type` argument to create a layer folder. +* Added new object `LayerFolder` which uses the new layer folder functions (e.g: `tv_LayerFolderDelete`). +* Added new `Layer.folder` property in `Layer`. +* Added new `Layer.set_folder_position` function in `Layer` though it is pretty finicky to use (check bugs and breaking changes section). +* Added new object `CameraLayer`. +* Added new object `CTGLayer` which uses the new CTG layer functions : + \* `CTGLayer.squiggles_visible` / `tv_CTGSquigglesVisible`. + \* `CTGLayer.apply_changes` / `tv_CTGApplyChanges`. + \* `CTGLayer.sources` / `tv_CTGGetSource`. + \* `CTGLayer.add_sources` and `CTGLayer.remove_sources` / `tv_CTGSource`. + \* `CTGLayer.load_structure` / `tv_CTGLoadStructure`. +* Added new `Layer.is_ctg_layer` property in `Layer`. +* Added new `Layer.is_ctg_source` property in `Layer`. +* Added new `Layer.sourced_ctg_layers` property in `Layer`. +* Added `Layer.pan` function in `Layer`. +* Added `Layer.cut` , `Layer.copy`, and `Layer.paste` functions to the `Layer` class. +* Added `Layer.render_instances` function which only renders the instances in the layer not the whole sequence. + +### Camera + +* `Camera.get_point_data_at()` now returns a new read-only `InterpolationCameraPoint` object (inherited from `CameraPoint`). +* `Camera.get_point_data_at_frame()` now returns a new read-only `FrameCameraPoint` object (inherited from `CameraPoint`). +* `CameraPoint` object now has two new properties : `CameraPoint.width` and `CameraPoint.height`. + +*** + +### Fixes + +* fixed an issue with the values provided to `get_unique_name()` +* fixed `test_tv_clip_save_structure_json` file format +* fixed misc small bugs/typos + +*** + +### TVPaint 12 Bugs and Breaking Changes + +TVPaint 12 introduces a lot of welcome changes (especially for the artists) and some new needed functions for developers. +However, it also introduces a lot of breaking changes and some new bugs. + +To deal with these changes, as well as the new functions exclusive to TVPaint 12, we added a few decorators and functions : + +| Method | Description | +|:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| +| [`min_version_compatible`](api/george/misc.md#pytvpaint.george.grg_base.min_version_compatible) | When used as decorator, checks if the tvp instance making the call is above the miniumum version needed. | +| [`deprecated_warning`](api/george/misc.md#pytvpaint.george.grg_base.deprecated_warning) | When used as decorator, will log a warning message anytime the decorated function is called. | +| [`is_tvp_version_below_12`](api/george/misc.md#pytvpaint.george.grg_base.is_tvp_version_below_12) | Returns True if the tvp instances version is below 12, False otherwise. | + + +Below is also a list of the current breaking changes and bugs we noticed during development : + +### George Documentation +* George documentation is still not up to date and still contains many errors (seems stuck at some older TVP11 version), for now prefer the PyTVPaint documentation whenever possible. + +### General +* UI doesn't always update when values are set/updated via code, you either have to wait a few seconds for a refresh, or click somewhere else. This might lead to errors when users are using the UI at the same time. + +### C++ Plugin +* [BUG] plugin PIRF_HIDDEN_REQ and FILTERREQ_NO_TBAR don't seem to be working anymore, the plugin window is now visible and closing it kills the plugin with no way to restart it without restarting tvpaint. + +### Project +* [DEPRECATED/BREAKING] [`tv_ProjectInfo`](api/george/project.md#pytvpaint.george.grg_project.tv_project_info) values of `field_order` have been removed so for now we provide it ourselves. +* [BUG] [`tv_project_save_sequence`](api/george/project.md#pytvpaint.george.grg_project.tv_project_save_sequence) does not render empty instances even if they exist in the timeline. + +### Scene +* [BUG] [`tv_SceneCreate`](api/george/scene.md#pytvpaint.george.grg_scene.tv_scene_create) doesn't seem to work. + +### Clip +* [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json %fi does not work for folder structure (in v11 and v12) +* [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json seems to output low quality images, this may use the same function `george.tv_save_image`. + +### Layers +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually `tv_CTGGetSource` without the `s` at the end. +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) actually requires and returns layer Ids not names. +* Child layers have no way of knowing if they are in a folder layer or which one. +* A Folder layer has no way of knowing which layers are its children. +* [BUG] Creating a CTG layer from a folder crashes TVPaint. +* [BUG] Moving a layer that is already in a folder in the same folder crashes TVPaint. +* `tv_LayerMove` can now move layers into folders but the position is relative to the root and not the folder, this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers in and out of folders. +* Not providing a `FolderID` to `tv_LayerMove` doesn't move the layer to the root, you just need to move outside the folder range for it to work, which again is pretty tough to do since we can't get the child layers of a folder and therefore their positions. +* Moving a layer inside a folder can be done without providing a `FolderID`, just by moving the layer in the folder's range. +* [BUG] CTG layer sometimes takes a while to update in the UI (a few seconds), which means they can be unintentionally edited or reset before the update. +* CameraLayer is not really a layer and most layer functions will ignore it, prefer use of PyTVPaint `Camera` object instead. +* [BUG] Selecting the Camera Layer in the UI now disables/grays out most layer related tools (this is a new behavior), this causes a lot of errors when using pipeline tools or TVPaint panels as TVPaint now raises an error messages anytime you try to use a tool that is not camera related when the camera layer is selected. +* [BUG] `tv_LayerRename` does not work in TVP12 and will replace the name with an empty string, this is either a new bug or there is now a new argument that is needed but not documented. +* [BREAKING] default layers name is now `Anim_X` and when these layers are queried for their names they return an empty string. +* [BUG] `tv_layer_move` still sets position at (value-1) when value is superior to 0. +* [BUG] `tv_preserve_set` does not seem to work in TVP12 +* [BUG] `tv_LayerColor setcolor` will fail when the `name` argument is provided. +* [BUG] `tv_LayerColor setcolor` no longer returns -1 when given a bad color index. +* [BUG] `tv_layer_move` no longer returns -1 when given a bad position. +* [BUG] `tv_LayerCreate` should not be used when an empty string as it will consider the new variable `layer_type` as the name. +* [BUG] Many layer functions that used to return -1 when given a bad layer id or position no longer do so, this can lead to silent errors and breaking behavior, this also breaks any way to check/validate these functions return values. The functions are : + * `tv_layer_set` + * `tv_LayerSelection` + * `tv_layer_kill` + * `tv_LayerBlendingMode` + * `tv_LayerStencil` + * `tv_LayerPreBehavior` + * `tv_LayerPostBehavior` + * `tv_LayerLockPosition` + * `tv_LayerMarkSet` + +### Camera +* [DEPRECATED/BREAKING] [`Camera.anti_aliasing`](api/objects/camera.md#pytvpaint.camera.Camera.anti_aliasing) no longer returns anti_aliasing, for now it always returns 1. +* [DEPRECATED/BREAKING] [`Camera.fps`](api/objects/camera.md#pytvpaint.camera.Camera.fps) no longer returns/sets fps, now only fps is Project fps. +* [BUG] Most Camera values can still be edited/queried using [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) but they are not immediately reflected in the + UI, and so they can be unintentionally edited or reset. +* [DEPRECATED/BREAKING] [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) values of pixel aspect ratio and fps have been "swapped" (since camera fps is no longer provided). +* [BUG] [`tv_CameraInterpolation`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_interpolation) doesn't work properly in TVP12 and always returns an "empty" point. + +### Guidelines +* `tv_GuidelineModify "marks"` doesn't seem to work, values are never changed, added function with a warning. + +--- + +## [1.0.2] - 2024-10-02 +### Fixes +* FIX save_dependencies functions #14 + +--- + +## [1.0.1] - 2024-09-16 +### Fixes +* HOTFIX tv_save_sequence range handling + +--- + +## [1.0.0] - 2024-06-21 +### Added +* Releasing first production version after successful use in projects at Brunch Studio + +--- + +## [1.0.0b9] - 2024-05-28 +### Fixes +* FIX tv_load_sequence/tv_load_image always stretching loaded media + +### Features/Updates +* UPDATE default values for functions : + * grg_clip.tv_load_sequence + * grg_clip.tv_sound_clip_adjust + * grg_layer.tv_load_image + * grg_project.tv_load_project + * grg_project.tv_project_save_sequence + * grg_project.tv_frame_rate_project_set + * grg_project.tv_sound_project_adjust + +--- + +## [1.0.0b8] - 2024-05-22 +### Features/Updates +* UPDATE george.grg_clip.tv_clip_save_structure_json default values and param names + +### Fixes +* FIX Layer.new_background_layer not working when providing an image to set + +--- + +## [1.0.0b7] - 2024-04-30 +### Features/Updates +* UPDATE clip json export to handle all options (fill_bakcground, all_images, etc...) +* [BREAKING CHANGES] UPDATE tv_clip_save_structure_json default values + +--- + +## [1.0.0b6] - 2024-04-22 +### Features/Updates +* Updated documentation +* Added new functions in Layer class : render and render_instances +* Clamp values for position to minimum 0 +* Updated `get_` functions in classes to return None instead of raising an error when element not found +* Update tests +* [BREAKING CHANGES] Updated function signatures and default values for: + * Clip.load_media + * Clip.render + * Clip.export_json + * Clip.export_psd + * Clip.export_csv + * Clip.export_sprites + * Clip.export_flix + * Clip.set_layer_color + * Layer.render_frame + * Project.render + * Project.render_clips + +### Fixes +* Fix layer position not setting position 0 correctly +* Fix mark_in/mark_out not set correctly + +--- + +## [1.0.0b5] - 2024-04-02 +### Fixes +* FIX client auto-connect not working properly +* FIX background colors set in Project class + +--- + +## [1.0.0b4] - 2024-03-28 +### Features/Updates +* Change `tv_background_set` George function signature to accepts a single color argument of type `tuple[RGBColor, RGBColor] | RGBColor | None` to be coherent with `tv_background_get`. #9 + +--- + +## [1.0.0b3] - 2024-03-27 +### Added +* ADD background mode option in render functions +* UPDATE background mode and color functions in API + +### Fixes +* FIX render context setting save mode instead of alpha mode \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..a65a9cb --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,275 @@ +# Changelog + +## [1.1.0] - 2024-05-13 +### Features and Updates + +* Improved parsing logic and added a new features to handle tvpaint not escaping characters +* Added option to set aliases in the dataclass fields to have more pythonic variable names +* Updated `Layer.render...` functions to use the parent clips render instead of `george.tv_save_image` +* Updated `render` functions to use `Fileseq.FrameSet` instead of a `(start, end)` this allows rendering of non consecutive ranges and makes range handling more streamlined. +* Improved `utils.get_tvp_element` functions and added searching `by_regex` as well +* Updated all tests for compatibility with tvpaint 12 +* Improved documentation and fixed some typos +* Move project from `poetry` to `hatch` + +### George + +* Added dataclass `george.RGBAColor` +* Added function `george.tv_pick_color` +* Added warning to `george.tv_save_image` as it outputs low quality images +* added `to_extension` in SaveFormat + +### Project + +* Added Support for Guidelines in george functions and python classes + * new module `george/grg_guidelines.py` + * new module `guideline.py` with classes : + * `guideline.GuidelineImage` + * `guideline.GuidelineLine` + * `guideline.GuidelineSegment` + * `guideline.GuidelineCircle` + * `guideline.GuidelineEllipse` + * `guideline.GuidelineGrid` + * `guideline.GuidelineMarks` + * `guideline.GuidelineSafeArea` + * `guideline.GuidelineFieldChart` + * `guideline.GuidelineAnimatorField` + * `guideline.GuidelineVanishPoint1` + * `guideline.GuidelineVanishPoint2` + * `guideline.GuidelineVanishPoint3` +* Added `Project.guidelines` and `Project.add_guideline_...` functions to `Project` class + +### Scene + +* Added new `Scene.split()` function to `Scene` based on the new function `tv_SceneSplit`. +* Added new george function `tv_SceneCreate`, however this functions doesn't seem to work for now, check bugs and breaking changes section. + +### Clip + +* Added `Clip.ctg_layers()` property to `Clip`. +* Added `Clip.folders()` property to `Clip`. +* Added `Clip.camera_layer()` property to `Clip`. +* Added `Clip.get_layers()` property to `Clip` which returns the clip's layers using filters (use this instead of `Clip.layers` when using TVP 12 or above) .Added `Clip.camera_layer()` property to `Clip`. +* [DEPRECATED] `Clip.layers()` is deprecated and only returns animation layers (this remains unchanged from before) and ignores all new Folder, Camera and CTG layers. Prefer `Clip.get_layers()` instead. +* Added support for new unescape logic to : + * `Clip.action_text` + * `Clip.dialog_text` + * `Clip.note_text` + * `Project.header_info` + * `Project.author` + * `Project.notes` + +### Layers + +* `tv_LayerMove` now takes a `folder_id` argument, more on this in the bugs sections. +* `tv_LayerCreate` now takes a `layer_type` argument to create a layer folder. +* Added new object `LayerFolder` which uses the new layer folder functions (e.g: `tv_LayerFolderDelete`). +* Added new `Layer.folder` property in `Layer`. +* Added new `Layer.set_folder_position` function in `Layer` though it is pretty finicky to use (check bugs and breaking changes section). +* Added new object `CameraLayer`. +* Added new object `CTGLayer` which uses the new CTG layer functions : + \* `CTGLayer.squiggles_visible` / `tv_CTGSquigglesVisible`. + \* `CTGLayer.apply_changes` / `tv_CTGApplyChanges`. + \* `CTGLayer.sources` / `tv_CTGGetSource`. + \* `CTGLayer.add_sources` and `CTGLayer.remove_sources` / `tv_CTGSource`. + \* `CTGLayer.load_structure` / `tv_CTGLoadStructure`. +* Added new `Layer.is_ctg_layer` property in `Layer`. +* Added new `Layer.is_ctg_source` property in `Layer`. +* Added new `Layer.sourced_ctg_layers` property in `Layer`. +* Added `Layer.pan` function in `Layer`. +* Added `Layer.cut` , `Layer.copy`, and `Layer.paste` functions to the `Layer` class. +* Added `Layer.render_instances` function which only renders the instances in the layer not the whole sequence. + +### Camera + +* `Camera.get_point_data_at()` now returns a new read-only `InterpolationCameraPoint` object (inherited from `CameraPoint`). +* `Camera.get_point_data_at_frame()` now returns a new read-only `FrameCameraPoint` object (inherited from `CameraPoint`). +* `CameraPoint` object now has two new properties : `CameraPoint.width` and `CameraPoint.height`. + +*** + +### Fixes + +* fixed an issue with the values provided to `get_unique_name()` +* fixed `test_tv_clip_save_structure_json` file format +* fixed misc small bugs/typos + +*** + +### TVPaint 12 Bugs and Breaking Changes + +TVPaint 12 introduces a lot of welcome changes (especially for the artists) and some new needed functions for developers. +However, it also introduces a lot of breaking changes and some new bugs. + +To deal with these changes, as well as the new functions exclusive to TVPaint 12, we added a few decorators and functions : + +| Method | Description | +|:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| +| [`min_version_compatible`](api/george/misc.md#pytvpaint.george.grg_base.min_version_compatible) | When used as decorator, checks if the tvp instance making the call is above the miniumum version needed. | +| [`deprecated_warning`](api/george/misc.md#pytvpaint.george.grg_base.deprecated_warning) | When used as decorator, will log a warning message anytime the decorated function is called. | +| [`is_tvp_version_below_12`](api/george/misc.md#pytvpaint.george.grg_base.is_tvp_version_below_12) | Returns True if the tvp instances version is below 12, False otherwise. | + + +Below is also a list of the current breaking changes and bugs we noticed during development : + +### George Documentation +* George documentation is still not up to date and still contains many errors (seems stuck at some older TVP11 version), for now prefer the PyTVPaint documentation whenever possible. + +### General +* UI doesn't always update when values are set/updated via code, you either have to wait a few seconds for a refresh, or click somewhere else. This might lead to errors when users are using the UI at the same time. + +### C++ Plugin +* [BUG] plugin PIRF_HIDDEN_REQ and FILTERREQ_NO_TBAR don't seem to be working anymore, the plugin window is now visible and closing it kills the plugin with no way to restart it without restarting tvpaint. + +### Project +* [DEPRECATED/BREAKING] [`tv_ProjectInfo`](api/george/project.md#pytvpaint.george.grg_project.tv_project_info) values of `field_order` have been removed so for now we provide it ourselves. +* [BUG] [`tv_project_save_sequence`](api/george/project.md#pytvpaint.george.grg_project.tv_project_save_sequence) does not render empty instances even if they exist in the timeline. + +### Scene +* [BUG] [`tv_SceneCreate`](api/george/scene.md#pytvpaint.george.grg_scene.tv_scene_create) doesn't seem to work. + +### Clip +* [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json %fi does not work for folder structure (in v11 and v12) +* [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json seems to output low quality images, this may use the same function `george.tv_save_image`. + +### Layers +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually `tv_CTGGetSource` without the `s` at the end. +* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) actually requires and returns layer Ids not names. +* Child layers have no way of knowing if they are in a folder layer or which one. +* A Folder layer has no way of knowing which layers are its children. +* [BUG] Creating a CTG layer from a folder crashes TVPaint. +* [BUG] Moving a layer that is already in a folder in the same folder crashes TVPaint. +* `tv_LayerMove` can now move layers into folders but the position is relative to the root and not the folder, this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers in and out of folders. +* Not providing a `FolderID` to `tv_LayerMove` doesn't move the layer to the root, you just need to move outside the folder range for it to work, which again is pretty tough to do since we can't get the child layers of a folder and therefore their positions. +* Moving a layer inside a folder can be done without providing a `FolderID`, just by moving the layer in the folder's range. +* [BUG] CTG layer sometimes takes a while to update in the UI (a few seconds), which means they can be unintentionally edited or reset before the update. +* CameraLayer is not really a layer and most layer functions will ignore it, prefer use of PyTVPaint `Camera` object instead. +* [BUG] Selecting the Camera Layer in the UI now disables/grays out most layer related tools (this is a new behavior), this causes a lot of errors when using pipeline tools or TVPaint panels as TVPaint now raises an error messages anytime you try to use a tool that is not camera related when the camera layer is selected. +* [BUG] `tv_LayerRename` does not work in TVP12 and will replace the name with an empty string, this is either a new bug or there is now a new argument that is needed but not documented. +* [BREAKING] default layers name is now `Anim_X` and when these layers are queried for their names they return an empty string. +* [BUG] `tv_layer_move` still sets position at (value-1) when value is superior to 0. +* [BUG] `tv_preserve_set` does not seem to work in TVP12 +* [BUG] `tv_LayerColor setcolor` will fail when the `name` argument is provided. +* [BUG] `tv_LayerColor setcolor` no longer returns -1 when given a bad color index. +* [BUG] `tv_layer_move` no longer returns -1 when given a bad position. +* [BUG] `tv_LayerCreate` should not be used when an empty string as it will consider the new variable `layer_type` as the name. +* [BUG] Many layer functions that used to return -1 when given a bad layer id or position no longer do so, this can lead to silent errors and breaking behavior, this also breaks any way to check/validate these functions return values. The functions are : + * `tv_layer_set` + * `tv_LayerSelection` + * `tv_layer_kill` + * `tv_LayerBlendingMode` + * `tv_LayerStencil` + * `tv_LayerPreBehavior` + * `tv_LayerPostBehavior` + * `tv_LayerLockPosition` + * `tv_LayerMarkSet` + +### Camera +* [DEPRECATED/BREAKING] [`Camera.anti_aliasing`](api/objects/camera.md#pytvpaint.camera.Camera.anti_aliasing) no longer returns anti_aliasing, for now it always returns 1. +* [DEPRECATED/BREAKING] [`Camera.fps`](api/objects/camera.md#pytvpaint.camera.Camera.fps) no longer returns/sets fps, now only fps is Project fps. +* [BUG] Most Camera values can still be edited/queried using [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) but they are not immediately reflected in the + UI, and so they can be unintentionally edited or reset. +* [DEPRECATED/BREAKING] [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) values of pixel aspect ratio and fps have been "swapped" (since camera fps is no longer provided). +* [BUG] [`tv_CameraInterpolation`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_interpolation) doesn't work properly in TVP12 and always returns an "empty" point. + +### Guidelines +* `tv_GuidelineModify "marks"` doesn't seem to work, values are never changed, added function with a warning. + +--- + +## [1.0.2] - 2024-10-02 +### Fixes +* FIX save_dependencies functions #14 + +--- + +## [1.0.1] - 2024-09-16 +### Fixes +* HOTFIX tv_save_sequence range handling + +--- + +## [1.0.0] - 2024-06-21 +### Added +* Releasing first production version after successful use in projects at Brunch Studio + +--- + +## [1.0.0b9] - 2024-05-28 +### Fixes +* FIX tv_load_sequence/tv_load_image always stretching loaded media + +### Features/Updates +* UPDATE default values for functions : + * grg_clip.tv_load_sequence + * grg_clip.tv_sound_clip_adjust + * grg_layer.tv_load_image + * grg_project.tv_load_project + * grg_project.tv_project_save_sequence + * grg_project.tv_frame_rate_project_set + * grg_project.tv_sound_project_adjust + +--- + +## [1.0.0b8] - 2024-05-22 +### Features/Updates +* UPDATE george.grg_clip.tv_clip_save_structure_json default values and param names + +### Fixes +* FIX Layer.new_background_layer not working when providing an image to set + +--- + +## [1.0.0b7] - 2024-04-30 +### Features/Updates +* UPDATE clip json export to handle all options (fill_bakcground, all_images, etc...) +* [BREAKING CHANGES] UPDATE tv_clip_save_structure_json default values + +--- + +## [1.0.0b6] - 2024-04-22 +### Features/Updates +* Updated documentation +* Added new functions in Layer class : render and render_instances +* Clamp values for position to minimum 0 +* Updated `get_` functions in classes to return None instead of raising an error when element not found +* Update tests +* [BREAKING CHANGES] Updated function signatures and default values for: + * Clip.load_media + * Clip.render + * Clip.export_json + * Clip.export_psd + * Clip.export_csv + * Clip.export_sprites + * Clip.export_flix + * Clip.set_layer_color + * Layer.render_frame + * Project.render + * Project.render_clips + +### Fixes +* Fix layer position not setting position 0 correctly +* Fix mark_in/mark_out not set correctly + +--- + +## [1.0.0b5] - 2024-04-02 +### Fixes +* FIX client auto-connect not working properly +* FIX background colors set in Project class + +--- + +## [1.0.0b4] - 2024-03-28 +### Features/Updates +* Change `tv_background_set` George function signature to accepts a single color argument of type `tuple[RGBColor, RGBColor] | RGBColor | None` to be coherent with `tv_background_get`. #9 + +--- + +## [1.0.0b3] - 2024-03-27 +### Added +* ADD background mode option in render functions +* UPDATE background mode and color functions in API + +### Fixes +* FIX render context setting save mode instead of alpha mode \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 58f553c..8e0a73b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,6 +49,7 @@ nav: - Internals: contributing/internals.md - Wrapping George commands: contributing/wrap_george.md - Modifying high-level classes: contributing/modify_objects.md + - Changelog: changelog.md - Credits: credits.md - C++ plugin: - Introduction: cpp/index.md From 20dbf194b2501ad8f5ba2be4fc14a523e0d200f0 Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Wed, 13 May 2026 17:09:54 +0200 Subject: [PATCH 25/26] CLEAN code --- pytvpaint/george/client/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytvpaint/george/client/rpc.py b/pytvpaint/george/client/rpc.py index da26e97..0c6693c 100644 --- a/pytvpaint/george/client/rpc.py +++ b/pytvpaint/george/client/rpc.py @@ -113,7 +113,7 @@ def connect(self, timeout: int = 0) -> None: sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # Apply OS-specific keepalive configurations if available - if hasattr(socket, "TCP_KEEPIDLE"): # linux and windows specific + if hasattr(socket, "TCP_KEEPIDLE"): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, self.timeout) if hasattr(socket, "TCP_KEEPINTVL"): probe_interval = max(1, max(self.timeout, 1) // 6) From 6c3ee1b14d36850902a425cd6589304c62c3942d Mon Sep 17 00:00:00 2001 From: rlahmidi Date: Wed, 13 May 2026 18:20:17 +0200 Subject: [PATCH 26/26] ADD changelog CI --- .github/workflows/update-changelog.yml | 62 +++++++++ CHANGELOG.md | 177 ------------------------- 2 files changed, 62 insertions(+), 177 deletions(-) create mode 100644 .github/workflows/update-changelog.yml diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 0000000..c796ec4 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,62 @@ +name: Update Changelogs on Release + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + append-release-notes: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Process Release Metadata and Update Root Changelog + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_DATE: ${{ github.event.release.published_at }} + RELEASE_BODY: ${{ github.event.release.body }} + run: | + python3 -c ' + import os + from pathlib import Path + from datetime import datetime + + tag = os.environ.get("RELEASE_TAG", "Unversioned") + date_str = os.environ.get("RELEASE_DATE", "") + body = os.environ.get("RELEASE_BODY", "") + + try: + date_obj = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ") + formatted_date = date_obj.strftime("%Y-%m-%d") + except ValueError: + formatted_date = datetime.utcnow().strftime("%Y-%m-%d") + + new_entry = f"## [{tag}] - {formatted_date}\n\n{body}\n\n---\n\n" + + file_path = Path("CHANGELOG.md") + content = file_path.read_text() + + # Inject the new entry precisely below the standardized header + insert_marker = "# Changelog\n\n" + new_content = content.replace(insert_marker, insert_marker + new_entry, 1) + content = file_path.write_text(new_content) + ' + + - name: Synchronize to Docs Directory + run: | + mkdir -p docs + cp CHANGELOG.md docs/changelog.md + + - name: Commit and Push Changes + run: | + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add CHANGELOG.md docs/changelog.md + + git diff --quiet && git diff --staged --quiet || (git commit -m "docs: append release notes for ${{ github.event.release.tag_name }}" && git push) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a65a9cb..e80ad72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,182 +1,5 @@ # Changelog -## [1.1.0] - 2024-05-13 -### Features and Updates - -* Improved parsing logic and added a new features to handle tvpaint not escaping characters -* Added option to set aliases in the dataclass fields to have more pythonic variable names -* Updated `Layer.render...` functions to use the parent clips render instead of `george.tv_save_image` -* Updated `render` functions to use `Fileseq.FrameSet` instead of a `(start, end)` this allows rendering of non consecutive ranges and makes range handling more streamlined. -* Improved `utils.get_tvp_element` functions and added searching `by_regex` as well -* Updated all tests for compatibility with tvpaint 12 -* Improved documentation and fixed some typos -* Move project from `poetry` to `hatch` - -### George - -* Added dataclass `george.RGBAColor` -* Added function `george.tv_pick_color` -* Added warning to `george.tv_save_image` as it outputs low quality images -* added `to_extension` in SaveFormat - -### Project - -* Added Support for Guidelines in george functions and python classes - * new module `george/grg_guidelines.py` - * new module `guideline.py` with classes : - * `guideline.GuidelineImage` - * `guideline.GuidelineLine` - * `guideline.GuidelineSegment` - * `guideline.GuidelineCircle` - * `guideline.GuidelineEllipse` - * `guideline.GuidelineGrid` - * `guideline.GuidelineMarks` - * `guideline.GuidelineSafeArea` - * `guideline.GuidelineFieldChart` - * `guideline.GuidelineAnimatorField` - * `guideline.GuidelineVanishPoint1` - * `guideline.GuidelineVanishPoint2` - * `guideline.GuidelineVanishPoint3` -* Added `Project.guidelines` and `Project.add_guideline_...` functions to `Project` class - -### Scene - -* Added new `Scene.split()` function to `Scene` based on the new function `tv_SceneSplit`. -* Added new george function `tv_SceneCreate`, however this functions doesn't seem to work for now, check bugs and breaking changes section. - -### Clip - -* Added `Clip.ctg_layers()` property to `Clip`. -* Added `Clip.folders()` property to `Clip`. -* Added `Clip.camera_layer()` property to `Clip`. -* Added `Clip.get_layers()` property to `Clip` which returns the clip's layers using filters (use this instead of `Clip.layers` when using TVP 12 or above) .Added `Clip.camera_layer()` property to `Clip`. -* [DEPRECATED] `Clip.layers()` is deprecated and only returns animation layers (this remains unchanged from before) and ignores all new Folder, Camera and CTG layers. Prefer `Clip.get_layers()` instead. -* Added support for new unescape logic to : - * `Clip.action_text` - * `Clip.dialog_text` - * `Clip.note_text` - * `Project.header_info` - * `Project.author` - * `Project.notes` - -### Layers - -* `tv_LayerMove` now takes a `folder_id` argument, more on this in the bugs sections. -* `tv_LayerCreate` now takes a `layer_type` argument to create a layer folder. -* Added new object `LayerFolder` which uses the new layer folder functions (e.g: `tv_LayerFolderDelete`). -* Added new `Layer.folder` property in `Layer`. -* Added new `Layer.set_folder_position` function in `Layer` though it is pretty finicky to use (check bugs and breaking changes section). -* Added new object `CameraLayer`. -* Added new object `CTGLayer` which uses the new CTG layer functions : - \* `CTGLayer.squiggles_visible` / `tv_CTGSquigglesVisible`. - \* `CTGLayer.apply_changes` / `tv_CTGApplyChanges`. - \* `CTGLayer.sources` / `tv_CTGGetSource`. - \* `CTGLayer.add_sources` and `CTGLayer.remove_sources` / `tv_CTGSource`. - \* `CTGLayer.load_structure` / `tv_CTGLoadStructure`. -* Added new `Layer.is_ctg_layer` property in `Layer`. -* Added new `Layer.is_ctg_source` property in `Layer`. -* Added new `Layer.sourced_ctg_layers` property in `Layer`. -* Added `Layer.pan` function in `Layer`. -* Added `Layer.cut` , `Layer.copy`, and `Layer.paste` functions to the `Layer` class. -* Added `Layer.render_instances` function which only renders the instances in the layer not the whole sequence. - -### Camera - -* `Camera.get_point_data_at()` now returns a new read-only `InterpolationCameraPoint` object (inherited from `CameraPoint`). -* `Camera.get_point_data_at_frame()` now returns a new read-only `FrameCameraPoint` object (inherited from `CameraPoint`). -* `CameraPoint` object now has two new properties : `CameraPoint.width` and `CameraPoint.height`. - -*** - -### Fixes - -* fixed an issue with the values provided to `get_unique_name()` -* fixed `test_tv_clip_save_structure_json` file format -* fixed misc small bugs/typos - -*** - -### TVPaint 12 Bugs and Breaking Changes - -TVPaint 12 introduces a lot of welcome changes (especially for the artists) and some new needed functions for developers. -However, it also introduces a lot of breaking changes and some new bugs. - -To deal with these changes, as well as the new functions exclusive to TVPaint 12, we added a few decorators and functions : - -| Method | Description | -|:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| -| [`min_version_compatible`](api/george/misc.md#pytvpaint.george.grg_base.min_version_compatible) | When used as decorator, checks if the tvp instance making the call is above the miniumum version needed. | -| [`deprecated_warning`](api/george/misc.md#pytvpaint.george.grg_base.deprecated_warning) | When used as decorator, will log a warning message anytime the decorated function is called. | -| [`is_tvp_version_below_12`](api/george/misc.md#pytvpaint.george.grg_base.is_tvp_version_below_12) | Returns True if the tvp instances version is below 12, False otherwise. | - - -Below is also a list of the current breaking changes and bugs we noticed during development : - -### George Documentation -* George documentation is still not up to date and still contains many errors (seems stuck at some older TVP11 version), for now prefer the PyTVPaint documentation whenever possible. - -### General -* UI doesn't always update when values are set/updated via code, you either have to wait a few seconds for a refresh, or click somewhere else. This might lead to errors when users are using the UI at the same time. - -### C++ Plugin -* [BUG] plugin PIRF_HIDDEN_REQ and FILTERREQ_NO_TBAR don't seem to be working anymore, the plugin window is now visible and closing it kills the plugin with no way to restart it without restarting tvpaint. - -### Project -* [DEPRECATED/BREAKING] [`tv_ProjectInfo`](api/george/project.md#pytvpaint.george.grg_project.tv_project_info) values of `field_order` have been removed so for now we provide it ourselves. -* [BUG] [`tv_project_save_sequence`](api/george/project.md#pytvpaint.george.grg_project.tv_project_save_sequence) does not render empty instances even if they exist in the timeline. - -### Scene -* [BUG] [`tv_SceneCreate`](api/george/scene.md#pytvpaint.george.grg_scene.tv_scene_create) doesn't seem to work. - -### Clip -* [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json %fi does not work for folder structure (in v11 and v12) -* [BUG] [`tv_ClipSaveStructure`](api/george/clip.md#pytvpaint.george.grg_clip.tv_clip_save_structure_json) json seems to output low quality images, this may use the same function `george.tv_save_image`. - -### Layers -* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) is actually misspelled and is actually `tv_CTGGetSource` without the `s` at the end. -* [ERROR] [`tv_CTGGetSources`](api/george/layer.md#pytvpaint.george.grg_layer.tv_ctg_get_source) actually requires and returns layer Ids not names. -* Child layers have no way of knowing if they are in a folder layer or which one. -* A Folder layer has no way of knowing which layers are its children. -* [BUG] Creating a CTG layer from a folder crashes TVPaint. -* [BUG] Moving a layer that is already in a folder in the same folder crashes TVPaint. -* `tv_LayerMove` can now move layers into folders but the position is relative to the root and not the folder, this is not ideal, since we can't know if a layer is already in a folder or not, which adds a lot of uncertainty when moving layers in and out of folders. -* Not providing a `FolderID` to `tv_LayerMove` doesn't move the layer to the root, you just need to move outside the folder range for it to work, which again is pretty tough to do since we can't get the child layers of a folder and therefore their positions. -* Moving a layer inside a folder can be done without providing a `FolderID`, just by moving the layer in the folder's range. -* [BUG] CTG layer sometimes takes a while to update in the UI (a few seconds), which means they can be unintentionally edited or reset before the update. -* CameraLayer is not really a layer and most layer functions will ignore it, prefer use of PyTVPaint `Camera` object instead. -* [BUG] Selecting the Camera Layer in the UI now disables/grays out most layer related tools (this is a new behavior), this causes a lot of errors when using pipeline tools or TVPaint panels as TVPaint now raises an error messages anytime you try to use a tool that is not camera related when the camera layer is selected. -* [BUG] `tv_LayerRename` does not work in TVP12 and will replace the name with an empty string, this is either a new bug or there is now a new argument that is needed but not documented. -* [BREAKING] default layers name is now `Anim_X` and when these layers are queried for their names they return an empty string. -* [BUG] `tv_layer_move` still sets position at (value-1) when value is superior to 0. -* [BUG] `tv_preserve_set` does not seem to work in TVP12 -* [BUG] `tv_LayerColor setcolor` will fail when the `name` argument is provided. -* [BUG] `tv_LayerColor setcolor` no longer returns -1 when given a bad color index. -* [BUG] `tv_layer_move` no longer returns -1 when given a bad position. -* [BUG] `tv_LayerCreate` should not be used when an empty string as it will consider the new variable `layer_type` as the name. -* [BUG] Many layer functions that used to return -1 when given a bad layer id or position no longer do so, this can lead to silent errors and breaking behavior, this also breaks any way to check/validate these functions return values. The functions are : - * `tv_layer_set` - * `tv_LayerSelection` - * `tv_layer_kill` - * `tv_LayerBlendingMode` - * `tv_LayerStencil` - * `tv_LayerPreBehavior` - * `tv_LayerPostBehavior` - * `tv_LayerLockPosition` - * `tv_LayerMarkSet` - -### Camera -* [DEPRECATED/BREAKING] [`Camera.anti_aliasing`](api/objects/camera.md#pytvpaint.camera.Camera.anti_aliasing) no longer returns anti_aliasing, for now it always returns 1. -* [DEPRECATED/BREAKING] [`Camera.fps`](api/objects/camera.md#pytvpaint.camera.Camera.fps) no longer returns/sets fps, now only fps is Project fps. -* [BUG] Most Camera values can still be edited/queried using [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) but they are not immediately reflected in the - UI, and so they can be unintentionally edited or reset. -* [DEPRECATED/BREAKING] [`tv_CameraInfo`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_info_get) values of pixel aspect ratio and fps have been "swapped" (since camera fps is no longer provided). -* [BUG] [`tv_CameraInterpolation`](api/george/camera.md#pytvpaint.george.grg_camera.tv_camera_interpolation) doesn't work properly in TVP12 and always returns an "empty" point. - -### Guidelines -* `tv_GuidelineModify "marks"` doesn't seem to work, values are never changed, added function with a warning. - ---- - ## [1.0.2] - 2024-10-02 ### Fixes * FIX save_dependencies functions #14