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:
Jérome Eertmans
2023-10-18 19:24:14 +02:00
committed by GitHub
parent 498e33ad8d
commit 5daa94b823
20 changed files with 153 additions and 151 deletions

View File

@ -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'

View File

@ -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:

View File

@ -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(
{

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

@ -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
"""

View File

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

View File

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

View File

@ -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 *

View File

@ -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 *

View File

@ -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 *

View File

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

View File

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

View File

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

View File

@ -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"

View File

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

View File

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