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

View File

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

View File

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

View File

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

View File

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