diff --git a/CHANGELOG.md b/CHANGELOG.md index b691c65..6bd38ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to slide config. [#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) Prior to v5, there was no real CHANGELOG other than the GitHub releases, diff --git a/manim_slides/config.py b/manim_slides/config.py index a086d8d..fffe537 100644 --- a/manim_slides/config.py +++ b/manim_slides/config.py @@ -1,5 +1,7 @@ import json import shutil +from functools import wraps +from inspect import Parameter, signature from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Set, Tuple @@ -135,14 +137,76 @@ class Config(BaseModel): # type: ignore[misc] return self -class PreSlideConfig(BaseModel): # type: ignore - start_animation: int - end_animation: int +class BaseSlideConfig(BaseModel): # type: ignore + """Base class for slide config.""" + loop: bool = False auto_next: bool = False 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 ````. + """ + + 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") @classmethod 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) -class SlideConfig(BaseModel): # type: ignore[misc] +class SlideConfig(BaseSlideConfig): + """Slide config to be used after rendering.""" + file: FilePath rev_file: FilePath - loop: bool = False - auto_next: bool = False - playback_rate: float = 1.0 - reversed_playback_rate: float = 1.0 @classmethod def from_pre_slide_config_and_files( cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path ) -> "SlideConfig": - return cls( - 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, - ) + return cls(file=file, rev_file=rev_file, **pre_slide_config.dict()) class PresentationConfig(BaseModel): # type: ignore[misc] diff --git a/manim_slides/present/__init__.py b/manim_slides/present/__init__.py index cbe39b0..0990c7f 100644 --- a/manim_slides/present/__init__.py +++ b/manim_slides/present/__init__.py @@ -217,7 +217,9 @@ def start_at_callback( metavar="RATE", type=float, 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( "--next-terminates-loop", diff --git a/manim_slides/present/player.py b/manim_slides/present/player.py index 1ea6681..f784007 100644 --- a/manim_slides/present/player.py +++ b/manim_slides/present/player.py @@ -104,7 +104,7 @@ class Player(QMainWindow): # type: ignore[misc] self.media_player = QMediaPlayer(self) 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.slide_changed.connect(self.slide_changed_callback) @@ -238,10 +238,12 @@ class Player(QMainWindow): # type: ignore[misc] if self.playing_reversed_slide: self.media_player.setPlaybackRate( - self.current_slide_config.reversed_playback_rate + self.current_slide_config.reversed_playback_rate * self.playback_rate ) 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: self.media_player.pause() diff --git a/manim_slides/slide/base.py b/manim_slides/slide/base.py index ba350ab..0875598 100644 --- a/manim_slides/slide/base.py +++ b/manim_slides/slide/base.py @@ -8,7 +8,7 @@ from typing import Any, List, MutableMapping, Optional, Sequence, Tuple, ValuesV import numpy as np 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 ..logger import logger from ..utils import concatenate_video_files, merge_basenames, reverse_video_file @@ -29,7 +29,7 @@ class BaseSlide: super().__init__(*args, **kwargs) self._output_folder: Path = output_folder self._slides: List[PreSlideConfig] = [] - self._pre_slide_config_kwargs: MutableMapping[str, Any] = {} + self._base_slide_config: BaseSlideConfig = BaseSlideConfig() self._current_slide = 1 self._current_animation = 0 self._start_animation = 0 @@ -254,13 +254,11 @@ class BaseSlide: super().play(*args, **kwargs) # type: ignore[misc] self._current_animation += 1 + @BaseSlideConfig.wrapper("base_slide_config") def next_slide( self, *, - loop: bool = False, - auto_next: bool = False, - playback_rate: float = 1.0, - reversed_playback_rate: float = 1.0, + base_slide_config: BaseSlideConfig, **kwargs: Any, ) -> None: """ @@ -380,21 +378,16 @@ class BaseSlide: self.wait(self.wait_time_between_slides) # type: ignore[attr-defined] self._slides.append( - PreSlideConfig( - start_animation=self._start_animation, - end_animation=self._current_animation, - **self._pre_slide_config_kwargs, + PreSlideConfig.from_base_slide_config_and_animation_indices( + self._base_slide_config, + self._start_animation, + self._current_animation, ) ) self._current_slide += 1 - self._pre_slide_config_kwargs = dict( - loop=loop, - auto_next=auto_next, - playback_rate=playback_rate, - reversed_playback_rate=reversed_playback_rate, - ) + self._base_slide_config = base_slide_config self._start_animation = self._current_animation def _add_last_slide(self) -> None: @@ -406,10 +399,10 @@ class BaseSlide: return self._slides.append( - PreSlideConfig( - start_animation=self._start_animation, - end_animation=self._current_animation, - **self._pre_slide_config_kwargs, + PreSlideConfig.from_base_slide_config_and_animation_indices( + self._base_slide_config, + self._start_animation, + self._current_animation, ) ) diff --git a/manim_slides/slide/manim.py b/manim_slides/slide/manim.py index 7863590..eb9677a 100644 --- a/manim_slides/slide/manim.py +++ b/manim_slides/slide/manim.py @@ -3,6 +3,7 @@ from typing import Any, List, Optional, Tuple from manim import Scene, ThreeDScene, config +from ..config import BaseSlideConfig from .base import BaseSlide @@ -79,22 +80,17 @@ class Slide(BaseSlide, Scene): # type: ignore[misc] """ self.next_slide(*args, **kwargs) + @BaseSlideConfig.wrapper("base_slide_config") def next_slide( self, *args: Any, - loop: bool = False, - auto_next: bool = False, - playback_rate: float = 1.0, - reversed_playback_rate: float = 1.0, + base_slide_config: BaseSlideConfig, **kwargs: Any, ) -> None: Scene.next_section(self, *args, **kwargs) - BaseSlide.next_slide( + BaseSlide.next_slide.__wrapped__( self, - loop=loop, - auto_next=auto_next, - playback_rate=playback_rate, - reversed_playback_rate=reversed_playback_rate, + base_slide_config=base_slide_config, ) def render(self, *args: Any, **kwargs: Any) -> None: diff --git a/tests/test_slide.py b/tests/test_slide.py index c36c3cb..63f6d63 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -140,16 +140,16 @@ class TestSlide: 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.play(text.animate.scale(2)) - assert self._pre_slide_config_kwargs["loop"] + assert self._base_slide_config.loop self.next_slide(loop=False) - assert not self._pre_slide_config_kwargs["loop"] + assert not self._base_slide_config.loop @assert_constructs class TestAutoNext(Slide): @@ -158,16 +158,16 @@ class TestSlide: 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.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) - assert not self._pre_slide_config_kwargs["auto_next"] + assert not self._base_slide_config.auto_next @assert_constructs class TestLoopAndAutoNextFails(Slide): @@ -189,12 +189,12 @@ class TestSlide: 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.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 class TestWipe(Slide):