Skip to content

Commit 529a6c5

Browse files
fix(lib/cli): relative paths, empty slides, and tests (#223)
* fix(lib/cli): relative paths, empty slides, and tests This fixes two issues: 1. Empty slides are now reported as error, to prevent indexing error; 2. Changing the folder path will now produce an absolute path to slides, which was not the case before and would lead to a "file does not exist error". A few tests were also added to cover those * fix(lib): fix from_file, remove useless field, and more * chore(tests): remove print * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 2b6240c commit 529a6c5

14 files changed

+200
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ __pycache__/
1010
/.vscode
1111

1212
slides/
13+
!tests/slides/
1314

1415
.manim-slides.json
1516

manim_slides/config.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import hashlib
2+
import json
23
import os
34
import shutil
45
import subprocess
@@ -7,7 +8,14 @@
78
from pathlib import Path
89
from typing import Any, Dict, List, Optional, Set, Tuple, Union
910

10-
from pydantic import BaseModel, FilePath, PositiveInt, field_validator, model_validator
11+
from pydantic import (
12+
BaseModel,
13+
Field,
14+
FilePath,
15+
PositiveInt,
16+
field_validator,
17+
model_validator,
18+
)
1119
from pydantic_extra_types.color import Color
1220
from PySide6.QtCore import Qt
1321

@@ -71,6 +79,17 @@ class Config(BaseModel): # type: ignore
7179
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
7280
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
7381

82+
@classmethod
83+
def from_file(cls, path: Path) -> "Config":
84+
"""Reads a configuration from a file."""
85+
with open(path, "r") as f:
86+
return cls.model_validate_json(f.read()) # type: ignore
87+
88+
def to_file(self, path: Path) -> None:
89+
"""Dumps the configuration to a file."""
90+
with open(path, "w") as f:
91+
f.write(self.model_dump_json(indent=2))
92+
7493
@model_validator(mode="before")
7594
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
7695
ids: Set[int] = set()
@@ -104,7 +123,7 @@ class SlideConfig(BaseModel): # type: ignore
104123
start_animation: int
105124
end_animation: int
106125
number: int
107-
terminated: bool = False
126+
terminated: bool = Field(False, exclude=True)
108127

109128
@field_validator("start_animation", "end_animation")
110129
@classmethod
@@ -151,11 +170,31 @@ def slides_slice(self) -> slice:
151170

152171

153172
class PresentationConfig(BaseModel): # type: ignore
154-
slides: List[SlideConfig]
173+
slides: List[SlideConfig] = Field(min_length=1)
155174
files: List[FilePath]
156175
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
157176
background_color: Color = "black"
158177

178+
@classmethod
179+
def from_file(cls, path: Path) -> "PresentationConfig":
180+
"""Reads a presentation configuration from a file."""
181+
with open(path, "r") as f:
182+
obj = json.load(f)
183+
184+
if files := obj.get("files", None):
185+
# First parent is ../slides
186+
# so we take the parent of this parent
187+
parent = Path(path).parents[1]
188+
for i in range(len(files)):
189+
files[i] = parent / files[i]
190+
191+
return cls.model_validate(obj) # type: ignore
192+
193+
def to_file(self, path: Path) -> None:
194+
"""Dumps the presentation configuration to a file."""
195+
with open(path, "w") as f:
196+
f.write(self.model_dump_json(indent=2))
197+
159198
@model_validator(mode="after")
160199
def animation_indices_match_files(
161200
cls, config: "PresentationConfig"

manim_slides/present.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ def _list_scenes(folder: Path) -> List[str]:
786786

787787
for filepath in folder.glob("*.json"):
788788
try:
789-
_ = PresentationConfig.parse_file(filepath)
789+
_ = PresentationConfig.from_file(filepath)
790790
scenes.append(filepath.stem)
791791
except (
792792
Exception
@@ -851,7 +851,7 @@ def get_scenes_presentation_config(
851851
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
852852
)
853853
try:
854-
presentation_configs.append(PresentationConfig.parse_file(config_file))
854+
presentation_configs.append(PresentationConfig.from_file(config_file))
855855
except ValidationError as e:
856856
raise click.UsageError(str(e))
857857

@@ -1047,7 +1047,7 @@ def present(
10471047

10481048
if config_path.exists():
10491049
try:
1050-
config = Config.parse_file(config_path)
1050+
config = Config.from_file(config_path)
10511051
except ValidationError as e:
10521052
raise click.UsageError(str(e))
10531053
else:
@@ -1070,7 +1070,11 @@ def present(
10701070
if start_at[2]:
10711071
start_at_animation_number = start_at[2]
10721072

1073-
app = QApplication(sys.argv)
1073+
if not QApplication.instance():
1074+
app = QApplication(sys.argv)
1075+
else:
1076+
app = QApplication.instance()
1077+
10741078
app.setApplicationName("Manim Slides")
10751079
a = App(
10761080
presentations,

manim_slides/wizard.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import os
21
import sys
32
from functools import partial
3+
from pathlib import Path
44
from typing import Any
55

66
import click
@@ -102,7 +102,7 @@ def closeEvent(self, event: Any) -> None:
102102

103103
def saveConfig(self) -> None:
104104
try:
105-
Config.parse_obj(self.config.dict())
105+
Config.model_validate(self.config.dict())
106106
except ValueError:
107107
msg = QMessageBox()
108108
msg.setIcon(QMessageBox.Critical)
@@ -130,7 +130,7 @@ def openDialog(self, button_number: int, key: Key) -> None:
130130
@config_options
131131
@click.help_option("-h", "--help")
132132
@verbosity_option
133-
def wizard(config_path: str, force: bool, merge: bool) -> None:
133+
def wizard(config_path: Path, force: bool, merge: bool) -> None:
134134
"""Launch configuration wizard."""
135135
return _init(config_path, force, merge, skip_interactive=False)
136136

@@ -140,18 +140,18 @@ def wizard(config_path: str, force: bool, merge: bool) -> None:
140140
@click.help_option("-h", "--help")
141141
@verbosity_option
142142
def init(
143-
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
143+
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
144144
) -> None:
145145
"""Initialize a new default configuration file."""
146146
return _init(config_path, force, merge, skip_interactive=True)
147147

148148

149149
def _init(
150-
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
150+
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
151151
) -> None:
152152
"""Actual initialization code for configuration file, with optional interactive mode."""
153153

154-
if os.path.exists(config_path):
154+
if config_path.exists():
155155
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
156156

157157
if not force and not merge:
@@ -175,8 +175,8 @@ def _init(
175175
logger.debug("Merging new config into `{config_path}`")
176176

177177
if not skip_interactive:
178-
if os.path.exists(config_path):
179-
config = Config.parse_file(config_path)
178+
if config_path.exists():
179+
config = Config.from_file(config_path)
180180

181181
app = QApplication(sys.argv)
182182
app.setApplicationName("Manim Slides Wizard")
@@ -187,9 +187,8 @@ def _init(
187187
config = window.config
188188

189189
if merge:
190-
config = Config.parse_file(config_path).merge_with(config)
190+
config = Config.from_file(config_path).merge_with(config)
191191

192-
with open(config_path, "w") as config_file:
193-
config_file.write(config.json(indent=2))
192+
config.to_file(config_path)
194193

195194
click.secho(f"Configuration file successfully saved to `{config_path}`")

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
from pathlib import Path
2+
from typing import Iterator
3+
4+
import pytest
5+
16
from manim_slides.logger import make_logger
27

38
_ = make_logger() # This is run so that "PERF" level is created
9+
10+
11+
@pytest.fixture
12+
def folder_path() -> Iterator[Path]:
13+
yield (Path(__file__).parent / "slides").resolve()

tests/slides/BasicExample.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"slides": [
3+
{
4+
"type": "slide",
5+
"start_animation": 0,
6+
"end_animation": 1,
7+
"number": 1
8+
},
9+
{
10+
"type": "loop",
11+
"start_animation": 1,
12+
"end_animation": 2,
13+
"number": 2
14+
},
15+
{
16+
"type": "last",
17+
"start_animation": 2,
18+
"end_animation": 3,
19+
"number": 3
20+
}
21+
],
22+
"files": [
23+
"slides/files/BasicExample/1413466013_3346521118_223132457.mp4",
24+
"slides/files/BasicExample/1672018281_3136302242_2191168284.mp4",
25+
"slides/files/BasicExample/1672018281_1369283980_3942561600.mp4"
26+
],
27+
"resolution": [
28+
1920,
29+
1080
30+
],
31+
"background_color": "black"
32+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)