manim 0.18.0.post0__py3-none-any.whl → 0.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of manim might be problematic. Click here for more details.

Files changed (146) hide show
  1. manim/__init__.py +3 -6
  2. manim/__main__.py +61 -20
  3. manim/_config/__init__.py +6 -3
  4. manim/_config/cli_colors.py +16 -8
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +14 -8
  7. manim/_config/utils.py +651 -472
  8. manim/animation/animation.py +152 -5
  9. manim/animation/composition.py +80 -39
  10. manim/animation/creation.py +196 -14
  11. manim/animation/fading.py +5 -9
  12. manim/animation/indication.py +103 -47
  13. manim/animation/movement.py +22 -5
  14. manim/animation/rotation.py +3 -2
  15. manim/animation/specialized.py +4 -6
  16. manim/animation/speedmodifier.py +10 -5
  17. manim/animation/transform.py +4 -5
  18. manim/animation/transform_matching_parts.py +1 -1
  19. manim/animation/updaters/mobject_update_utils.py +17 -14
  20. manim/camera/camera.py +15 -6
  21. manim/cli/__init__.py +17 -0
  22. manim/cli/cfg/group.py +70 -44
  23. manim/cli/checkhealth/checks.py +93 -75
  24. manim/cli/checkhealth/commands.py +14 -5
  25. manim/cli/default_group.py +157 -25
  26. manim/cli/init/commands.py +32 -24
  27. manim/cli/plugins/commands.py +16 -3
  28. manim/cli/render/commands.py +72 -60
  29. manim/cli/render/ease_of_access_options.py +4 -3
  30. manim/cli/render/global_options.py +51 -15
  31. manim/cli/render/output_options.py +6 -5
  32. manim/cli/render/render_options.py +97 -32
  33. manim/constants.py +65 -19
  34. manim/gui/gui.py +2 -0
  35. manim/mobject/frame.py +0 -1
  36. manim/mobject/geometry/arc.py +112 -78
  37. manim/mobject/geometry/boolean_ops.py +32 -25
  38. manim/mobject/geometry/labeled.py +300 -77
  39. manim/mobject/geometry/line.py +132 -64
  40. manim/mobject/geometry/polygram.py +126 -30
  41. manim/mobject/geometry/shape_matchers.py +35 -15
  42. manim/mobject/geometry/tips.py +38 -29
  43. manim/mobject/graph.py +414 -133
  44. manim/mobject/graphing/coordinate_systems.py +126 -64
  45. manim/mobject/graphing/functions.py +25 -15
  46. manim/mobject/graphing/number_line.py +24 -10
  47. manim/mobject/graphing/probability.py +2 -10
  48. manim/mobject/graphing/scale.py +6 -5
  49. manim/mobject/matrix.py +17 -19
  50. manim/mobject/mobject.py +314 -165
  51. manim/mobject/opengl/opengl_compatibility.py +2 -0
  52. manim/mobject/opengl/opengl_geometry.py +30 -9
  53. manim/mobject/opengl/opengl_image_mobject.py +2 -0
  54. manim/mobject/opengl/opengl_mobject.py +509 -343
  55. manim/mobject/opengl/opengl_point_cloud_mobject.py +5 -7
  56. manim/mobject/opengl/opengl_surface.py +3 -2
  57. manim/mobject/opengl/opengl_three_dimensions.py +2 -0
  58. manim/mobject/opengl/opengl_vectorized_mobject.py +46 -79
  59. manim/mobject/svg/brace.py +63 -13
  60. manim/mobject/svg/svg_mobject.py +4 -3
  61. manim/mobject/table.py +11 -13
  62. manim/mobject/text/code_mobject.py +186 -548
  63. manim/mobject/text/numbers.py +9 -7
  64. manim/mobject/text/tex_mobject.py +23 -14
  65. manim/mobject/text/text_mobject.py +70 -24
  66. manim/mobject/three_d/polyhedra.py +98 -1
  67. manim/mobject/three_d/three_d_utils.py +4 -4
  68. manim/mobject/three_d/three_dimensions.py +62 -34
  69. manim/mobject/types/image_mobject.py +42 -24
  70. manim/mobject/types/point_cloud_mobject.py +105 -67
  71. manim/mobject/types/vectorized_mobject.py +496 -228
  72. manim/mobject/value_tracker.py +5 -4
  73. manim/mobject/vector_field.py +5 -5
  74. manim/opengl/__init__.py +3 -3
  75. manim/plugins/__init__.py +14 -1
  76. manim/plugins/plugins_flags.py +14 -8
  77. manim/renderer/cairo_renderer.py +20 -10
  78. manim/renderer/opengl_renderer.py +21 -23
  79. manim/renderer/opengl_renderer_window.py +2 -0
  80. manim/renderer/shader.py +2 -3
  81. manim/renderer/shader_wrapper.py +5 -2
  82. manim/renderer/vectorized_mobject_rendering.py +5 -0
  83. manim/scene/moving_camera_scene.py +23 -0
  84. manim/scene/scene.py +90 -43
  85. manim/scene/scene_file_writer.py +316 -165
  86. manim/scene/section.py +17 -15
  87. manim/scene/three_d_scene.py +13 -21
  88. manim/scene/vector_space_scene.py +22 -9
  89. manim/typing.py +830 -70
  90. manim/utils/bezier.py +1667 -399
  91. manim/utils/caching.py +13 -5
  92. manim/utils/color/AS2700.py +2 -0
  93. manim/utils/color/BS381.py +3 -0
  94. manim/utils/color/DVIPSNAMES.py +96 -0
  95. manim/utils/color/SVGNAMES.py +179 -0
  96. manim/utils/color/X11.py +3 -0
  97. manim/utils/color/XKCD.py +3 -0
  98. manim/utils/color/__init__.py +8 -5
  99. manim/utils/color/core.py +844 -309
  100. manim/utils/color/manim_colors.py +7 -9
  101. manim/utils/commands.py +48 -20
  102. manim/utils/config_ops.py +18 -13
  103. manim/utils/debug.py +8 -7
  104. manim/utils/deprecation.py +90 -40
  105. manim/utils/docbuild/__init__.py +17 -0
  106. manim/utils/docbuild/autoaliasattr_directive.py +234 -0
  107. manim/utils/docbuild/autocolor_directive.py +21 -17
  108. manim/utils/docbuild/manim_directive.py +50 -35
  109. manim/utils/docbuild/module_parsing.py +245 -0
  110. manim/utils/exceptions.py +6 -0
  111. manim/utils/family.py +5 -3
  112. manim/utils/family_ops.py +17 -4
  113. manim/utils/file_ops.py +26 -16
  114. manim/utils/hashing.py +9 -7
  115. manim/utils/images.py +10 -4
  116. manim/utils/ipython_magic.py +14 -8
  117. manim/utils/iterables.py +161 -119
  118. manim/utils/module_ops.py +57 -19
  119. manim/utils/opengl.py +83 -24
  120. manim/utils/parameter_parsing.py +32 -0
  121. manim/utils/paths.py +21 -23
  122. manim/utils/polylabel.py +168 -0
  123. manim/utils/qhull.py +218 -0
  124. manim/utils/rate_functions.py +74 -39
  125. manim/utils/simple_functions.py +24 -15
  126. manim/utils/sounds.py +7 -1
  127. manim/utils/space_ops.py +125 -69
  128. manim/utils/testing/__init__.py +17 -0
  129. manim/utils/testing/_frames_testers.py +13 -8
  130. manim/utils/testing/_show_diff.py +5 -3
  131. manim/utils/testing/_test_class_makers.py +33 -18
  132. manim/utils/testing/frames_comparison.py +27 -19
  133. manim/utils/tex.py +127 -197
  134. manim/utils/tex_file_writing.py +47 -45
  135. manim/utils/tex_templates.py +2 -1
  136. manim/utils/unit.py +6 -5
  137. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE.community +1 -1
  138. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/METADATA +40 -39
  139. manim-0.19.0.dist-info/RECORD +221 -0
  140. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
  141. manim/cli/new/__init__.py +0 -0
  142. manim/cli/new/group.py +0 -189
  143. manim/plugins/import_plugins.py +0 -43
  144. manim-0.18.0.post0.dist-info/RECORD +0 -217
  145. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
  146. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
manim/camera/camera.py CHANGED
@@ -1,6 +1,5 @@
1
1
  """A camera converts the mobjects contained in a Scene into an array of pixels."""
2
2
 
3
-
4
3
  from __future__ import annotations
5
4
 
6
5
  __all__ = ["Camera", "BackgroundColoredVMobjectDisplayer"]
@@ -9,8 +8,9 @@ import copy
9
8
  import itertools as it
10
9
  import operator as op
11
10
  import pathlib
11
+ from collections.abc import Iterable
12
12
  from functools import reduce
13
- from typing import Any, Callable, Iterable
13
+ from typing import Any, Callable
14
14
 
15
15
  import cairo
16
16
  import numpy as np
@@ -37,6 +37,14 @@ LINE_JOIN_MAP = {
37
37
  }
38
38
 
39
39
 
40
+ CAP_STYLE_MAP = {
41
+ CapStyleType.AUTO: None, # TODO: this could be improved
42
+ CapStyleType.ROUND: cairo.LineCap.ROUND,
43
+ CapStyleType.BUTT: cairo.LineCap.BUTT,
44
+ CapStyleType.SQUARE: cairo.LineCap.SQUARE,
45
+ }
46
+
47
+
40
48
  class Camera:
41
49
  """Base camera class.
42
50
 
@@ -369,7 +377,6 @@ class Camera:
369
377
  np.array
370
378
  The pixel array which can then be passed to set_background.
371
379
  """
372
-
373
380
  logger.info("Starting set_background")
374
381
  coords = self.get_coords_of_all_pixels()
375
382
  new_background = np.apply_along_axis(coords_to_colors_func, 2, coords)
@@ -778,11 +785,13 @@ class Camera:
778
785
  ctx.set_line_width(
779
786
  width
780
787
  * self.cairo_line_width_multiple
781
- # This ensures lines have constant width as you zoom in on them.
782
788
  * (self.frame_width / self.frame_width),
789
+ # This ensures lines have constant width as you zoom in on them.
783
790
  )
784
791
  if vmobject.joint_type != LineJointType.AUTO:
785
792
  ctx.set_line_join(LINE_JOIN_MAP[vmobject.joint_type])
793
+ if vmobject.cap_style != CapStyleType.AUTO:
794
+ ctx.set_line_cap(CAP_STYLE_MAP[vmobject.cap_style])
786
795
  ctx.stroke_preserve()
787
796
  return self
788
797
 
@@ -973,8 +982,8 @@ class Camera:
973
982
  sub_image = Image.fromarray(image_mobject.get_pixel_array(), mode="RGBA")
974
983
 
975
984
  # Reshape
976
- pixel_width = max(int(pdist([ul_coords, ur_coords])), 1)
977
- pixel_height = max(int(pdist([ul_coords, dl_coords])), 1)
985
+ pixel_width = max(int(pdist([ul_coords, ur_coords]).item()), 1)
986
+ pixel_height = max(int(pdist([ul_coords, dl_coords]).item()), 1)
978
987
  sub_image = sub_image.resize(
979
988
  (pixel_width, pixel_height),
980
989
  resample=image_mobject.resampling_algorithm,
manim/cli/__init__.py CHANGED
@@ -0,0 +1,17 @@
1
+ """The Manim CLI, and the available commands for ``manim``.
2
+
3
+ This page is a work in progress. Please run ``manim`` or ``manim --help`` in
4
+ your terminal to find more information on the following commands.
5
+
6
+ Available commands
7
+ ------------------
8
+
9
+ .. autosummary::
10
+ :toctree: ../reference
11
+
12
+ cfg
13
+ checkhealth
14
+ init
15
+ plugins
16
+ render
17
+ """
manim/cli/cfg/group.py CHANGED
@@ -5,32 +5,45 @@ cfg``. Here you can specify options, subcommands, and subgroups for the cfg
5
5
  group.
6
6
 
7
7
  """
8
+
8
9
  from __future__ import annotations
9
10
 
10
- import os
11
+ import contextlib
11
12
  from ast import literal_eval
12
13
  from pathlib import Path
14
+ from typing import Any, cast
13
15
 
14
- import click
15
16
  import cloup
16
17
  from rich.errors import StyleSyntaxError
17
18
  from rich.style import Style
18
19
 
19
- from ... import cli_ctx_settings, console
20
- from ..._config.utils import config_file_paths, make_config_parser
21
- from ...constants import EPILOG
22
- from ...utils.file_ops import guarantee_existence, open_file
20
+ from manim._config import cli_ctx_settings, console
21
+ from manim._config.utils import config_file_paths, make_config_parser
22
+ from manim.constants import EPILOG
23
+ from manim.utils.file_ops import guarantee_existence, open_file
23
24
 
24
25
  RICH_COLOUR_INSTRUCTIONS: str = """
25
26
  [red]The default colour is used by the input statement.
26
27
  If left empty, the default colour will be used.[/red]
27
28
  [magenta] For a full list of styles, visit[/magenta] [green]https://rich.readthedocs.io/en/latest/style.html[/green]
28
29
  """
29
- RICH_NON_STYLE_ENTRIES: str = ["log.width", "log.height", "log.timestamps"]
30
+ RICH_NON_STYLE_ENTRIES: list[str] = ["log.width", "log.height", "log.timestamps"]
31
+
32
+ __all__ = [
33
+ "value_from_string",
34
+ "value_from_string",
35
+ "is_valid_style",
36
+ "replace_keys",
37
+ "cfg",
38
+ "write",
39
+ "show",
40
+ "export",
41
+ ]
30
42
 
31
43
 
32
44
  def value_from_string(value: str) -> str | int | bool:
33
- """Extracts the literal of proper datatype from a string.
45
+ """Extract the literal of proper datatype from a ``value`` string.
46
+
34
47
  Parameters
35
48
  ----------
36
49
  value
@@ -38,51 +51,60 @@ def value_from_string(value: str) -> str | int | bool:
38
51
 
39
52
  Returns
40
53
  -------
41
- Union[:class:`str`, :class:`int`, :class:`bool`]
42
- Returns the literal of appropriate datatype.
54
+ :class:`str` | :class:`int` | :class:`bool`
55
+ The literal of appropriate datatype.
43
56
  """
44
- try:
57
+ with contextlib.suppress(SyntaxError, ValueError):
45
58
  value = literal_eval(value)
46
- except (SyntaxError, ValueError):
47
- pass
48
59
  return value
49
60
 
50
61
 
51
- def _is_expected_datatype(value: str, expected: str, style: bool = False) -> bool:
52
- """Checks whether `value` is the same datatype as `expected`,
53
- and checks if it is a valid `style` if `style` is true.
62
+ def _is_expected_datatype(
63
+ value: str, expected: str, validate_style: bool = False
64
+ ) -> bool:
65
+ """Check whether the literal from ``value`` is the same datatype as the
66
+ literal from ``expected``. If ``validate_style`` is ``True``, also check if
67
+ the style given by ``value`` is valid, according to ``rich``.
54
68
 
55
69
  Parameters
56
70
  ----------
57
71
  value
58
- The string of the value to check (obtained from reading the user input).
72
+ The string of the value to check, obtained from reading the user input.
59
73
  expected
60
- The string of the literal datatype must be matched by `value`. Obtained from
61
- reading the cfg file.
62
- style
63
- Whether or not to confirm if `value` is a style, by default False
74
+ The string of the literal datatype which must be matched by ``value``.
75
+ This is obtained from reading the ``cfg`` file.
76
+ validate_style
77
+ Whether or not to confirm if ``value`` is a valid style, according to
78
+ ``rich``. Default is ``False``.
64
79
 
65
80
  Returns
66
81
  -------
67
82
  :class:`bool`
68
- Whether or not `value` matches the datatype of `expected`.
83
+ Whether or not the literal from ``value`` matches the datatype of the
84
+ literal from ``expected``.
69
85
  """
70
- value = value_from_string(value)
71
- expected = type(value_from_string(expected))
86
+ value_literal = value_from_string(value)
87
+ ExpectedLiteralType = type(value_from_string(expected))
72
88
 
73
- return isinstance(value, expected) and (is_valid_style(value) if style else True)
89
+ return isinstance(value_literal, ExpectedLiteralType) and (
90
+ (isinstance(value_literal, str) and is_valid_style(value_literal))
91
+ if validate_style
92
+ else True
93
+ )
74
94
 
75
95
 
76
96
  def is_valid_style(style: str) -> bool:
77
- """Checks whether the entered color is a valid color according to rich
97
+ """Checks whether the entered color style is valid, according to ``rich``.
98
+
78
99
  Parameters
79
100
  ----------
80
101
  style
81
102
  The style to check whether it is valid.
103
+
82
104
  Returns
83
105
  -------
84
- Boolean
85
- Returns whether it is valid style or not according to rich.
106
+ :class:`bool`
107
+ Whether the color style is valid or not, according to ``rich``.
86
108
  """
87
109
  try:
88
110
  Style.parse(style)
@@ -91,16 +113,20 @@ def is_valid_style(style: str) -> bool:
91
113
  return False
92
114
 
93
115
 
94
- def replace_keys(default: dict) -> dict:
95
- """Replaces _ to . and vice versa in a dictionary for rich
116
+ def replace_keys(default: dict[str, Any]) -> dict[str, Any]:
117
+ """Replace ``_`` with ``.`` and vice versa in a dictionary's keys for
118
+ ``rich``.
119
+
96
120
  Parameters
97
121
  ----------
98
122
  default
99
- The dictionary to check and replace
123
+ The dictionary whose keys will be checked and replaced.
124
+
100
125
  Returns
101
126
  -------
102
127
  :class:`dict`
103
- The dictionary which is modified by replacing _ with . and vice versa
128
+ The dictionary whose keys are modified by replacing ``_`` with ``.``
129
+ and vice versa.
104
130
  """
105
131
  for key in default:
106
132
  if "_" in key:
@@ -123,22 +149,22 @@ def replace_keys(default: dict) -> dict:
123
149
  epilog=EPILOG,
124
150
  help="Manages Manim configuration files.",
125
151
  )
126
- @click.pass_context
127
- def cfg(ctx):
152
+ @cloup.pass_context
153
+ def cfg(ctx: cloup.Context) -> None:
128
154
  """Responsible for the cfg subcommand."""
129
155
  pass
130
156
 
131
157
 
132
158
  @cfg.command(context_settings=cli_ctx_settings, no_args_is_help=True)
133
- @click.option(
159
+ @cloup.option(
134
160
  "-l",
135
161
  "--level",
136
- type=click.Choice(["user", "cwd"], case_sensitive=False),
162
+ type=cloup.Choice(["user", "cwd"], case_sensitive=False),
137
163
  default="cwd",
138
164
  help="Specify if this config is for user or the working directory.",
139
165
  )
140
- @click.option("-o", "--open", "openfile", is_flag=True)
141
- def write(level: str = None, openfile: bool = False) -> None:
166
+ @cloup.option("-o", "--open", "openfile", is_flag=True)
167
+ def write(level: str | None = None, openfile: bool = False) -> None:
142
168
  config_paths = config_file_paths()
143
169
  console.print(
144
170
  "[yellow bold]Manim Configuration File Writer[/yellow bold]",
@@ -157,7 +183,7 @@ To save your config please save that file and place it in your current working d
157
183
  action = "save this as"
158
184
  for category in parser:
159
185
  console.print(f"{category}", style="bold green underline")
160
- default = parser[category]
186
+ default = cast(dict[str, Any], parser[category])
161
187
  if category == "logger":
162
188
  console.print(RICH_COLOUR_INSTRUCTIONS)
163
189
  default = replace_keys(default)
@@ -187,7 +213,7 @@ To save your config please save that file and place it in your current working d
187
213
  """Not enough values in input.
188
214
  You may have added a new entry to default.cfg, in which case you will have to
189
215
  modify write_cfg_subcmd_input to account for it.""",
190
- )
216
+ ) from None
191
217
  if temp:
192
218
  while temp and not _is_expected_datatype(
193
219
  temp,
@@ -240,7 +266,7 @@ modify write_cfg_subcmd_input to account for it.""",
240
266
 
241
267
 
242
268
  @cfg.command(context_settings=cli_ctx_settings)
243
- def show():
269
+ def show() -> None:
244
270
  parser = make_config_parser()
245
271
  rich_non_style_entries = [a.replace(".", "_") for a in RICH_NON_STYLE_ENTRIES]
246
272
  for category in parser:
@@ -258,9 +284,9 @@ def show():
258
284
 
259
285
 
260
286
  @cfg.command(context_settings=cli_ctx_settings)
261
- @click.option("-d", "--directory", default=Path.cwd())
262
- @click.pass_context
263
- def export(ctx, directory):
287
+ @cloup.option("-d", "--directory", default=Path.cwd())
288
+ @cloup.pass_context
289
+ def export(ctx: cloup.Context, directory: str) -> None:
264
290
  directory_path = Path(directory)
265
291
  if directory_path.absolute == Path.cwd().absolute:
266
292
  console.print(
@@ -1,63 +1,79 @@
1
1
  """Auxiliary module for the checkhealth subcommand, contains
2
- the actual check implementations."""
2
+ the actual check implementations.
3
+ """
3
4
 
4
5
  from __future__ import annotations
5
6
 
6
7
  import os
7
8
  import shutil
8
- import subprocess
9
- from typing import Callable
9
+ from typing import Callable, Protocol, cast
10
10
 
11
- from ..._config import config
11
+ __all__ = ["HEALTH_CHECKS"]
12
12
 
13
- HEALTH_CHECKS = []
13
+
14
+ class HealthCheckFunction(Protocol):
15
+ description: str
16
+ recommendation: str
17
+ skip_on_failed: list[str]
18
+ post_fail_fix_hook: Callable[..., object] | None
19
+ __name__: str
20
+
21
+ def __call__(self) -> bool: ...
22
+
23
+
24
+ HEALTH_CHECKS: list[HealthCheckFunction] = []
14
25
 
15
26
 
16
27
  def healthcheck(
17
28
  description: str,
18
29
  recommendation: str,
19
- skip_on_failed: list[Callable | str] | None = None,
20
- post_fail_fix_hook: Callable | None = None,
21
- ):
30
+ skip_on_failed: list[HealthCheckFunction | str] | None = None,
31
+ post_fail_fix_hook: Callable[..., object] | None = None,
32
+ ) -> Callable[[Callable[[], bool]], HealthCheckFunction]:
22
33
  """Decorator used for declaring health checks.
23
34
 
24
- This decorator attaches some data to a function,
25
- which is then added to a list containing all checks.
35
+ This decorator attaches some data to a function, which is then added to a
36
+ a list containing all checks.
26
37
 
27
38
  Parameters
28
39
  ----------
29
40
  description
30
- A brief description of this check, displayed when
31
- the checkhealth subcommand is run.
41
+ A brief description of this check, displayed when the ``checkhealth``
42
+ subcommand is run.
32
43
  recommendation
33
44
  Help text which is displayed in case the check fails.
34
45
  skip_on_failed
35
- A list of check functions which, if they fail, cause
36
- the current check to be skipped.
46
+ A list of check functions which, if they fail, cause the current check
47
+ to be skipped.
37
48
  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.
49
+ A function that is meant to (interactively) help to fix the detected
50
+ problem, if possible. This is only called upon explicit confirmation of
51
+ the user.
41
52
 
42
53
  Returns
43
54
  -------
44
- A check function, as required by the checkhealth subcommand.
55
+ Callable[Callable[[], bool], :class:`HealthCheckFunction`]
56
+ A decorator which converts a function into a health check function, as
57
+ required by the ``checkhealth`` subcommand.
45
58
  """
59
+ new_skip_on_failed: list[str]
46
60
  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
- ]
61
+ new_skip_on_failed = []
62
+ else:
63
+ new_skip_on_failed = [
64
+ skip.__name__ if callable(skip) else skip for skip in skip_on_failed
65
+ ]
51
66
 
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
67
+ def wrapper(func: Callable[[], bool]) -> HealthCheckFunction:
68
+ health_func = cast(HealthCheckFunction, func)
69
+ health_func.description = description
70
+ health_func.recommendation = recommendation
71
+ health_func.skip_on_failed = new_skip_on_failed
72
+ health_func.post_fail_fix_hook = post_fail_fix_hook
73
+ HEALTH_CHECKS.append(health_func)
74
+ return health_func
59
75
 
60
- return decorator
76
+ return wrapper
61
77
 
62
78
 
63
79
  @healthcheck(
@@ -75,7 +91,14 @@ def healthcheck(
75
91
  "PATH variable."
76
92
  ),
77
93
  )
78
- def is_manim_on_path():
94
+ def is_manim_on_path() -> bool:
95
+ """Check whether ``manim`` is in ``PATH``.
96
+
97
+ Returns
98
+ -------
99
+ :class:`bool`
100
+ Whether ``manim`` is in ``PATH`` or not.
101
+ """
79
102
  path_to_manim = shutil.which("manim")
80
103
  return path_to_manim is not None
81
104
 
@@ -91,10 +114,30 @@ def is_manim_on_path():
91
114
  ),
92
115
  skip_on_failed=[is_manim_on_path],
93
116
  )
94
- def is_manim_executable_associated_to_this_library():
117
+ def is_manim_executable_associated_to_this_library() -> bool:
118
+ """Check whether the ``manim`` executable in ``PATH`` is associated to this
119
+ library. To verify this, the executable should look like this:
120
+
121
+ .. code-block:: python
122
+
123
+ #!<MANIM_PATH>/.../python
124
+ import sys
125
+ from manim.__main__ import main
126
+
127
+ if __name__ == "__main__":
128
+ sys.exit(main())
129
+
130
+
131
+ Returns
132
+ -------
133
+ :class:`bool`
134
+ Whether the ``manim`` executable in ``PATH`` is associated to this
135
+ library or not.
136
+ """
95
137
  path_to_manim = shutil.which("manim")
96
- with open(path_to_manim, "rb") as f:
97
- manim_exec = f.read()
138
+ assert path_to_manim is not None
139
+ with open(path_to_manim, "rb") as manim_binary:
140
+ manim_exec = manim_binary.read()
98
141
 
99
142
  # first condition below corresponds to the executable being
100
143
  # some sort of python script. second condition happens when
@@ -102,45 +145,6 @@ def is_manim_executable_associated_to_this_library():
102
145
  return b"manim.__main__" in manim_exec or b'"%~dp0\\manim"' in manim_exec
103
146
 
104
147
 
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
148
  @healthcheck(
145
149
  description="Checking whether latex is available",
146
150
  recommendation=(
@@ -153,7 +157,14 @@ def is_ffmpeg_working():
153
157
  "LaTeX distribution on your operating system."
154
158
  ),
155
159
  )
156
- def is_latex_available():
160
+ def is_latex_available() -> bool:
161
+ """Check whether ``latex`` is in ``PATH`` and can be executed.
162
+
163
+ Returns
164
+ -------
165
+ :class:`bool`
166
+ Whether ``latex`` is in ``PATH`` and can be executed or not.
167
+ """
157
168
  path_to_latex = shutil.which("latex")
158
169
  return path_to_latex is not None and os.access(path_to_latex, os.X_OK)
159
170
 
@@ -168,6 +179,13 @@ def is_latex_available():
168
179
  ),
169
180
  skip_on_failed=[is_latex_available],
170
181
  )
171
- def is_dvisvgm_available():
182
+ def is_dvisvgm_available() -> bool:
183
+ """Check whether ``dvisvgm`` is in ``PATH`` and can be executed.
184
+
185
+ Returns
186
+ -------
187
+ :class:`bool`
188
+ Whether ``dvisvgm`` is in ``PATH`` and can be executed or not.
189
+ """
172
190
  path_to_dvisvgm = shutil.which("dvisvgm")
173
191
  return path_to_dvisvgm is not None and os.access(path_to_dvisvgm, os.X_OK)
@@ -6,23 +6,26 @@ your Manim installation.
6
6
  from __future__ import annotations
7
7
 
8
8
  import sys
9
+ import timeit
9
10
 
10
11
  import click
11
12
  import cloup
12
13
 
13
- from .checks import HEALTH_CHECKS
14
+ from manim.cli.checkhealth.checks import HEALTH_CHECKS, HealthCheckFunction
15
+
16
+ __all__ = ["checkhealth"]
14
17
 
15
18
 
16
19
  @cloup.command(
17
20
  context_settings=None,
18
21
  )
19
- def checkhealth():
22
+ def checkhealth() -> None:
20
23
  """This subcommand checks whether Manim is installed correctly
21
24
  and has access to its required (and optional) system dependencies.
22
25
  """
23
26
  click.echo(f"Python executable: {sys.executable}\n")
24
27
  click.echo("Checking whether your installation of Manim Community is healthy...")
25
- failed_checks = []
28
+ failed_checks: list[HealthCheckFunction] = []
26
29
 
27
30
  for check in HEALTH_CHECKS:
28
31
  click.echo(f"- {check.description} ... ", nl=False)
@@ -60,7 +63,7 @@ def checkhealth():
60
63
  import manim as mn
61
64
 
62
65
  class CheckHealthDemo(mn.Scene):
63
- def construct(self):
66
+ def _inner_construct(self) -> None:
64
67
  banner = mn.ManimBanner().shift(mn.UP * 0.5)
65
68
  self.play(banner.create())
66
69
  self.wait(0.5)
@@ -77,5 +80,11 @@ def checkhealth():
77
80
  mn.FadeOut(text_tex_group, shift=mn.DOWN),
78
81
  )
79
82
 
83
+ def construct(self) -> None:
84
+ self.execution_time = timeit.timeit(self._inner_construct, number=1)
85
+
80
86
  with mn.tempconfig({"preview": True, "disable_caching": True}):
81
- CheckHealthDemo().render()
87
+ scene = CheckHealthDemo()
88
+ scene.render()
89
+
90
+ click.echo(f"Scene rendered in {scene.execution_time:.2f} seconds.")