chore(lib): add missing type hints and mypy is happy! (#75)

* chore(lib): add missing type hints and mypy is happy!

Closes #34

* fix(ci): add missing dep for mypy
This commit is contained in:
Jérome Eertmans
2022-11-28 14:28:42 +01:00
committed by GitHub
parent 85ea9f3096
commit 2f0453c9a6
7 changed files with 115 additions and 93 deletions

View File

@ -29,12 +29,12 @@ repos:
rev: 'v0.991'
hooks:
- id: mypy
additional_dependencies: [types-setuptools]
args:
- --install-types
- --non-interactive
- --ignore-missing-imports
# Disallow dynamic typing
- --disallow-any-unimported
- --disallow-any-generics
- --disallow-subclassing-any
@ -49,7 +49,7 @@ repos:
# Configuring warnings
- --warn-unused-ignores
- --warn-no-return
- --warn-return-any
- --no-warn-return-any
- --warn-redundant-casts
# Strict equality

View File

@ -1,4 +1,4 @@
from typing import Callable
from typing import Any, Callable
import click
from click import Context, Parameter
@ -6,10 +6,13 @@ from click import Context, Parameter
from .defaults import CONFIG_PATH, FOLDER_PATH
from .manim import logger
F = Callable[..., Any]
Wrapper = Callable[[F], F]
def config_path_option(function: Callable) -> Callable:
def config_path_option(function: F) -> F:
"""Wraps a function to add configuration path option."""
return click.option(
wrapper: Wrapper = click.option(
"-c",
"--config",
"config_path",
@ -18,10 +21,11 @@ def config_path_option(function: Callable) -> Callable:
type=click.Path(dir_okay=False),
help="Set path to configuration file.",
show_default=True,
)(function)
)
return wrapper(function)
def config_options(function: Callable) -> Callable:
def config_options(function: F) -> F:
"""Wraps a function to add configuration options."""
function = config_path_option(function)
function = click.option(
@ -36,7 +40,7 @@ def config_options(function: Callable) -> Callable:
return function
def verbosity_option(function: Callable) -> Callable:
def verbosity_option(function: F) -> F:
"""Wraps a function to add verbosity option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
@ -46,7 +50,7 @@ def verbosity_option(function: Callable) -> Callable:
logger.setLevel(value)
return click.option(
wrapper: Wrapper = click.option(
"-v",
"--verbosity",
type=click.Choice(
@ -59,16 +63,20 @@ def verbosity_option(function: Callable) -> Callable:
envvar="MANIM_SLIDES_VERBOSITY",
show_envvar=True,
callback=callback,
)(function)
)
return wrapper(function)
def folder_path_option(function: Callable) -> Callable:
def folder_path_option(function: F) -> F:
"""Wraps a function to add folder path option."""
return click.option(
wrapper: Wrapper = click.option(
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
show_default=True,
)(function)
)
return wrapper(function)

View File

@ -3,7 +3,7 @@ import shutil
import subprocess
import tempfile
from enum import Enum
from typing import List, Optional, Set
from typing import Callable, Dict, List, Optional, Set, Union
from pydantic import BaseModel, root_validator, validator
from PySide6.QtCore import Qt
@ -24,7 +24,7 @@ def merge_basenames(files: List[str]) -> str:
return os.path.join(dirname, basename + ext)
class Key(BaseModel):
class Key(BaseModel): # type: ignore
"""Represents a list of key codes, with optionally a name."""
ids: Set[int]
@ -48,7 +48,7 @@ class Key(BaseModel):
return m
class Config(BaseModel):
class Config(BaseModel): # type: ignore
"""General Manim Slides config"""
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
@ -60,8 +60,8 @@ class Config(BaseModel):
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
@root_validator
def ids_are_unique_across_keys(cls, values):
ids = set()
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
ids: Set[int] = set()
for key in values.values():
if len(ids.intersection(key.ids)) != 0:
@ -87,7 +87,7 @@ class SlideType(str, Enum):
last = "last"
class SlideConfig(BaseModel):
class SlideConfig(BaseModel): # type: ignore
type: SlideType
start_animation: int
end_animation: int
@ -95,20 +95,22 @@ class SlideConfig(BaseModel):
terminated: bool = False
@validator("start_animation", "end_animation")
def index_is_posint(cls, v: int):
def index_is_posint(cls, v: int) -> int:
if v < 0:
raise ValueError("Animation index (start or end) cannot be negative")
return v
@validator("number")
def number_is_strictly_posint(cls, v: int):
def number_is_strictly_posint(cls, v: int) -> int:
if v <= 0:
raise ValueError("Slide number cannot be negative or zero")
return v
@root_validator
def start_animation_is_before_end(cls, values):
if values["start_animation"] >= values["end_animation"]:
def start_animation_is_before_end(
cls, values: Dict[str, Union[SlideType, int, bool]]
) -> Dict[str, Union[SlideType, int, bool]]:
if values["start_animation"] >= values["end_animation"]: # type: ignore
if values["start_animation"] == values["end_animation"] == 0:
raise ValueError(
@ -135,12 +137,12 @@ class SlideConfig(BaseModel):
return slice(self.start_animation, self.end_animation)
class PresentationConfig(BaseModel):
class PresentationConfig(BaseModel): # type: ignore
slides: List[SlideConfig]
files: List[str]
@validator("files", pre=True, each_item=True)
def is_file_and_exists(cls, v):
def is_file_and_exists(cls, v: str) -> str:
if not os.path.exists(v):
raise ValueError(
f"Animation file {v} does not exist. Are you in the right directory?"
@ -152,7 +154,9 @@ class PresentationConfig(BaseModel):
return v
@root_validator
def animation_indices_match_files(cls, values):
def animation_indices_match_files(
cls, values: Dict[str, Union[List[SlideConfig], List[str]]]
) -> Dict[str, Union[List[SlideConfig], List[str]]]:
files = values.get("files")
slides = values.get("slides")
@ -162,18 +166,20 @@ class PresentationConfig(BaseModel):
n_files = len(files)
for slide in slides:
if slide.end_animation > n_files:
if slide.end_animation > n_files: # type: ignore
raise ValueError(
f"The following slide's contains animations not listed in files {files}: {slide}"
)
return values
def move_to(self, dest: str, copy=True) -> "PresentationConfig":
def move_to(self, dest: str, copy: bool = True) -> "PresentationConfig":
"""
Moves (or copy) the files to a given directory.
"""
move = shutil.copy if copy else shutil.move
copy_func: Callable[[str, str], None] = shutil.copy
move_func: Callable[[str, str], None] = shutil.move
move = copy_func if copy else move_func
n = len(self.files)
for i in range(n):

View File

@ -31,11 +31,11 @@ def validate_config_option(
return config
class Converter(BaseModel):
class Converter(BaseModel): # type: ignore
presentation_configs: List[PresentationConfig] = []
assets_dir: str = "{basename}_assets"
def convert_to(self, dest: str):
def convert_to(self, dest: str) -> None:
"""Converts self, i.e., a list of presentations, into a given format."""
raise NotImplementedError
@ -105,7 +105,7 @@ class RevealJS(Converter):
__name__, "data/revealjs_template.html"
).decode()
def convert_to(self, dest: str):
def convert_to(self, dest: str) -> None:
"""Converts this configuration into a RevealJS HTML presentation, saved to DEST."""
dirname = os.path.dirname(dest)
basename, ext = os.path.splitext(os.path.basename(dest))
@ -130,7 +130,7 @@ class RevealJS(Converter):
f.write(content)
def show_config_options(function: Callable) -> Callable:
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wraps a function to add a `--show-config` option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
@ -191,7 +191,15 @@ def show_config_options(function: Callable) -> Callable:
)
@show_config_options
@verbosity_option
def convert(scenes, folder, dest, to, open_result, force, config_options):
def convert(
scenes: List[str],
folder: str,
dest: str,
to: str,
open_result: bool,
force: bool,
config_options: Dict[str, str],
) -> None:
"""
Convert SCENE(s) into a given format and writes the result in DEST.
"""

View File

@ -3,7 +3,7 @@ import platform
import sys
import time
from enum import IntEnum, auto, unique
from typing import List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union
import click
import cv2
@ -11,7 +11,7 @@ import numpy as np
from pydantic import ValidationError
from PySide6 import QtGui
from PySide6.QtCore import Qt, QThread, Signal, Slot
from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtGui import QCloseEvent, QIcon, QKeyEvent, QPixmap, QResizeEvent
from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
from tqdm import tqdm
@ -67,7 +67,7 @@ class Presentation:
self.current_slide_index: int = 0
self.current_animation: int = self.current_slide.start_animation
self.current_file: Optional[str] = None
self.current_file: str = ""
self.loaded_animation_cap: int = -1
self.cap = None # cap = cv2.VideoCapture
@ -222,7 +222,7 @@ class Presentation:
"""Returns current frame number."""
return int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
def update_state(self, state) -> Tuple[np.ndarray, State]:
def update_state(self, state: State) -> Tuple[np.ndarray, State]:
"""
Updates the current state given the previous one.
@ -236,7 +236,7 @@ class Presentation:
still_playing, frame = self.current_cap.read()
if still_playing:
self.lastframe = frame
elif state in [state.WAIT, state.PAUSED]:
elif state == state.WAIT or state == state.PAUSED: # type: ignore
return self.lastframe, state
elif self.current_slide.is_last() and self.current_slide.terminated:
return self.lastframe, State.END
@ -268,7 +268,7 @@ class Presentation:
return self.lastframe, state
class Display(QThread):
class Display(QThread): # type: ignore
"""Displays one or more presentations one after each other."""
change_video_signal = Signal(np.ndarray)
@ -277,12 +277,12 @@ class Display(QThread):
def __init__(
self,
presentations,
presentations: List[PresentationConfig],
config: Config = DEFAULT_CONFIG,
start_paused=False,
skip_all=False,
record_to=None,
exit_after_last_slide=False,
start_paused: bool = False,
skip_all: bool = False,
record_to: Optional[str] = None,
exit_after_last_slide: bool = False,
) -> None:
super().__init__()
self.presentations = presentations
@ -444,14 +444,14 @@ class Display(QThread):
self.key = -1 # No more key to be handled
def stop(self):
def stop(self) -> None:
"""Stops current thread, without doing anything after."""
self.run_flag = False
self.wait()
class Info(QWidget):
def __init__(self):
class Info(QWidget): # type: ignore
def __init__(self) -> None:
super().__init__()
self.setWindowTitle(WINDOW_INFO_NAME)
@ -474,7 +474,7 @@ class Info(QWidget):
self.update_info({})
@Slot(dict)
def update_info(self, info: dict):
def update_info(self, info: Dict[str, Union[str, int]]) -> None:
self.animationLabel.setText("Animation: {}".format(info.get("animation", "na")))
self.stateLabel.setText("State: {}".format(info.get("state", "unknown")))
self.slideLabel.setText(
@ -490,27 +490,27 @@ class Info(QWidget):
)
class InfoThread(QThread):
def __init__(self):
class InfoThread(QThread): # type: ignore
def __init__(self) -> None:
super().__init__()
self.dialog = Info()
self.run_flag = True
def start(self):
def start(self) -> None:
super().start()
self.dialog.show()
def stop(self):
def stop(self) -> None:
self.dialog.deleteLater()
class App(QWidget):
class App(QWidget): # type: ignore
send_key_signal = Signal(int)
def __init__(
self,
*args,
*args: Any,
config: Config = DEFAULT_CONFIG,
fullscreen: bool = False,
resolution: Tuple[int, int] = (1980, 1080),
@ -518,7 +518,7 @@ class App(QWidget):
aspect_ratio: Qt.AspectRatioMode = Qt.IgnoreAspectRatio,
resize_mode: Qt.TransformationMode = Qt.SmoothTransformation,
background_color: str = "black",
**kwargs,
**kwargs: Any,
):
super().__init__()
@ -543,7 +543,8 @@ class App(QWidget):
self.label.setMinimumSize(1, 1)
# create the video capture thread
self.thread = Display(*args, config=config, **kwargs)
kwargs["config"] = config
self.thread = Display(*args, **kwargs)
# create the info dialog
self.info = Info()
self.info.show()
@ -563,7 +564,7 @@ class App(QWidget):
# start the thread
self.thread.start()
def keyPressEvent(self, event):
def keyPressEvent(self, event: QKeyEvent) -> None:
key = event.key()
if self.config.HIDE_MOUSE.match(key):
@ -577,35 +578,30 @@ class App(QWidget):
self.send_key_signal.emit(key)
event.accept()
def closeAll(self):
def closeAll(self) -> None:
logger.debug("Closing all QT windows")
self.thread.stop()
self.info.deleteLater()
self.deleteLater()
def resizeEvent(self, event):
def resizeEvent(self, event: QResizeEvent) -> None:
self.pixmap = self.pixmap.scaled(
self.width(), self.height(), self.aspect_ratio, self.resize_mode
)
self.label.setPixmap(self.pixmap)
self.label.resize(self.width(), self.height())
def closeEvent(self, event):
def closeEvent(self, event: QCloseEvent) -> None:
self.closeAll()
event.accept()
@Slot(np.ndarray)
def update_image(self, cv_img: dict):
"""Updates the image_label with a new opencv image"""
def update_image(self, cv_img: np.ndarray) -> None:
"""Updates the (image) label with a new opencv image"""
self.pixmap = self.convert_cv_qt(cv_img)
self.label.setPixmap(self.pixmap)
@Slot(dict)
def update_info(self, info: dict):
"""Updates the image_label with a new opencv image"""
pass
def convert_cv_qt(self, cv_img):
def convert_cv_qt(self, cv_img: np.ndarray) -> np.ndarray:
"""Convert from an opencv image to QPixmap"""
rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
@ -633,14 +629,14 @@ class App(QWidget):
)
@click.help_option("-h", "--help")
@verbosity_option
def list_scenes(folder) -> None:
def list_scenes(folder: str) -> 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) -> List[str]:
def _list_scenes(folder: str) -> List[str]:
"""Lists available scenes in given directory."""
scenes = []
@ -792,19 +788,19 @@ def get_scenes_presentation_config(
@click.help_option("-h", "--help")
@verbosity_option
def present(
scenes,
config_path,
folder,
start_paused,
fullscreen,
skip_all,
resolution,
record_to,
exit_after_last_slide,
hide_mouse,
aspect_ratio,
resize_mode,
background_color,
scenes: List[str],
config_path: str,
folder: str,
start_paused: bool,
fullscreen: bool,
skip_all: bool,
resolution: Tuple[int, int],
record_to: Optional[str],
exit_after_last_slide: bool,
hide_mouse: bool,
aspect_ratio: str,
resize_mode: str,
background_color: str,
) -> None:
"""
Present SCENE(s), one at a time, in order.

View File

@ -25,7 +25,7 @@ def reverse_video_file(src: str, dst: str) -> None:
logger.debug(error.decode())
class Slide(Scene):
class Slide(Scene): # type:ignore
"""
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
"""
@ -222,7 +222,7 @@ class Slide(Scene):
self.save_slides()
class ThreeDSlide(Slide, ThreeDScene):
class ThreeDSlide(Slide, ThreeDScene): # type: ignore
"""
Inherits from `manim.ThreeDScene` or `manimlib.ThreeDScene` and provide necessary tools for slides rendering.

View File

@ -5,7 +5,7 @@ from typing import Any
import click
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
from PySide6.QtGui import QIcon, QKeyEvent
from PySide6.QtWidgets import (
QApplication,
QDialog,
@ -31,7 +31,7 @@ for key in Qt.Key:
keymap[key.value] = key.name.partition("_")[2]
class KeyInput(QDialog):
class KeyInput(QDialog): # type: ignore
def __init__(self) -> None:
super().__init__()
self.key = None
@ -43,13 +43,13 @@ class KeyInput(QDialog):
self.layout.addWidget(self.label)
self.setLayout(self.layout)
def keyPressEvent(self, event: Any) -> None:
def keyPressEvent(self, event: QKeyEvent) -> None:
self.key = event.key()
self.deleteLater()
event.accept()
class Wizard(QWidget):
class Wizard(QWidget): # type: ignore
def __init__(self, config: Config):
super().__init__()
@ -131,7 +131,7 @@ class Wizard(QWidget):
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path, force, merge):
def wizard(config_path: str, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@ -140,12 +140,16 @@ def wizard(config_path, force, merge):
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def init(config_path, force, merge, skip_interactive=False):
def init(
config_path: str, 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, force, merge, skip_interactive=False):
def _init(
config_path: str, 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):