mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-08-06 06:12:56 +08:00
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>
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@ -7,7 +8,14 @@ from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from pydantic import BaseModel, FilePath, PositiveInt, field_validator, model_validator
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
FilePath,
|
||||
PositiveInt,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_extra_types.color import Color
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
@ -71,6 +79,17 @@ class Config(BaseModel): # type: ignore
|
||||
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
|
||||
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> "Config":
|
||||
"""Reads a configuration from a file."""
|
||||
with open(path, "r") as f:
|
||||
return cls.model_validate_json(f.read()) # type: ignore
|
||||
|
||||
def to_file(self, path: Path) -> None:
|
||||
"""Dumps the configuration to a file."""
|
||||
with open(path, "w") as f:
|
||||
f.write(self.model_dump_json(indent=2))
|
||||
|
||||
@model_validator(mode="before")
|
||||
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
|
||||
ids: Set[int] = set()
|
||||
@ -104,7 +123,7 @@ class SlideConfig(BaseModel): # type: ignore
|
||||
start_animation: int
|
||||
end_animation: int
|
||||
number: int
|
||||
terminated: bool = False
|
||||
terminated: bool = Field(False, exclude=True)
|
||||
|
||||
@field_validator("start_animation", "end_animation")
|
||||
@classmethod
|
||||
@ -151,11 +170,31 @@ class SlideConfig(BaseModel): # type: ignore
|
||||
|
||||
|
||||
class PresentationConfig(BaseModel): # type: ignore
|
||||
slides: List[SlideConfig]
|
||||
slides: List[SlideConfig] = Field(min_length=1)
|
||||
files: List[FilePath]
|
||||
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
|
||||
background_color: Color = "black"
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> "PresentationConfig":
|
||||
"""Reads a presentation configuration from a file."""
|
||||
with open(path, "r") as f:
|
||||
obj = json.load(f)
|
||||
|
||||
if files := obj.get("files", None):
|
||||
# First parent is ../slides
|
||||
# so we take the parent of this parent
|
||||
parent = Path(path).parents[1]
|
||||
for i in range(len(files)):
|
||||
files[i] = parent / files[i]
|
||||
|
||||
return cls.model_validate(obj) # type: ignore
|
||||
|
||||
def to_file(self, path: Path) -> None:
|
||||
"""Dumps the presentation configuration to a file."""
|
||||
with open(path, "w") as f:
|
||||
f.write(self.model_dump_json(indent=2))
|
||||
|
||||
@model_validator(mode="after")
|
||||
def animation_indices_match_files(
|
||||
cls, config: "PresentationConfig"
|
||||
|
@ -786,7 +786,7 @@ def _list_scenes(folder: Path) -> List[str]:
|
||||
|
||||
for filepath in folder.glob("*.json"):
|
||||
try:
|
||||
_ = PresentationConfig.parse_file(filepath)
|
||||
_ = PresentationConfig.from_file(filepath)
|
||||
scenes.append(filepath.stem)
|
||||
except (
|
||||
Exception
|
||||
@ -851,7 +851,7 @@ def get_scenes_presentation_config(
|
||||
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
|
||||
)
|
||||
try:
|
||||
presentation_configs.append(PresentationConfig.parse_file(config_file))
|
||||
presentation_configs.append(PresentationConfig.from_file(config_file))
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e))
|
||||
|
||||
@ -1047,7 +1047,7 @@ def present(
|
||||
|
||||
if config_path.exists():
|
||||
try:
|
||||
config = Config.parse_file(config_path)
|
||||
config = Config.from_file(config_path)
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e))
|
||||
else:
|
||||
@ -1070,7 +1070,11 @@ def present(
|
||||
if start_at[2]:
|
||||
start_at_animation_number = start_at[2]
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
if not QApplication.instance():
|
||||
app = QApplication(sys.argv)
|
||||
else:
|
||||
app = QApplication.instance()
|
||||
|
||||
app.setApplicationName("Manim Slides")
|
||||
a = App(
|
||||
presentations,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
@ -102,7 +102,7 @@ class Wizard(QWidget): # type: ignore
|
||||
|
||||
def saveConfig(self) -> None:
|
||||
try:
|
||||
Config.parse_obj(self.config.dict())
|
||||
Config.model_validate(self.config.dict())
|
||||
except ValueError:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Critical)
|
||||
@ -130,7 +130,7 @@ class Wizard(QWidget): # type: ignore
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def wizard(config_path: str, force: bool, merge: bool) -> None:
|
||||
def wizard(config_path: Path, force: bool, merge: bool) -> None:
|
||||
"""Launch configuration wizard."""
|
||||
return _init(config_path, force, merge, skip_interactive=False)
|
||||
|
||||
@ -140,18 +140,18 @@ def wizard(config_path: str, force: bool, merge: bool) -> None:
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def init(
|
||||
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
|
||||
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
|
||||
) -> None:
|
||||
"""Initialize a new default configuration file."""
|
||||
return _init(config_path, force, merge, skip_interactive=True)
|
||||
|
||||
|
||||
def _init(
|
||||
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
|
||||
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
|
||||
) -> None:
|
||||
"""Actual initialization code for configuration file, with optional interactive mode."""
|
||||
|
||||
if os.path.exists(config_path):
|
||||
if config_path.exists():
|
||||
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
||||
|
||||
if not force and not merge:
|
||||
@ -175,8 +175,8 @@ def _init(
|
||||
logger.debug("Merging new config into `{config_path}`")
|
||||
|
||||
if not skip_interactive:
|
||||
if os.path.exists(config_path):
|
||||
config = Config.parse_file(config_path)
|
||||
if config_path.exists():
|
||||
config = Config.from_file(config_path)
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Manim Slides Wizard")
|
||||
@ -187,9 +187,8 @@ def _init(
|
||||
config = window.config
|
||||
|
||||
if merge:
|
||||
config = Config.parse_file(config_path).merge_with(config)
|
||||
config = Config.from_file(config_path).merge_with(config)
|
||||
|
||||
with open(config_path, "w") as config_file:
|
||||
config_file.write(config.json(indent=2))
|
||||
config.to_file(config_path)
|
||||
|
||||
click.secho(f"Configuration file successfully saved to `{config_path}`")
|
||||
|
Reference in New Issue
Block a user