Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(lib/cli): relative paths, empty slides, and tests #223

Merged
merged 4 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ __pycache__/
/.vscode

slides/
!tests/slides/

.manim-slides.json

Expand Down
45 changes: 42 additions & 3 deletions manim_slides/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import hashlib
import json
import os
import shutil
import subprocess
Expand All @@ -7,7 +8,14 @@
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

Expand Down Expand Up @@ -71,6 +79,17 @@
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

Check warning on line 86 in manim_slides/config.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/config.py#L85-L86

Added lines #L85 - L86 were not covered by tests

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()
Expand Down Expand Up @@ -104,7 +123,7 @@
start_animation: int
end_animation: int
number: int
terminated: bool = False
terminated: bool = Field(False, exclude=True)

@field_validator("start_animation", "end_animation")
@classmethod
Expand Down Expand Up @@ -151,11 +170,31 @@


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))

Check warning on line 196 in manim_slides/config.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/config.py#L195-L196

Added lines #L195 - L196 were not covered by tests

@model_validator(mode="after")
def animation_indices_match_files(
cls, config: "PresentationConfig"
Expand Down
12 changes: 8 additions & 4 deletions manim_slides/present.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@

for filepath in folder.glob("*.json"):
try:
_ = PresentationConfig.parse_file(filepath)
_ = PresentationConfig.from_file(filepath)
scenes.append(filepath.stem)
except (
Exception
Expand Down Expand Up @@ -851,7 +851,7 @@
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))

Expand Down Expand Up @@ -1047,7 +1047,7 @@

if config_path.exists():
try:
config = Config.parse_file(config_path)
config = Config.from_file(config_path)

Check warning on line 1050 in manim_slides/present.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present.py#L1050

Added line #L1050 was not covered by tests
except ValidationError as e:
raise click.UsageError(str(e))
else:
Expand All @@ -1070,7 +1070,11 @@
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,
Expand Down
21 changes: 10 additions & 11 deletions manim_slides/wizard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import sys
from functools import partial
from pathlib import Path
from typing import Any

import click
Expand Down Expand Up @@ -102,7 +102,7 @@

def saveConfig(self) -> None:
try:
Config.parse_obj(self.config.dict())
Config.model_validate(self.config.dict())

Check warning on line 105 in manim_slides/wizard.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/wizard.py#L105

Added line #L105 was not covered by tests
except ValueError:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
Expand Down Expand Up @@ -130,7 +130,7 @@
@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)

Expand All @@ -140,18 +140,18 @@
@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:
Expand All @@ -175,8 +175,8 @@
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)

Check warning on line 179 in manim_slides/wizard.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/wizard.py#L178-L179

Added lines #L178 - L179 were not covered by tests

app = QApplication(sys.argv)
app.setApplicationName("Manim Slides Wizard")
Expand All @@ -187,9 +187,8 @@
config = window.config

if merge:
config = Config.parse_file(config_path).merge_with(config)
config = Config.from_file(config_path).merge_with(config)

Check warning on line 190 in manim_slides/wizard.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/wizard.py#L190

Added line #L190 was not covered by tests

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}`")
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
from pathlib import Path
from typing import Iterator

import pytest

from manim_slides.logger import make_logger

_ = make_logger() # This is run so that "PERF" level is created


@pytest.fixture
def folder_path() -> Iterator[Path]:
yield (Path(__file__).parent / "slides").resolve()
32 changes: 32 additions & 0 deletions tests/slides/BasicExample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"slides": [
{
"type": "slide",
"start_animation": 0,
"end_animation": 1,
"number": 1
},
{
"type": "loop",
"start_animation": 1,
"end_animation": 2,
"number": 2
},
{
"type": "last",
"start_animation": 2,
"end_animation": 3,
"number": 3
}
],
"files": [
"slides/files/BasicExample/1413466013_3346521118_223132457.mp4",
"slides/files/BasicExample/1672018281_3136302242_2191168284.mp4",
"slides/files/BasicExample/1672018281_1369283980_3942561600.mp4"
],
"resolution": [
1920,
1080
],
"background_color": "black"
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 4 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@ def test_validate(self, presentation_config: PresentationConfig) -> None:

def test_bump_to_json(self, presentation_config: PresentationConfig) -> None:
_ = presentation_config.model_dump_json(indent=2)

def test_empty_presentation_config(self) -> None:
with pytest.raises(ValidationError):
_ = PresentationConfig(slides=[], files=[])
93 changes: 93 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from pathlib import Path

from click.testing import CliRunner

from manim_slides.__main__ import cli


def test_help() -> None:
runner = CliRunner()
results = runner.invoke(cli, ["-S", "--help"])

assert results.exit_code == 0

results = runner.invoke(cli, ["-S", "-h"])

assert results.exit_code == 0


def test_defaults_to_present(folder_path: Path) -> None:
runner = CliRunner()

with runner.isolated_filesystem():
results = runner.invoke(
cli, ["BasicExample", "--folder", str(folder_path), "-s"]
)

assert results.exit_code == 0


def test_present(folder_path: Path) -> None:
runner = CliRunner()

with runner.isolated_filesystem():
results = runner.invoke(
cli, ["present", "BasicExample", "--folder", str(folder_path), "-s"]
)

assert results.exit_code == 0


def test_convert(folder_path: Path) -> None:
runner = CliRunner()

with runner.isolated_filesystem():
results = runner.invoke(
cli,
[
"convert",
"BasicExample",
"basic_example.html",
"--folder",
str(folder_path),
],
)

assert results.exit_code == 0


def test_init() -> None:
runner = CliRunner()

with runner.isolated_filesystem():
results = runner.invoke(
cli,
[
"init",
"--force",
],
)

assert results.exit_code == 0


def test_list_scenes(folder_path: Path) -> None:
runner = CliRunner()

with runner.isolated_filesystem():
results = runner.invoke(
cli,
[
"list-scenes",
"--folder",
str(folder_path),
],
)

assert results.exit_code == 0
assert "BasicExample" in results.output


def test_wizard() -> None:
# TODO
pass
Loading