Skip to content

Commit

Permalink
chore(lib): simplify how to add config options (#321)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
jeertmans and pre-commit-ci[bot] authored Nov 23, 2023
1 parent eb8efa8 commit b09a000
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 57 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
87 changes: 71 additions & 16 deletions manim_slides/config.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -135,14 +137,76 @@ def merge_with(self, other: "Config") -> "Config":
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 ``<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")
@classmethod
def index_is_posint(cls, v: int) -> int:
Expand Down Expand Up @@ -187,26 +251,17 @@ def slides_slice(self) -> slice:
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]
Expand Down
4 changes: 3 additions & 1 deletion manim_slides/present/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ def str_to_int_or_none(value: str) -> Optional[int]:
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",
Expand Down
8 changes: 5 additions & 3 deletions manim_slides/present/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def __init__(

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)
Expand Down Expand Up @@ -238,10 +238,12 @@ def load_current_media(self, start_paused: bool = False) -> None:

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()
Expand Down
33 changes: 13 additions & 20 deletions manim_slides/slide/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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
Expand All @@ -29,7 +29,7 @@ def __init__(
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
Expand Down Expand Up @@ -254,13 +254,11 @@ def play(self, *args: Any, **kwargs: Any) -> None:
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:
"""
Expand Down Expand Up @@ -380,21 +378,16 @@ def construct(self):
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:
Expand All @@ -406,10 +399,10 @@ def _add_last_slide(self) -> None:
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,
)
)

Expand Down
14 changes: 5 additions & 9 deletions manim_slides/slide/manim.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from manim import Scene, ThreeDScene, config

from ..config import BaseSlideConfig
from .base import BaseSlide


Expand Down Expand Up @@ -79,22 +80,17 @@ def next_section(self, *args: Any, **kwargs: Any) -> None:
"""
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:
Expand Down
16 changes: 8 additions & 8 deletions tests/test_slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,16 @@ def construct(self) -> None:

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):
Expand All @@ -158,16 +158,16 @@ def construct(self) -> None:

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):
Expand All @@ -189,12 +189,12 @@ def construct(self) -> None:

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):
Expand Down

0 comments on commit b09a000

Please sign in to comment.