Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add type hints to manim.cli module #3988

Merged
merged 12 commits into from
Nov 4, 2024
1 change: 1 addition & 0 deletions docs/source/reference_index/utilities_misc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Module Index
:toctree: ../reference

~utils.bezier
cli
~utils.color
~utils.commands
~utils.config_ops
Expand Down
17 changes: 17 additions & 0 deletions manim/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""The Manim CLI, and the available commands for ``manim``.

This page is a work in progress. Please run ``manim`` or ``manim --help`` in
your terminal to find more information on the following commands.

Available commands
------------------

.. autosummary::
:toctree: ../reference

cfg
checkhealth
init
plugins
render
"""
67 changes: 41 additions & 26 deletions manim/cli/cfg/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
import contextlib
from ast import literal_eval
from pathlib import Path
from typing import Any

import cloup
from rich.errors import StyleSyntaxError
from rich.style import Style

from ... import cli_ctx_settings, console
from ..._config.utils import config_file_paths, make_config_parser
from ...constants import EPILOG
from ...utils.file_ops import guarantee_existence, open_file
from manim._config import cli_ctx_settings, console
from manim._config.utils import config_file_paths, make_config_parser
from manim.constants import EPILOG
from manim.utils.file_ops import guarantee_existence, open_file

RICH_COLOUR_INSTRUCTIONS: str = """
[red]The default colour is used by the input statement.
Expand All @@ -41,57 +42,67 @@


def value_from_string(value: str) -> str | int | bool:
"""Extracts the literal of proper datatype from a string.
"""Extract the literal of proper datatype from a ``value`` string.

Parameters
----------
value
The value to check get the literal from.

Returns
-------
Union[:class:`str`, :class:`int`, :class:`bool`]
Returns the literal of appropriate datatype.
:class:`str` | :class:`int` | :class:`bool`
The literal of appropriate datatype.
"""
with contextlib.suppress(SyntaxError, ValueError):
value = literal_eval(value)
return value


def _is_expected_datatype(value: str, expected: str, style: bool = False) -> bool:
"""Checks whether `value` is the same datatype as `expected`,
and checks if it is a valid `style` if `style` is true.
def _is_expected_datatype(
value: str, expected: str, validate_style: bool = False
) -> bool:
"""Check whether the literal from ``value`` is the same datatype as the
literal from ``expected``. If ``validate_style`` is ``True``, also check if
the style given by ``value`` is valid, according to ``rich``.

Parameters
----------
value
The string of the value to check (obtained from reading the user input).
The string of the value to check, obtained from reading the user input.
expected
The string of the literal datatype must be matched by `value`. Obtained from
reading the cfg file.
style
Whether or not to confirm if `value` is a style, by default False
The string of the literal datatype which must be matched by ``value``.
This is obtained from reading the ``cfg`` file.
validate_style
Whether or not to confirm if ``value`` is a valid style, according to
``rich``. Default is ``False``.

Returns
-------
:class:`bool`
Whether or not `value` matches the datatype of `expected`.
Whether or not the literal from ``value`` matches the datatype of the
literal from ``expected``.
"""
value = value_from_string(value)
expected = type(value_from_string(expected))

return isinstance(value, expected) and (is_valid_style(value) if style else True)
return isinstance(value, expected) and (
is_valid_style(value) if validate_style else True
)


def is_valid_style(style: str) -> bool:
"""Checks whether the entered color is a valid color according to rich
"""Checks whether the entered color style is valid, according to ``rich``.

Parameters
----------
style
The style to check whether it is valid.

Returns
-------
Boolean
Returns whether it is valid style or not according to rich.
:class:`bool`
Whether the color style is valid or not, according to ``rich``.
"""
try:
Style.parse(style)
Expand All @@ -100,16 +111,20 @@ def is_valid_style(style: str) -> bool:
return False


def replace_keys(default: dict) -> dict:
"""Replaces _ to . and vice versa in a dictionary for rich
def replace_keys(default: dict[str, Any]) -> dict[str, Any]:
"""Replace ``_`` with ``.`` and vice versa in a dictionary's keys for
``rich``.

Parameters
----------
default
The dictionary to check and replace
The dictionary whose keys will be checked and replaced.

Returns
-------
:class:`dict`
The dictionary which is modified by replacing _ with . and vice versa
The dictionary whose keys are modified by replacing ``_`` with ``.``
and vice versa.
"""
for key in default:
if "_" in key:
Expand All @@ -133,7 +148,7 @@ def replace_keys(default: dict) -> dict:
help="Manages Manim configuration files.",
)
@cloup.pass_context
def cfg(ctx):
def cfg(ctx: cloup.Context):
"""Responsible for the cfg subcommand."""
pass

Expand Down Expand Up @@ -269,7 +284,7 @@ def show():
@cfg.command(context_settings=cli_ctx_settings)
@cloup.option("-d", "--directory", default=Path.cwd())
@cloup.pass_context
def export(ctx, directory):
def export(ctx: cloup.Context, directory: str) -> None:
JasonGrace2282 marked this conversation as resolved.
Show resolved Hide resolved
directory_path = Path(directory)
if directory_path.absolute == Path.cwd().absolute:
console.print(
Expand Down
114 changes: 83 additions & 31 deletions manim/cli/checkhealth/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,70 @@

import os
import shutil
from typing import Callable
from typing import Callable, Protocol

__all__ = ["HEALTH_CHECKS"]

HEALTH_CHECKS = []

class HealthCheckFunction(Protocol):
description: str
recommendation: str
skip_on_failed: list[str]
post_fail_fix_hook: Callable | None
JasonGrace2282 marked this conversation as resolved.
Show resolved Hide resolved

def __call__(self) -> bool: ...
Dismissed Show dismissed Hide dismissed


HEALTH_CHECKS: list[HealthCheckFunction] = []


def healthcheck(
description: str,
recommendation: str,
skip_on_failed: list[Callable | str] | None = None,
skip_on_failed: list[HealthCheckFunction | str] | None = None,
post_fail_fix_hook: Callable | None = None,
):
) -> Callable[Callable, HealthCheckFunction]:
"""Decorator used for declaring health checks.

This decorator attaches some data to a function,
which is then added to a list containing all checks.
This decorator attaches some data to a function, which is then added to a
a list containing all checks.

Parameters
----------
description
A brief description of this check, displayed when
the checkhealth subcommand is run.
A brief description of this check, displayed when the ``checkhealth``
subcommand is run.
recommendation
Help text which is displayed in case the check fails.
skip_on_failed
A list of check functions which, if they fail, cause
the current check to be skipped.
A list of check functions which, if they fail, cause the current check
to be skipped.
post_fail_fix_hook
A function that is supposed to (interactively) help
to fix the detected problem, if possible. This is
only called upon explicit confirmation of the user.
A function that is meant to (interactively) help to fix the detected
problem, if possible. This is only called upon explicit confirmation of
the user.

Returns
-------
A check function, as required by the checkhealth subcommand.
Callable[Callable, :class:`HealthCheckFunction`]
A decorator which converts a function into a health check function, as
required by the ``checkhealth`` subcommand.
"""
if skip_on_failed is None:
skip_on_failed = []
skip_on_failed = [
skip_on_failed: list[str] = [
skip.__name__ if callable(skip) else skip for skip in skip_on_failed
]

def decorator(func):
func.description = description
func.recommendation = recommendation
func.skip_on_failed = skip_on_failed
func.post_fail_fix_hook = post_fail_fix_hook
def wrapper(func: Callable) -> HealthCheckFunction:
JasonGrace2282 marked this conversation as resolved.
Show resolved Hide resolved
func.description: str = description
func.recommendation: str = recommendation
func.skip_on_failed: list[str] = skip_on_failed
func.post_fail_fix_hook: Callable | None = post_fail_fix_hook
HEALTH_CHECKS.append(func)
return func

return decorator
return wrapper


@healthcheck(
Expand All @@ -75,8 +87,15 @@
"PATH variable."
),
)
def is_manim_on_path():
path_to_manim = shutil.which("manim")
def is_manim_on_path() -> bool:
"""Check whether ``manim`` is in ``PATH``.

Returns
-------
:class:`bool`
Whether ``manim`` is in ``PATH`` or not.
"""
path_to_manim: str | None = shutil.which("manim")
return path_to_manim is not None


Expand All @@ -91,10 +110,29 @@
),
skip_on_failed=[is_manim_on_path],
)
def is_manim_executable_associated_to_this_library():
path_to_manim = shutil.which("manim")
with open(path_to_manim, "rb") as f:
manim_exec = f.read()
def is_manim_executable_associated_to_this_library() -> bool:
"""Check whether the ``manim`` executable in ``PATH`` is associated to this
library. To verify this, the executable should look like this:

.. code-block:: python

#!<MANIM_PATH>/.../python
import sys
from manim.__main__ import main

if __name__ == "__main__":
sys.exit(main())


Returns
-------
:class:`bool`
Whether the ``manim`` executable in ``PATH`` is associated to this
library or not.
"""
path_to_manim: str = shutil.which("manim")
with open(path_to_manim, "rb") as manim_binary:
manim_exec = manim_binary.read()

# first condition below corresponds to the executable being
# some sort of python script. second condition happens when
Expand All @@ -114,8 +152,15 @@
"LaTeX distribution on your operating system."
),
)
def is_latex_available():
path_to_latex = shutil.which("latex")
def is_latex_available() -> bool:
"""Check whether ``latex`` is in ``PATH`` and can be executed.

Returns
-------
:class:`bool`
Whether ``latex`` is in ``PATH`` and can be executed or not.
"""
path_to_latex: str | None = shutil.which("latex")
return path_to_latex is not None and os.access(path_to_latex, os.X_OK)


Expand All @@ -129,6 +174,13 @@
),
skip_on_failed=[is_latex_available],
)
def is_dvisvgm_available():
path_to_dvisvgm = shutil.which("dvisvgm")
def is_dvisvgm_available() -> bool:
"""Check whether ``dvisvgm`` is in ``PATH`` and can be executed.

Returns
-------
:class:`bool`
Whether ``dvisvgm`` is in ``PATH`` and can be executed or not.
"""
path_to_dvisvgm: str | None = shutil.which("dvisvgm")
JasonGrace2282 marked this conversation as resolved.
Show resolved Hide resolved
return path_to_dvisvgm is not None and os.access(path_to_dvisvgm, os.X_OK)
6 changes: 3 additions & 3 deletions manim/cli/checkhealth/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@
import click
import cloup

from manim.cli.checkhealth.checks import HEALTH_CHECKS
from manim.cli.checkhealth.checks import HEALTH_CHECKS, HealthCheckFunction

__all__ = ["checkhealth"]


@cloup.command(
context_settings=None,
)
def checkhealth():
def checkhealth() -> None:
"""This subcommand checks whether Manim is installed correctly
and has access to its required (and optional) system dependencies.
"""
click.echo(f"Python executable: {sys.executable}\n")
click.echo("Checking whether your installation of Manim Community is healthy...")
failed_checks = []
failed_checks: list[HealthCheckFunction] = []

for check in HEALTH_CHECKS:
click.echo(f"- {check.description} ... ", nl=False)
Expand Down
Loading
Loading