chore(lib): simplify how to add config options (#321)

* chore(lib): simplify how to add config options

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(lint): some fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jérome Eertmans
2023-11-23 15:38:49 +01:00
committed by GitHub
parent eb8efa8e3d
commit b09a000c17
7 changed files with 111 additions and 57 deletions

View File

@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
to slide config. to slide config.
[#320](https://github.com/jeertmans/manim-slides/pull/320) [#320](https://github.com/jeertmans/manim-slides/pull/320)
(v5.1-modified)=
### Modified
- Modified the internal logic to simplify adding configuration options.
[#321](https://github.com/jeertmans/manim-slides/pull/321)
## [v5](https://github.com/jeertmans/manim-slides/compare/v4.16.0...v5.0.0) ## [v5](https://github.com/jeertmans/manim-slides/compare/v4.16.0...v5.0.0)
Prior to v5, there was no real CHANGELOG other than the GitHub releases, Prior to v5, there was no real CHANGELOG other than the GitHub releases,

View File

@ -1,5 +1,7 @@
import json import json
import shutil import shutil
from functools import wraps
from inspect import Parameter, signature
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Set, Tuple from typing import Any, Callable, Dict, List, Optional, Set, Tuple
@ -135,14 +137,76 @@ class Config(BaseModel): # type: ignore[misc]
return self return self
class PreSlideConfig(BaseModel): # type: ignore class BaseSlideConfig(BaseModel): # type: ignore
start_animation: int """Base class for slide config."""
end_animation: int
loop: bool = False loop: bool = False
auto_next: bool = False auto_next: bool = False
playback_rate: float = 1.0 playback_rate: float = 1.0
reversed_playback_rate: float = 1.0 reversed_playback_rate: float = 1.0
@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
"""
Wrap a function to transform keyword argument into an instance of this class.
The function signature is updated to reflect the new keyword-only arguments.
The wrapped function must follow two criteria:
- its last parameter must be ``**kwargs`` (or equivalent);
- and its second last parameter must be ``<arg_name>``.
"""
def _wrapper_(fun: Callable[..., Any]) -> Callable[..., Any]:
@wraps(fun)
def __wrapper__(*args: Any, **kwargs: Any) -> Any: # noqa: N807
fun_kwargs = {
key: value
for key, value in kwargs.items()
if key not in cls.__fields__
}
fun_kwargs[arg_name] = cls(**kwargs)
return fun(*args, **fun_kwargs)
sig = signature(fun)
parameters = list(sig.parameters.values())
parameters[-2:-1] = [
Parameter(
field_name,
Parameter.KEYWORD_ONLY,
default=field_info.default,
annotation=field_info.annotation,
)
for field_name, field_info in cls.__fields__.items()
]
sig = sig.replace(parameters=parameters)
__wrapper__.__signature__ = sig # type: ignore[attr-defined]
return __wrapper__
return _wrapper_
class PreSlideConfig(BaseSlideConfig):
"""Slide config to be used prior to rendering."""
start_animation: int
end_animation: int
@classmethod
def from_base_slide_config_and_animation_indices(
cls,
base_slide_config: BaseSlideConfig,
start_animation: int,
end_animation: int,
) -> "PreSlideConfig":
return cls(
start_animation=start_animation,
end_animation=end_animation,
**base_slide_config.dict(),
)
@field_validator("start_animation", "end_animation") @field_validator("start_animation", "end_animation")
@classmethod @classmethod
def index_is_posint(cls, v: int) -> int: def index_is_posint(cls, v: int) -> int:
@ -187,26 +251,17 @@ class PreSlideConfig(BaseModel): # type: ignore
return slice(self.start_animation, self.end_animation) return slice(self.start_animation, self.end_animation)
class SlideConfig(BaseModel): # type: ignore[misc] class SlideConfig(BaseSlideConfig):
"""Slide config to be used after rendering."""
file: FilePath file: FilePath
rev_file: FilePath rev_file: FilePath
loop: bool = False
auto_next: bool = False
playback_rate: float = 1.0
reversed_playback_rate: float = 1.0
@classmethod @classmethod
def from_pre_slide_config_and_files( def from_pre_slide_config_and_files(
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
) -> "SlideConfig": ) -> "SlideConfig":
return cls( return cls(file=file, rev_file=rev_file, **pre_slide_config.dict())
file=file,
rev_file=rev_file,
loop=pre_slide_config.loop,
auto_next=pre_slide_config.auto_next,
playback_rate=pre_slide_config.playback_rate,
reversed_playback_rate=pre_slide_config.reversed_playback_rate,
)
class PresentationConfig(BaseModel): # type: ignore[misc] class PresentationConfig(BaseModel): # type: ignore[misc]

View File

@ -217,7 +217,9 @@ def start_at_callback(
metavar="RATE", metavar="RATE",
type=float, type=float,
default=1.0, default=1.0,
help="Playback rate of the video slides, see PySide6 docs for details.", help="Playback rate of the video slides, see PySide6 docs for details. "
" The playback rate of each slide is defined as the product of its default "
" playback rate and the provided value.",
) )
@click.option( @click.option(
"--next-terminates-loop", "--next-terminates-loop",

View File

@ -104,7 +104,7 @@ class Player(QMainWindow): # type: ignore[misc]
self.media_player = QMediaPlayer(self) self.media_player = QMediaPlayer(self)
self.media_player.setVideoOutput(self.video_widget) self.media_player.setVideoOutput(self.video_widget)
self.media_player.setPlaybackRate(playback_rate) self.playback_rate = playback_rate
self.presentation_changed.connect(self.presentation_changed_callback) self.presentation_changed.connect(self.presentation_changed_callback)
self.slide_changed.connect(self.slide_changed_callback) self.slide_changed.connect(self.slide_changed_callback)
@ -238,10 +238,12 @@ class Player(QMainWindow): # type: ignore[misc]
if self.playing_reversed_slide: if self.playing_reversed_slide:
self.media_player.setPlaybackRate( self.media_player.setPlaybackRate(
self.current_slide_config.reversed_playback_rate self.current_slide_config.reversed_playback_rate * self.playback_rate
) )
else: else:
self.media_player.setPlaybackRate(self.current_slide_config.playback_rate) self.media_player.setPlaybackRate(
self.current_slide_config.playback_rate * self.playback_rate
)
if start_paused: if start_paused:
self.media_player.pause() self.media_player.pause()

View File

@ -8,7 +8,7 @@ from typing import Any, List, MutableMapping, Optional, Sequence, Tuple, ValuesV
import numpy as np import numpy as np
from tqdm import tqdm from tqdm import tqdm
from ..config import PresentationConfig, PreSlideConfig, SlideConfig from ..config import BaseSlideConfig, PresentationConfig, PreSlideConfig, SlideConfig
from ..defaults import FFMPEG_BIN, FOLDER_PATH from ..defaults import FFMPEG_BIN, FOLDER_PATH
from ..logger import logger from ..logger import logger
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
@ -29,7 +29,7 @@ class BaseSlide:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._output_folder: Path = output_folder self._output_folder: Path = output_folder
self._slides: List[PreSlideConfig] = [] self._slides: List[PreSlideConfig] = []
self._pre_slide_config_kwargs: MutableMapping[str, Any] = {} self._base_slide_config: BaseSlideConfig = BaseSlideConfig()
self._current_slide = 1 self._current_slide = 1
self._current_animation = 0 self._current_animation = 0
self._start_animation = 0 self._start_animation = 0
@ -254,13 +254,11 @@ class BaseSlide:
super().play(*args, **kwargs) # type: ignore[misc] super().play(*args, **kwargs) # type: ignore[misc]
self._current_animation += 1 self._current_animation += 1
@BaseSlideConfig.wrapper("base_slide_config")
def next_slide( def next_slide(
self, self,
*, *,
loop: bool = False, base_slide_config: BaseSlideConfig,
auto_next: bool = False,
playback_rate: float = 1.0,
reversed_playback_rate: float = 1.0,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
""" """
@ -380,21 +378,16 @@ class BaseSlide:
self.wait(self.wait_time_between_slides) # type: ignore[attr-defined] self.wait(self.wait_time_between_slides) # type: ignore[attr-defined]
self._slides.append( self._slides.append(
PreSlideConfig( PreSlideConfig.from_base_slide_config_and_animation_indices(
start_animation=self._start_animation, self._base_slide_config,
end_animation=self._current_animation, self._start_animation,
**self._pre_slide_config_kwargs, self._current_animation,
) )
) )
self._current_slide += 1 self._current_slide += 1
self._pre_slide_config_kwargs = dict( self._base_slide_config = base_slide_config
loop=loop,
auto_next=auto_next,
playback_rate=playback_rate,
reversed_playback_rate=reversed_playback_rate,
)
self._start_animation = self._current_animation self._start_animation = self._current_animation
def _add_last_slide(self) -> None: def _add_last_slide(self) -> None:
@ -406,10 +399,10 @@ class BaseSlide:
return return
self._slides.append( self._slides.append(
PreSlideConfig( PreSlideConfig.from_base_slide_config_and_animation_indices(
start_animation=self._start_animation, self._base_slide_config,
end_animation=self._current_animation, self._start_animation,
**self._pre_slide_config_kwargs, self._current_animation,
) )
) )

View File

@ -3,6 +3,7 @@ from typing import Any, List, Optional, Tuple
from manim import Scene, ThreeDScene, config from manim import Scene, ThreeDScene, config
from ..config import BaseSlideConfig
from .base import BaseSlide from .base import BaseSlide
@ -79,22 +80,17 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
""" """
self.next_slide(*args, **kwargs) self.next_slide(*args, **kwargs)
@BaseSlideConfig.wrapper("base_slide_config")
def next_slide( def next_slide(
self, self,
*args: Any, *args: Any,
loop: bool = False, base_slide_config: BaseSlideConfig,
auto_next: bool = False,
playback_rate: float = 1.0,
reversed_playback_rate: float = 1.0,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
Scene.next_section(self, *args, **kwargs) Scene.next_section(self, *args, **kwargs)
BaseSlide.next_slide( BaseSlide.next_slide.__wrapped__(
self, self,
loop=loop, base_slide_config=base_slide_config,
auto_next=auto_next,
playback_rate=playback_rate,
reversed_playback_rate=reversed_playback_rate,
) )
def render(self, *args: Any, **kwargs: Any) -> None: def render(self, *args: Any, **kwargs: Any) -> None:

View File

@ -140,16 +140,16 @@ class TestSlide:
self.add(text) self.add(text)
assert "loop" not in self._pre_slide_config_kwargs assert not self._base_slide_config.loop
self.next_slide(loop=True) self.next_slide(loop=True)
self.play(text.animate.scale(2)) self.play(text.animate.scale(2))
assert self._pre_slide_config_kwargs["loop"] assert self._base_slide_config.loop
self.next_slide(loop=False) self.next_slide(loop=False)
assert not self._pre_slide_config_kwargs["loop"] assert not self._base_slide_config.loop
@assert_constructs @assert_constructs
class TestAutoNext(Slide): class TestAutoNext(Slide):
@ -158,16 +158,16 @@ class TestSlide:
self.add(text) self.add(text)
assert "auto_next" not in self._pre_slide_config_kwargs assert not self._base_slide_config.auto_next
self.next_slide(auto_next=True) self.next_slide(auto_next=True)
self.play(text.animate.scale(2)) self.play(text.animate.scale(2))
assert self._pre_slide_config_kwargs["auto_next"] assert self._base_slide_config.auto_next
self.next_slide(auto_next=False) self.next_slide(auto_next=False)
assert not self._pre_slide_config_kwargs["auto_next"] assert not self._base_slide_config.auto_next
@assert_constructs @assert_constructs
class TestLoopAndAutoNextFails(Slide): class TestLoopAndAutoNextFails(Slide):
@ -189,12 +189,12 @@ class TestSlide:
self.add(text) self.add(text)
assert "playback_rate" not in self._pre_slide_config_kwargs assert self._base_slide_config.playback_rate == 1.0
self.next_slide(playback_rate=2.0) self.next_slide(playback_rate=2.0)
self.play(text.animate.scale(2)) self.play(text.animate.scale(2))
assert self._pre_slide_config_kwargs["playback_rate"] == 2.0 assert self._base_slide_config.playback_rate == 2.0
@assert_constructs @assert_constructs
class TestWipe(Slide): class TestWipe(Slide):