Skip to content

Commit efd6474

Browse files
Added new manim checkhealth CLI subcommand (#3299)
* added new checkhealth subcommand * basic checkhealth tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * check -> healthcheck * more helpful test output on checkhealth fail * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * callable -> Callable * fix executable check for windows * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fixed type * shutil already returns .exe in case it is there * debug commit ... * do proper debug commit for windows ... * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix failing test if executable is batch file * added more helpful (?) recommendations on failed tests * allow rendering ManimBanner from prerendered SVG path * improved test scene, actually test text / latex * added debug info on python executable * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove debug test again * move SCALE_FACTOR_PER_FONT_POINT to constants * access constants via module in logo.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * replaced other occurrence of 48 / 960 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 2c74f62 commit efd6474

File tree

8 files changed

+390
-24
lines changed

8 files changed

+390
-24
lines changed

manim/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from . import __version__, cli_ctx_settings, console
99
from .cli.cfg.group import cfg
10+
from .cli.checkhealth.commands import checkhealth
1011
from .cli.default_group import DefaultGroup
1112
from .cli.init.commands import init
1213
from .cli.new.group import new
@@ -48,6 +49,7 @@ def main(ctx):
4849
pass
4950

5051

52+
main.add_command(checkhealth)
5153
main.add_command(cfg)
5254
main.add_command(plugins)
5355
main.add_command(init)

manim/cli/checkhealth/__init__.py

Whitespace-only changes.

manim/cli/checkhealth/checks.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Auxiliary module for the checkhealth subcommand, contains
2+
the actual check implementations."""
3+
4+
from __future__ import annotations
5+
6+
import os
7+
import shutil
8+
import subprocess
9+
from typing import Callable
10+
11+
from ..._config import config
12+
13+
HEALTH_CHECKS = []
14+
15+
16+
def healthcheck(
17+
description: str,
18+
recommendation: str,
19+
skip_on_failed: list[Callable | str] | None = None,
20+
post_fail_fix_hook: Callable | None = None,
21+
):
22+
"""Decorator used for declaring health checks.
23+
24+
This decorator attaches some data to a function,
25+
which is then added to a list containing all checks.
26+
27+
Parameters
28+
----------
29+
description
30+
A brief description of this check, displayed when
31+
the checkhealth subcommand is run.
32+
recommendation
33+
Help text which is displayed in case the check fails.
34+
skip_on_failed
35+
A list of check functions which, if they fail, cause
36+
the current check to be skipped.
37+
post_fail_fix_hook
38+
A function that is supposed to (interactively) help
39+
to fix the detected problem, if possible. This is
40+
only called upon explicit confirmation of the user.
41+
42+
Returns
43+
-------
44+
A check function, as required by the checkhealth subcommand.
45+
"""
46+
if skip_on_failed is None:
47+
skip_on_failed = []
48+
skip_on_failed = [
49+
skip.__name__ if callable(skip) else skip for skip in skip_on_failed
50+
]
51+
52+
def decorator(func):
53+
func.description = description
54+
func.recommendation = recommendation
55+
func.skip_on_failed = skip_on_failed
56+
func.post_fail_fix_hook = post_fail_fix_hook
57+
HEALTH_CHECKS.append(func)
58+
return func
59+
60+
return decorator
61+
62+
63+
@healthcheck(
64+
description="Checking whether manim is on your PATH",
65+
recommendation=(
66+
"The command <manim> is currently not on your system's PATH.\n\n"
67+
"You can work around this by calling the manim module directly "
68+
"via <python -m manim> instead of just <manim>.\n\n"
69+
"To fix the PATH issue properly: "
70+
"Usually, the Python package installer pip issues a warning "
71+
"during the installation which contains more information. "
72+
"Consider reinstalling manim via <pip uninstall manim> "
73+
"followed by <pip install manim> to see the warning again, "
74+
"then consult the internet on how to modify your system's "
75+
"PATH variable."
76+
),
77+
)
78+
def is_manim_on_path():
79+
path_to_manim = shutil.which("manim")
80+
return path_to_manim is not None
81+
82+
83+
@healthcheck(
84+
description="Checking whether the executable belongs to manim",
85+
recommendation=(
86+
"The command <manim> does not belong to your installed version "
87+
"of this library, it likely belongs to manimgl / manimlib.\n\n"
88+
"Run manim via <python -m manim> or via <manimce>, or uninstall "
89+
"and reinstall manim via <pip install --upgrade "
90+
"--force-reinstall manim> to fix this."
91+
),
92+
skip_on_failed=[is_manim_on_path],
93+
)
94+
def is_manim_executable_associated_to_this_library():
95+
path_to_manim = shutil.which("manim")
96+
with open(path_to_manim, "rb") as f:
97+
manim_exec = f.read()
98+
99+
# first condition below corresponds to the executable being
100+
# some sort of python script. second condition happens when
101+
# the executable is actually a Windows batch file.
102+
return b"manim.__main__" in manim_exec or b'"%~dp0\\manim"' in manim_exec
103+
104+
105+
@healthcheck(
106+
description="Checking whether ffmpeg is available",
107+
recommendation=(
108+
"Manim does not work without ffmpeg. Please follow our "
109+
"installation instructions "
110+
"at https://docs.manim.community/en/stable/installation.html "
111+
"to download ffmpeg. Then, either ...\n\n"
112+
"(a) ... make the ffmpeg executable available to your system's PATH,\n"
113+
"(b) or, alternatively, use <manim cfg write --open> to create a "
114+
"custom configuration and set the ffmpeg_executable variable to the "
115+
"full absolute path to the ffmpeg executable."
116+
),
117+
)
118+
def is_ffmpeg_available():
119+
path_to_ffmpeg = shutil.which(config.ffmpeg_executable)
120+
return path_to_ffmpeg is not None and os.access(path_to_ffmpeg, os.X_OK)
121+
122+
123+
@healthcheck(
124+
description="Checking whether ffmpeg is working",
125+
recommendation=(
126+
"Your installed version of ffmpeg does not support x264 encoding, "
127+
"which manim requires. Please follow our installation instructions "
128+
"at https://docs.manim.community/en/stable/installation.html "
129+
"to download and install a newer version of ffmpeg."
130+
),
131+
skip_on_failed=[is_ffmpeg_available],
132+
)
133+
def is_ffmpeg_working():
134+
ffmpeg_version = subprocess.run(
135+
[config.ffmpeg_executable, "-version"],
136+
stdout=subprocess.PIPE,
137+
).stdout.decode()
138+
return (
139+
ffmpeg_version.startswith("ffmpeg version")
140+
and "--enable-libx264" in ffmpeg_version
141+
)
142+
143+
144+
@healthcheck(
145+
description="Checking whether latex is available",
146+
recommendation=(
147+
"Manim cannot find <latex> on your system's PATH. "
148+
"You will not be able to use Tex and MathTex mobjects "
149+
"in your scenes.\n\n"
150+
"Consult our installation instructions "
151+
"at https://docs.manim.community/en/stable/installation.html "
152+
"or search the web for instructions on how to install a "
153+
"LaTeX distribution on your operating system."
154+
),
155+
)
156+
def is_latex_available():
157+
path_to_latex = shutil.which("latex")
158+
return path_to_latex is not None and os.access(path_to_latex, os.X_OK)
159+
160+
161+
@healthcheck(
162+
description="Checking whether dvisvgm is available",
163+
recommendation=(
164+
"Manim could find <latex>, but not <dvisvgm> on your system's "
165+
"PATH. Make sure your installed LaTeX distribution comes with "
166+
"dvisvgm and consider installing a larger distribution if it "
167+
"does not."
168+
),
169+
skip_on_failed=[is_latex_available],
170+
)
171+
def is_dvisvgm_available():
172+
path_to_dvisvgm = shutil.which("dvisvgm")
173+
return path_to_dvisvgm is not None and os.access(path_to_dvisvgm, os.X_OK)

manim/cli/checkhealth/commands.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""A CLI utility helping to diagnose problems with
2+
your Manim installation.
3+
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import sys
9+
10+
import click
11+
import cloup
12+
13+
from .checks import HEALTH_CHECKS
14+
15+
16+
@cloup.command(
17+
context_settings=None,
18+
)
19+
def checkhealth():
20+
"""This subcommand checks whether Manim is installed correctly
21+
and has access to its required (and optional) system dependencies.
22+
"""
23+
click.echo(f"Python executable: {sys.executable}\n")
24+
click.echo("Checking whether your installation of Manim Community is healthy...")
25+
failed_checks = []
26+
27+
for check in HEALTH_CHECKS:
28+
click.echo(f"- {check.description} ... ", nl=False)
29+
if any(
30+
failed_check.__name__ in check.skip_on_failed
31+
for failed_check in failed_checks
32+
):
33+
click.secho("SKIPPED", fg="blue")
34+
continue
35+
check_result = check()
36+
if check_result:
37+
click.secho("PASSED", fg="green")
38+
else:
39+
click.secho("FAILED", fg="red")
40+
failed_checks.append(check)
41+
42+
click.echo()
43+
44+
if failed_checks:
45+
click.echo(
46+
"There are problems with your installation, "
47+
"here are some recommendations to fix them:"
48+
)
49+
for ind, failed_check in enumerate(failed_checks):
50+
click.echo(failed_check.recommendation)
51+
if ind + 1 < len(failed_checks):
52+
click.confirm("Continue with next recommendation?")
53+
54+
else: # no problems detected!
55+
click.echo("No problems detected, your installation seems healthy!")
56+
render_test_scene = click.confirm(
57+
"Would you like to render and preview a test scene?"
58+
)
59+
if render_test_scene:
60+
import manim as mn
61+
62+
class CheckHealthDemo(mn.Scene):
63+
def construct(self):
64+
banner = mn.ManimBanner().shift(mn.UP * 0.5)
65+
self.play(banner.create())
66+
self.wait(0.5)
67+
self.play(banner.expand())
68+
self.wait(0.5)
69+
text_left = mn.Text("All systems operational!")
70+
formula_right = mn.MathTex(r"\oint_{\gamma} f(z)~dz = 0")
71+
text_tex_group = mn.VGroup(text_left, formula_right)
72+
text_tex_group.arrange(mn.RIGHT, buff=1).next_to(banner, mn.DOWN)
73+
self.play(mn.Write(text_tex_group))
74+
self.wait(0.5)
75+
self.play(
76+
mn.FadeOut(banner, shift=mn.UP),
77+
mn.FadeOut(text_tex_group, shift=mn.DOWN),
78+
)
79+
80+
with mn.tempconfig({"preview": True, "disable_caching": True}):
81+
CheckHealthDemo().render()

manim/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"DEFAULT_POINT_DENSITY_1D",
6363
"DEFAULT_STROKE_WIDTH",
6464
"DEFAULT_FONT_SIZE",
65+
"SCALE_FACTOR_PER_FONT_POINT",
6566
"PI",
6667
"TAU",
6768
"DEGREES",
@@ -183,6 +184,7 @@
183184
DEFAULT_POINT_DENSITY_1D: int = 10
184185
DEFAULT_STROKE_WIDTH: int = 4
185186
DEFAULT_FONT_SIZE: float = 48
187+
SCALE_FACTOR_PER_FONT_POINT: float = 1 / 960
186188

187189
# Mathematical constants
188190
PI: float = np.pi

0 commit comments

Comments
 (0)