mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-20 12:05:56 +08:00
chore(ci): enhance current lint rules (#289)
* chore(ci): enhance current lint rules * run linter * fix all warnings * fix(convert): release VideoCapture * fix(ci): properly close file * better tests * fix(ci): setup opengl * Revert "fix(ci): setup opengl" This reverts commit a33f53a1c04f909d7660f2b5221c763a9ef97d53. * fix(ci): skipif Windows in workflows * actually xfail
This commit is contained in:
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@ -17,6 +17,7 @@ jobs:
|
||||
MANIM_SLIDES_VERBOSITY: debug
|
||||
PYTHONFAULTHANDLER: 1
|
||||
DISPLAY: :99
|
||||
GITHUB_WORKFLOWS: 1
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@ -68,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Run pytest
|
||||
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
|
||||
run: poetry run pytest -x -n auto
|
||||
run: poetry run pytest
|
||||
|
||||
- name: Run pytest and coverage
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
|
||||
|
@ -6,11 +6,6 @@ repos:
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||
rev: v2.11.0
|
||||
hooks:
|
||||
@ -29,12 +24,6 @@ repos:
|
||||
- id: blacken-docs
|
||||
additional_dependencies:
|
||||
- black==23.9.1
|
||||
- repo: https://github.com/PyCQA/docformatter
|
||||
rev: v1.7.5
|
||||
hooks:
|
||||
- id: docformatter
|
||||
additional_dependencies: [tomli]
|
||||
args: [--in-place, --config, ./pyproject.toml]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.292
|
||||
hooks:
|
||||
|
@ -1,4 +1,3 @@
|
||||
# flake8: noqa: F401
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any, List
|
||||
@ -6,7 +5,7 @@ from typing import Any, List
|
||||
from .__version__ import __version__
|
||||
|
||||
|
||||
class module(ModuleType):
|
||||
class Module(ModuleType):
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name == "Slide" or name == "ThreeDSlide":
|
||||
module = __import__(
|
||||
@ -48,7 +47,7 @@ class module(ModuleType):
|
||||
|
||||
|
||||
old_module = sys.modules["manim_slides"]
|
||||
new_module = sys.modules["manim_slides"] = module("manim_slides")
|
||||
new_module = sys.modules["manim_slides"] = Module("manim_slides")
|
||||
|
||||
new_module.__dict__.update(
|
||||
{
|
||||
|
@ -12,7 +12,7 @@ Wrapper = Callable[[F], F]
|
||||
|
||||
|
||||
def config_path_option(function: F) -> F:
|
||||
"""Wraps a function to add configuration path option."""
|
||||
"""Wrap a function to add configuration path option."""
|
||||
wrapper: Wrapper = click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
@ -27,7 +27,7 @@ def config_path_option(function: F) -> F:
|
||||
|
||||
|
||||
def config_options(function: F) -> F:
|
||||
"""Wraps a function to add configuration options."""
|
||||
"""Wrap a function to add configuration options."""
|
||||
function = config_path_option(function)
|
||||
function = click.option(
|
||||
"-f", "--force", is_flag=True, help="Overwrite any existing configuration file."
|
||||
@ -42,7 +42,7 @@ def config_options(function: F) -> F:
|
||||
|
||||
|
||||
def verbosity_option(function: F) -> F:
|
||||
"""Wraps a function to add verbosity option."""
|
||||
"""Wrap a function to add verbosity option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: str) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
@ -69,7 +69,7 @@ def verbosity_option(function: F) -> F:
|
||||
|
||||
|
||||
def folder_path_option(function: F) -> F:
|
||||
"""Wraps a function to add folder path option."""
|
||||
"""Wrap a function to add folder path option."""
|
||||
wrapper: Wrapper = click.option(
|
||||
"--folder",
|
||||
metavar="DIRECTORY",
|
||||
|
@ -80,6 +80,7 @@ class Keys(BaseModel): # type: ignore[misc]
|
||||
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
|
||||
ids: Set[int] = set()
|
||||
|
||||
@ -121,14 +122,15 @@ class Config(BaseModel): # type: ignore[misc]
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> "Config":
|
||||
"""Reads a configuration from a file."""
|
||||
"""Read a configuration from a file."""
|
||||
return cls.model_validate(rtoml.load(path)) # type: ignore
|
||||
|
||||
def to_file(self, path: Path) -> None:
|
||||
"""Dumps the configuration to a file."""
|
||||
"""Dump the configuration to a file."""
|
||||
rtoml.dump(self.model_dump(), path, pretty=True)
|
||||
|
||||
def merge_with(self, other: "Config") -> "Config":
|
||||
"""Merge with another config."""
|
||||
self.keys = self.keys.merge_with(other.keys)
|
||||
return self
|
||||
|
||||
@ -146,6 +148,7 @@ class PreSlideConfig(BaseModel): # type: ignore
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
@classmethod
|
||||
def start_animation_is_before_end(
|
||||
cls, pre_slide_config: "PreSlideConfig"
|
||||
) -> "PreSlideConfig":
|
||||
@ -185,8 +188,8 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> "PresentationConfig":
|
||||
"""Reads a presentation configuration from a file."""
|
||||
with open(path, "r") as f:
|
||||
"""Read a presentation configuration from a file."""
|
||||
with open(path) as f:
|
||||
obj = json.load(f)
|
||||
|
||||
slides = obj.setdefault("slides", [])
|
||||
@ -202,7 +205,7 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||
return cls.model_validate(obj) # type: ignore
|
||||
|
||||
def to_file(self, path: Path) -> None:
|
||||
"""Dumps the presentation configuration to a file."""
|
||||
"""Dump the presentation configuration to a file."""
|
||||
with open(path, "w") as f:
|
||||
f.write(self.model_dump_json(indent=2))
|
||||
|
||||
|
@ -9,7 +9,7 @@ from base64 import b64encode
|
||||
from enum import Enum
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
||||
from typing import Any, Callable, ClassVar, Dict, List, Optional, Type, Union
|
||||
|
||||
import click
|
||||
import cv2
|
||||
@ -60,13 +60,13 @@ def validate_config_option(
|
||||
except ValueError:
|
||||
raise click.BadParameter(
|
||||
f"Configuration options `{c_option}` could not be parsed into a proper (key, value) pair. Please use an `=` sign to separate key from value."
|
||||
)
|
||||
) from None
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def file_to_data_uri(file: Path) -> str:
|
||||
"""Reads a video and returns the corresponding data-uri."""
|
||||
"""Read a video and return the corresponding data-uri."""
|
||||
b64 = b64encode(file.read_bytes()).decode("ascii")
|
||||
mime_type = mimetypes.guess_type(file)[0] or "video/mp4"
|
||||
|
||||
@ -79,24 +79,24 @@ class Converter(BaseModel): # type: ignore
|
||||
template: Optional[Path] = None
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Converts self, i.e., a list of presentations, into a given format."""
|
||||
"""Convert self, i.e., a list of presentations, into a given format."""
|
||||
raise NotImplementedError
|
||||
|
||||
def load_template(self) -> str:
|
||||
"""
|
||||
Returns the template as a string.
|
||||
Return the template as a string.
|
||||
|
||||
An empty string is returned if no template is used.
|
||||
"""
|
||||
return ""
|
||||
|
||||
def open(self, file: Path) -> Any:
|
||||
"""Opens a file, generated with converter, using appropriate application."""
|
||||
"""Open a file, generated with converter, using appropriate application."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, s: str) -> Type["Converter"]:
|
||||
"""Returns the appropriate converter from a string name."""
|
||||
"""Return the appropriate converter from a string name."""
|
||||
return {
|
||||
"html": RevealJS,
|
||||
"pdf": PDF,
|
||||
@ -117,7 +117,7 @@ class Str(str):
|
||||
return core_schema.str_schema()
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Ensures that the string is correctly quoted."""
|
||||
"""Ensure that the string is correctly quoted."""
|
||||
if self in ["true", "false", "null"]:
|
||||
return self
|
||||
else:
|
||||
@ -303,7 +303,7 @@ class RevealJS(Converter):
|
||||
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
|
||||
auto_animate_duration: float = 1.0
|
||||
auto_animate_unmatched: JsBool = JsBool.true
|
||||
auto_animate_styles: List[str] = [
|
||||
auto_animate_styles: ClassVar[List[str]] = [
|
||||
"opacity",
|
||||
"color",
|
||||
"background-color",
|
||||
@ -346,7 +346,7 @@ class RevealJS(Converter):
|
||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||
|
||||
def load_template(self) -> str:
|
||||
"""Returns the RevealJS HTML template as a string."""
|
||||
"""Return the RevealJS HTML template as a string."""
|
||||
if isinstance(self.template, Path):
|
||||
return self.template.read_text()
|
||||
|
||||
@ -359,8 +359,10 @@ class RevealJS(Converter):
|
||||
return webbrowser.open(file.absolute().as_uri())
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Converts this configuration into a RevealJS HTML presentation, saved to
|
||||
DEST."""
|
||||
"""
|
||||
Convert this configuration into a RevealJS HTML presentation, saved to
|
||||
DEST.
|
||||
"""
|
||||
if self.data_uri:
|
||||
assets_dir = Path("") # Actually we won't care.
|
||||
else:
|
||||
@ -409,7 +411,7 @@ class PDF(Converter):
|
||||
return open_with_default(file)
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Converts this configuration into a PDF presentation, saved to DEST."""
|
||||
"""Convert this configuration into a PDF presentation, saved to DEST."""
|
||||
|
||||
def read_image_from_video_file(file: Path, frame_index: FrameIndex) -> Image:
|
||||
cap = cv2.VideoCapture(str(file))
|
||||
@ -419,6 +421,7 @@ class PDF(Converter):
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, index - 1)
|
||||
|
||||
ret, frame = cap.read()
|
||||
cap.release()
|
||||
|
||||
if ret:
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
@ -462,7 +465,7 @@ class PowerPoint(Converter):
|
||||
return open_with_default(file)
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Converts this configuration into a PowerPoint presentation, saved to DEST."""
|
||||
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
|
||||
prs = pptx.Presentation()
|
||||
prs.slide_width = self.width * 9525
|
||||
prs.slide_height = self.height * 9525
|
||||
@ -493,10 +496,12 @@ class PowerPoint(Converter):
|
||||
def save_first_image_from_video_file(file: Path) -> Optional[str]:
|
||||
cap = cv2.VideoCapture(file.as_posix())
|
||||
ret, frame = cap.read()
|
||||
cap.release()
|
||||
|
||||
if ret:
|
||||
f = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".png")
|
||||
cv2.imwrite(f.name, frame)
|
||||
f.close()
|
||||
return f.name
|
||||
else:
|
||||
logger.warn("Failed to read first image from video file")
|
||||
@ -535,7 +540,7 @@ class PowerPoint(Converter):
|
||||
|
||||
|
||||
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Wraps a function to add a `--show-config` option."""
|
||||
"""Wrap a function to add a `--show-config` option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
@ -547,7 +552,7 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
presentation_configs=[PresentationConfig()]
|
||||
)
|
||||
for key, value in converter.dict().items():
|
||||
click.echo(f"{key}: {repr(value)}")
|
||||
click.echo(f"{key}: {value!r}")
|
||||
|
||||
ctx.exit()
|
||||
|
||||
@ -563,7 +568,7 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
|
||||
|
||||
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Wraps a function to add a `--show-template` option."""
|
||||
"""Wrap a function to add a `--show-template` option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
@ -637,7 +642,6 @@ def convert(
|
||||
template: Optional[Path],
|
||||
) -> None:
|
||||
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
||||
|
||||
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
||||
|
||||
try:
|
||||
@ -664,4 +668,4 @@ def convert(
|
||||
_msg = error["msg"]
|
||||
msg.append(f"Option '{option}': {_msg}")
|
||||
|
||||
raise click.UsageError("\n".join(msg))
|
||||
raise click.UsageError("\n".join(msg)) from None
|
||||
|
@ -114,7 +114,7 @@ directive:
|
||||
A list of methods, separated by spaces,
|
||||
that is rendered in a reference block after the source code.
|
||||
|
||||
"""
|
||||
""" # noqa: D400, D415
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
@ -123,7 +123,6 @@ import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from timeit import timeit
|
||||
from typing import Tuple
|
||||
|
||||
import jinja2
|
||||
from docutils import nodes
|
||||
@ -182,10 +181,11 @@ class ManimSlidesDirective(Directive):
|
||||
|
||||
See the module docstring for documentation.
|
||||
"""
|
||||
|
||||
has_content = True
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
option_spec = {
|
||||
option_spec = { # noqa: RUF012
|
||||
"hide_source": bool,
|
||||
"quality": lambda arg: directives.choice(
|
||||
arg,
|
||||
@ -198,7 +198,7 @@ class ManimSlidesDirective(Directive):
|
||||
}
|
||||
final_argument_whitespace = True
|
||||
|
||||
def run(self):
|
||||
def run(self): # noqa: C901
|
||||
# Rendering is skipped if the tag skip-manim is present,
|
||||
# or if we are making the pot-files
|
||||
should_skip = (
|
||||
@ -227,7 +227,7 @@ class ManimSlidesDirective(Directive):
|
||||
|
||||
global classnamedict
|
||||
|
||||
def split_file_cls(arg: str) -> Tuple[Path, str]:
|
||||
def split_file_cls(arg: str) -> tuple[Path, str]:
|
||||
if ":" in arg:
|
||||
file, cls = arg.split(":", maxsplit=1)
|
||||
_, file = self.state.document.settings.env.relfn2path(file)
|
||||
@ -314,7 +314,7 @@ class ManimSlidesDirective(Directive):
|
||||
|
||||
try:
|
||||
with tempconfig(example_config):
|
||||
print(f"Rendering {clsname}...")
|
||||
print(f"Rendering {clsname}...") # noqa: T201
|
||||
run_time = timeit(lambda: exec("\n".join(code), globals()), number=1)
|
||||
video_dir = config.get_dir("video_dir")
|
||||
except Exception as e:
|
||||
@ -375,7 +375,7 @@ def _log_rendering_times(*args):
|
||||
if len(data) == 0:
|
||||
sys.exit()
|
||||
|
||||
print("\nRendering Summary\n-----------------\n")
|
||||
print("\nRendering Summary\n-----------------\n") # noqa: T201
|
||||
|
||||
max_file_length = max(len(row[0]) for row in data)
|
||||
for key, group in it.groupby(data, key=lambda row: row[0]):
|
||||
@ -383,15 +383,17 @@ def _log_rendering_times(*args):
|
||||
group = list(group)
|
||||
if len(group) == 1:
|
||||
row = group[0]
|
||||
print(f"{key}{row[2].rjust(7, '.')}s {row[1]}")
|
||||
print(f"{key}{row[2].rjust(7, '.')}s {row[1]}") # noqa: T201
|
||||
continue
|
||||
time_sum = sum(float(row[2]) for row in group)
|
||||
print(
|
||||
print( # noqa: T201
|
||||
f"{key}{f'{time_sum:.3f}'.rjust(7, '.')}s => {len(group)} EXAMPLES",
|
||||
)
|
||||
for row in group:
|
||||
print(f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}")
|
||||
print("")
|
||||
print( # noqa: T201
|
||||
f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}"
|
||||
)
|
||||
print("") # noqa: T201
|
||||
|
||||
|
||||
def _delete_rendering_times(*args):
|
||||
|
@ -21,7 +21,7 @@ You can install them manually, or with the extra keyword:
|
||||
Note that you will still need to install Manim's platform-specific dependencies,
|
||||
see
|
||||
`their installation page <https://docs.manim.community/en/stable/installation.html>`_.
|
||||
"""
|
||||
""" # noqa: D400, D415
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -30,7 +30,7 @@ import mimetypes
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
|
||||
from IPython import get_ipython
|
||||
from IPython.core.interactiveshell import InteractiveShell
|
||||
@ -49,15 +49,15 @@ from ..present import get_scenes_presentation_config
|
||||
class ManimSlidesMagic(Magics): # type: ignore
|
||||
def __init__(self, shell: InteractiveShell) -> None:
|
||||
super().__init__(shell)
|
||||
self.rendered_files: Dict[Path, Path] = {}
|
||||
self.rendered_files: dict[Path, Path] = {}
|
||||
|
||||
@needs_local_scope
|
||||
@line_cell_magic
|
||||
def manim_slides(
|
||||
def manim_slides( # noqa: C901
|
||||
self,
|
||||
line: str,
|
||||
cell: Optional[str] = None,
|
||||
local_ns: Dict[str, Any] = {},
|
||||
cell: str | None = None,
|
||||
local_ns: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
r"""
|
||||
Render Manim Slides contained in IPython cells. Works as a line or cell magic.
|
||||
@ -118,7 +118,6 @@ class ManimSlidesMagic(Magics): # type: ignore
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
First make sure to put ``from manim_slides import ManimSlidesMagic``,
|
||||
or even ``from manim_slides import *``
|
||||
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
|
||||
@ -144,6 +143,8 @@ class ManimSlidesMagic(Magics): # type: ignore
|
||||
option should be set to ``None``. This can also be done by passing ``--progress_bar None`` as a
|
||||
CLI flag.
|
||||
"""
|
||||
if local_ns is None:
|
||||
local_ns = {}
|
||||
if cell:
|
||||
exec(cell, local_ns)
|
||||
|
||||
@ -173,8 +174,8 @@ class ManimSlidesMagic(Magics): # type: ignore
|
||||
renderer = OpenGLRenderer()
|
||||
|
||||
try:
|
||||
SceneClass = local_ns[config["scene_names"][0]]
|
||||
scene = SceneClass(renderer=renderer)
|
||||
scene_cls = local_ns[config["scene_names"][0]]
|
||||
scene = scene_cls(renderer=renderer)
|
||||
scene.render()
|
||||
finally:
|
||||
# Shader cache becomes invalid as the context is destroyed
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""
|
||||
Logger utils, mostly copied from Manim Community:
|
||||
Logger utils, mostly copied from Manim Community.
|
||||
|
||||
Source code:
|
||||
https://github.com/ManimCommunity/manim/blob/d5b65b844b8ce8ff5151a2f56f9dc98cebbc1db4/manim/_config/logger_utils.py#L29-L101
|
||||
"""
|
||||
|
||||
|
@ -26,13 +26,12 @@ ASPECT_RATIO_MODES = {
|
||||
@verbosity_option
|
||||
def list_scenes(folder: Path) -> None:
|
||||
"""List available scenes."""
|
||||
|
||||
for i, scene in enumerate(_list_scenes(folder), start=1):
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
|
||||
def _list_scenes(folder: Path) -> List[str]:
|
||||
"""Lists available scenes in given directory."""
|
||||
"""List available scenes in given directory."""
|
||||
scenes = []
|
||||
|
||||
for filepath in folder.glob("*.json"):
|
||||
@ -52,8 +51,7 @@ def _list_scenes(folder: Path) -> List[str]:
|
||||
|
||||
|
||||
def prompt_for_scenes(folder: Path) -> List[str]:
|
||||
"""Prompts the user to select scenes within a given folder."""
|
||||
|
||||
"""Prompt the user to select scenes within a given folder."""
|
||||
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
|
||||
|
||||
for i, scene in scene_choices.items():
|
||||
@ -82,14 +80,13 @@ def prompt_for_scenes(folder: Path) -> List[str]:
|
||||
scenes = click.prompt("Choice(s)", value_proc=value_proc)
|
||||
return scenes # type: ignore
|
||||
except ValueError as e:
|
||||
raise click.UsageError(str(e))
|
||||
raise click.UsageError(str(e)) from None
|
||||
|
||||
|
||||
def get_scenes_presentation_config(
|
||||
scenes: List[str], folder: Path
|
||||
) -> List[PresentationConfig]:
|
||||
"""Returns a list of presentation configurations based on the user input."""
|
||||
|
||||
"""Return a list of presentation configurations based on the user input."""
|
||||
if len(scenes) == 0:
|
||||
scenes = prompt_for_scenes(folder)
|
||||
|
||||
@ -103,7 +100,7 @@ def get_scenes_presentation_config(
|
||||
try:
|
||||
presentation_configs.append(PresentationConfig.from_file(config_file))
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e))
|
||||
raise click.UsageError(str(e)) from None
|
||||
|
||||
return presentation_configs
|
||||
|
||||
@ -125,7 +122,7 @@ def start_at_callback(
|
||||
f"start index can only be an integer or an empty string, not `{value}`",
|
||||
ctx=ctx,
|
||||
param=param,
|
||||
)
|
||||
) from None
|
||||
|
||||
values_tuple = values.split(",")
|
||||
n_values = len(values_tuple)
|
||||
@ -243,7 +240,6 @@ def present(
|
||||
Use ``manim-slide list-scenes`` to list all available
|
||||
scenes in a given folder.
|
||||
"""
|
||||
|
||||
if skip_all:
|
||||
exit_after_last_slide = True
|
||||
|
||||
@ -253,7 +249,7 @@ def present(
|
||||
try:
|
||||
config = Config.from_file(config_path)
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e))
|
||||
raise click.UsageError(str(e)) from None
|
||||
else:
|
||||
logger.debug("No configuration file found, using default configuration.")
|
||||
config = Config()
|
||||
|
@ -9,7 +9,7 @@ from PySide6.QtWidgets import QDialog, QGridLayout, QLabel, QMainWindow
|
||||
|
||||
from ..config import Config, PresentationConfig, SlideConfig
|
||||
from ..logger import logger
|
||||
from ..resources import * # noqa: F401, F403
|
||||
from ..resources import * # noqa: F403
|
||||
|
||||
WINDOW_NAME = "Manim Slides"
|
||||
|
||||
@ -337,10 +337,10 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
else:
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
||||
self.quit()
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
|
||||
key = event.key()
|
||||
self.dispatch(key)
|
||||
event.accept()
|
||||
|
@ -43,7 +43,6 @@ class Wipe(AnimationGroup): # type: ignore[misc]
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim-slides:: WipeClassExample
|
||||
|
||||
from manim import *
|
||||
@ -99,7 +98,6 @@ class Zoom(AnimationGroup): # type: ignore[misc]
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim-slides:: ZoomClassExample
|
||||
|
||||
from manim import *
|
||||
|
@ -19,6 +19,8 @@ if MANIM:
|
||||
else:
|
||||
Mobject = Any
|
||||
|
||||
LEFT: np.ndarray = np.array([-1.0, 0.0, 0.0])
|
||||
|
||||
|
||||
class BaseSlide:
|
||||
def __init__(
|
||||
@ -36,61 +38,61 @@ class BaseSlide:
|
||||
|
||||
@property
|
||||
def _ffmpeg_bin(self) -> Path:
|
||||
"""Returns the path to the ffmpeg binaries."""
|
||||
"""Return the path to the ffmpeg binaries."""
|
||||
return FFMPEG_BIN
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _frame_height(self) -> float:
|
||||
"""Returns the scene's frame height."""
|
||||
"""Return the scene's frame height."""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _frame_width(self) -> float:
|
||||
"""Returns the scene's frame width."""
|
||||
"""Return the scene's frame width."""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _background_color(self) -> str:
|
||||
"""Returns the scene's background color."""
|
||||
"""Return the scene's background color."""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _resolution(self) -> Tuple[int, int]:
|
||||
"""Returns the scene's resolution used during rendering."""
|
||||
"""Return the scene's resolution used during rendering."""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _partial_movie_files(self) -> List[Path]:
|
||||
"""Returns a list of partial movie files, a.k.a animations."""
|
||||
"""Return a list of partial movie files, a.k.a animations."""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _show_progress_bar(self) -> bool:
|
||||
"""Returns True if progress bar should be displayed."""
|
||||
"""Return True if progress bar should be displayed."""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _leave_progress_bar(self) -> bool:
|
||||
"""Returns True if progress bar should be left after completed."""
|
||||
"""Return True if progress bar should be left after completed."""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _start_at_animation_number(self) -> Optional[int]:
|
||||
"""If set, returns the animation number at which rendering start."""
|
||||
"""If set, return the animation number at which rendering start."""
|
||||
...
|
||||
|
||||
@property
|
||||
def canvas(self) -> MutableMapping[str, Mobject]:
|
||||
"""
|
||||
Returns the canvas associated to the current slide.
|
||||
Return the canvas associated to the current slide.
|
||||
|
||||
The canvas is a mapping between names and Mobjects,
|
||||
for objects that are assumed to stay in multiple slides.
|
||||
@ -99,7 +101,6 @@ class BaseSlide:
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim-slides:: CanvasExample
|
||||
|
||||
from manim import *
|
||||
@ -154,7 +155,7 @@ class BaseSlide:
|
||||
|
||||
def add_to_canvas(self, **objects: Mobject) -> None:
|
||||
"""
|
||||
Adds objects to the canvas, using key values as names.
|
||||
Add objects to the canvas, using key values as names.
|
||||
|
||||
:param objects: A mapping between names and Mobjects.
|
||||
|
||||
@ -168,19 +169,21 @@ class BaseSlide:
|
||||
self._canvas.update(objects)
|
||||
|
||||
def remove_from_canvas(self, *names: str) -> None:
|
||||
"""Removes objects from the canvas."""
|
||||
"""Remove objects from the canvas."""
|
||||
for name in names:
|
||||
self._canvas.pop(name)
|
||||
|
||||
@property
|
||||
def canvas_mobjects(self) -> ValuesView[Mobject]:
|
||||
"""Returns Mobjects contained in the canvas."""
|
||||
"""Return Mobjects contained in the canvas."""
|
||||
return self.canvas.values()
|
||||
|
||||
@property
|
||||
def mobjects_without_canvas(self) -> Sequence[Mobject]:
|
||||
"""Returns the list of objects contained in the scene, minus those present in
|
||||
the canvas."""
|
||||
"""
|
||||
Return the list of objects contained in the scene, minus those present in
|
||||
the canvas.
|
||||
"""
|
||||
return [
|
||||
mobject for mobject in self.mobjects if mobject not in self.canvas_mobjects # type: ignore[attr-defined]
|
||||
]
|
||||
@ -188,7 +191,7 @@ class BaseSlide:
|
||||
@property
|
||||
def wait_time_between_slides(self) -> float:
|
||||
r"""
|
||||
Returns the wait duration (in seconds) added between two slides.
|
||||
Return the wait duration (in seconds) added between two slides.
|
||||
|
||||
By default, this value is set to 0.
|
||||
|
||||
@ -203,7 +206,6 @@ class BaseSlide:
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim-slides:: WithoutWaitExample
|
||||
|
||||
from manim import *
|
||||
@ -246,13 +248,13 @@ class BaseSlide:
|
||||
self._wait_time_between_slides = max(wait_time, 0.0)
|
||||
|
||||
def play(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Overloads `self.play` and increment animation count."""
|
||||
"""Overload `self.play` and increment animation count."""
|
||||
super().play(*args, **kwargs) # type: ignore[misc]
|
||||
self._current_animation += 1
|
||||
|
||||
def next_slide(self) -> None:
|
||||
"""
|
||||
Creates a new slide with previous animations.
|
||||
Create a new slide with previous animations.
|
||||
|
||||
This usually means that the user will need to press some key before the
|
||||
next slide is played. By default, this is the right arrow key.
|
||||
@ -269,7 +271,6 @@ class BaseSlide:
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The following contains 3 slides:
|
||||
|
||||
#. the first with nothing on it;
|
||||
@ -307,8 +308,7 @@ class BaseSlide:
|
||||
self._pause_start_animation = self._current_animation
|
||||
|
||||
def _add_last_slide(self) -> None:
|
||||
"""Adds a 'last' slide to the end of slides."""
|
||||
|
||||
"""Add a 'last' slide to the end of slides."""
|
||||
if (
|
||||
len(self._slides) > 0
|
||||
and self._current_animation == self._slides[-1].end_animation
|
||||
@ -325,7 +325,7 @@ class BaseSlide:
|
||||
|
||||
def start_loop(self) -> None:
|
||||
"""
|
||||
Starts a loop. End it with :func:`end_loop`.
|
||||
Start a loop. End it with :func:`end_loop`.
|
||||
|
||||
A loop will automatically replay the slide, i.e., everything between
|
||||
:func:`start_loop` and :func:`end_loop`, upon reaching end.
|
||||
@ -342,7 +342,6 @@ class BaseSlide:
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The following contains one slide that will loop endlessly.
|
||||
|
||||
.. manim-slides:: LoopExample
|
||||
@ -370,7 +369,7 @@ class BaseSlide:
|
||||
|
||||
def end_loop(self) -> None:
|
||||
"""
|
||||
Ends an existing loop.
|
||||
End an existing loop.
|
||||
|
||||
See :func:`start_loop` for more details.
|
||||
"""
|
||||
@ -390,7 +389,7 @@ class BaseSlide:
|
||||
|
||||
def _save_slides(self, use_cache: bool = True) -> None:
|
||||
"""
|
||||
Saves slides, optionally using cached files.
|
||||
Save slides, optionally using cached files.
|
||||
|
||||
Note that cached files only work with Manim.
|
||||
"""
|
||||
@ -462,11 +461,11 @@ class BaseSlide:
|
||||
def wipe(
|
||||
self,
|
||||
*args: Any,
|
||||
direction: np.ndarray = np.array([-1.0, 0.0, 0.0]),
|
||||
direction: np.ndarray = LEFT,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Plays a wipe animation that will shift all the current objects outside of the
|
||||
Play a wipe animation that will shift all the current objects outside of the
|
||||
current scene's scope, and all the future objects inside.
|
||||
|
||||
:param args: Positional arguments passed to
|
||||
@ -477,7 +476,6 @@ class BaseSlide:
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim-slides:: WipeExample
|
||||
|
||||
from manim import *
|
||||
@ -522,7 +520,7 @@ class BaseSlide:
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Plays a zoom animation that will fade out all the current objects, and fade in
|
||||
Play a zoom animation that will fade out all the current objects, and fade in
|
||||
all the future objects. Objects are faded in a direction that goes towards the
|
||||
camera.
|
||||
|
||||
@ -533,7 +531,6 @@ class BaseSlide:
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim-slides:: ZoomExample
|
||||
|
||||
from manim import *
|
||||
|
@ -7,8 +7,10 @@ from .base import BaseSlide
|
||||
|
||||
|
||||
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
"""Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
|
||||
for slides rendering."""
|
||||
"""
|
||||
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
|
||||
for slides rendering.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _ffmpeg_bin(self) -> Path:
|
||||
@ -83,7 +85,6 @@ class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim-slides:: ThreeDExample
|
||||
|
||||
from manim import *
|
||||
|
@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Tuple
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Tuple
|
||||
|
||||
from manimlib import Scene, ThreeDCamera
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
@ -28,18 +28,14 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
|
||||
@property
|
||||
def _background_color(self) -> str:
|
||||
"""Returns the scene's background color."""
|
||||
return self.camera_config["background_color"].hex # type: ignore
|
||||
|
||||
@property
|
||||
def _resolution(self) -> Tuple[int, int]:
|
||||
"""Returns the scene's resolution used during rendering."""
|
||||
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
|
||||
|
||||
@property
|
||||
def _partial_movie_files(self) -> List[Path]:
|
||||
"""Returns a list of partial movie files, a.k.a animations."""
|
||||
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.file_writer.movie_file_extension,
|
||||
@ -70,7 +66,7 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
|
||||
|
||||
class ThreeDSlide(Slide):
|
||||
CONFIG = {
|
||||
CONFIG: ClassVar[Dict[str, Any]] = {
|
||||
"camera_class": ThreeDCamera,
|
||||
}
|
||||
pass
|
||||
|
@ -9,7 +9,6 @@ from .logger import logger
|
||||
|
||||
def concatenate_video_files(ffmpeg_bin: Path, files: List[Path], dest: Path) -> None:
|
||||
"""Concatenate multiple video files into one."""
|
||||
|
||||
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
||||
f.writelines(f"file '{path.absolute()}'\n" for path in files)
|
||||
f.close()
|
||||
|
@ -22,7 +22,7 @@ from .commons import config_options, verbosity_option
|
||||
from .config import Config, Key
|
||||
from .defaults import CONFIG_PATH
|
||||
from .logger import logger
|
||||
from .resources import * # noqa: F401, F403
|
||||
from .resources import * # noqa: F403
|
||||
|
||||
WINDOW_NAME: str = "Configuration Wizard"
|
||||
|
||||
@ -43,7 +43,7 @@ class KeyInput(QDialog): # type: ignore
|
||||
self.layout.addWidget(self.label)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
|
||||
self.key = event.key()
|
||||
self.deleteLater()
|
||||
event.accept()
|
||||
@ -58,11 +58,11 @@ class Wizard(QWidget): # type: ignore
|
||||
self.icon = QIcon(":/icon.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
|
||||
QBtn = QDialogButtonBox.Save | QDialogButtonBox.Cancel
|
||||
button = QDialogButtonBox.Save | QDialogButtonBox.Cancel
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.saveConfig)
|
||||
self.buttonBox.rejected.connect(self.closeWithoutSaving)
|
||||
self.buttonBox = QDialogButtonBox(button)
|
||||
self.buttonBox.accepted.connect(self.save_config)
|
||||
self.buttonBox.rejected.connect(self.close_without_saving)
|
||||
|
||||
self.buttons = []
|
||||
|
||||
@ -83,7 +83,7 @@ class Wizard(QWidget): # type: ignore
|
||||
)
|
||||
self.buttons.append(button)
|
||||
button.clicked.connect(
|
||||
partial(self.openDialog, i, getattr(self.config.keys, key))
|
||||
partial(self.open_dialog, i, getattr(self.config.keys, key))
|
||||
)
|
||||
self.layout.addWidget(button, i, 1)
|
||||
|
||||
@ -91,16 +91,16 @@ class Wizard(QWidget): # type: ignore
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def closeWithoutSaving(self) -> None:
|
||||
def close_without_saving(self) -> None:
|
||||
logger.debug("Closing configuration wizard without saving")
|
||||
self.deleteLater()
|
||||
sys.exit(0)
|
||||
|
||||
def closeEvent(self, event: Any) -> None:
|
||||
def closeEvent(self, event: Any) -> None: # noqa: N802
|
||||
self.closeWithoutSaving()
|
||||
event.accept()
|
||||
|
||||
def saveConfig(self) -> None:
|
||||
def save_config(self) -> None:
|
||||
try:
|
||||
Config.model_validate(self.config.dict())
|
||||
except ValueError:
|
||||
@ -116,7 +116,7 @@ class Wizard(QWidget): # type: ignore
|
||||
|
||||
self.deleteLater()
|
||||
|
||||
def openDialog(self, button_number: int, key: Key) -> None:
|
||||
def open_dialog(self, button_number: int, key: Key) -> None:
|
||||
button = self.buttons[button_number]
|
||||
dialog = KeyInput()
|
||||
dialog.exec_()
|
||||
@ -149,9 +149,10 @@ def init(
|
||||
def _init(
|
||||
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
|
||||
) -> None:
|
||||
"""Actual initialization code for configuration file, with optional interactive
|
||||
mode."""
|
||||
|
||||
"""
|
||||
Actual initialization code for configuration file, with optional interactive
|
||||
mode.
|
||||
"""
|
||||
if config_path.exists():
|
||||
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
||||
|
||||
|
@ -5,14 +5,6 @@ requires = ["setuptools", "poetry-core>=1.0.0"]
|
||||
[tool.black]
|
||||
target-version = ["py38"]
|
||||
|
||||
[tool.docformatter]
|
||||
black = true
|
||||
pre-summary-newline = true
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
py_version = 38
|
||||
|
||||
[tool.mypy]
|
||||
disallow_untyped_decorators = false
|
||||
install_types = true
|
||||
@ -129,7 +121,22 @@ filterwarnings = [
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
ignore = [
|
||||
extend-exclude = ["manim_slides/resources.py"]
|
||||
extend-ignore = [
|
||||
"D100",
|
||||
"D101",
|
||||
"D102",
|
||||
"D103",
|
||||
"D104",
|
||||
"D105",
|
||||
"D106",
|
||||
"D107",
|
||||
"D203",
|
||||
"D205",
|
||||
"D212",
|
||||
"E501"
|
||||
]
|
||||
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
|
||||
isort = {known-first-party = ['manim_slides', 'tests']}
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
|
@ -67,7 +67,7 @@ def test_format_enum(enum_type: EnumMeta) -> None:
|
||||
|
||||
assert expected == got
|
||||
|
||||
got = "{enum}".format(enum=enum)
|
||||
got = "{enum}".format(enum=enum) # noqa: UP032
|
||||
|
||||
assert expected == got
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
@ -43,7 +44,13 @@ cli = pytest.mark.parametrize(
|
||||
["cli"],
|
||||
[
|
||||
[manim_cli],
|
||||
[manimgl_cli],
|
||||
pytest.param(
|
||||
manimgl_cli,
|
||||
marks=pytest.mark.xfail(
|
||||
sys.platform == "win32" and os.environ.get("GITHUB_WORKFLOWS"),
|
||||
reason="OpenGL cannot be installed on Windows in GitHub workflows",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@ -89,7 +96,7 @@ def test_render_basic_slide(
|
||||
def assert_constructs(cls: type) -> type:
|
||||
class Wrapper:
|
||||
@classmethod
|
||||
def test_render(_) -> None:
|
||||
def test_render(_) -> None: # noqa: N804
|
||||
cls().construct()
|
||||
|
||||
return Wrapper
|
||||
@ -98,7 +105,7 @@ def assert_constructs(cls: type) -> type:
|
||||
def assert_renders(cls: type) -> type:
|
||||
class Wrapper:
|
||||
@classmethod
|
||||
def test_render(_) -> None:
|
||||
def test_render(_) -> None: # noqa: N804
|
||||
cls().render()
|
||||
|
||||
return Wrapper
|
||||
@ -118,7 +125,7 @@ class TestSlide:
|
||||
|
||||
@assert_renders
|
||||
class TestMultipleAnimationsInLastSlide(Slide):
|
||||
"""This is used to check against solution for issue #161."""
|
||||
"""Check against solution for issue #161."""
|
||||
|
||||
def construct(self) -> None:
|
||||
circle = Circle(color=BLUE)
|
||||
@ -135,7 +142,7 @@ class TestSlide:
|
||||
|
||||
@assert_renders
|
||||
class TestFileTooLong(Slide):
|
||||
"""This is used to check against solution for issue #123."""
|
||||
"""Check against solution for issue #123."""
|
||||
|
||||
def construct(self) -> None:
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
|
Reference in New Issue
Block a user