manim 0.18.1__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 (129) hide show
  1. manim/__main__.py +45 -12
  2. manim/_config/__init__.py +2 -2
  3. manim/_config/cli_colors.py +8 -4
  4. manim/_config/default.cfg +0 -2
  5. manim/_config/logger_utils.py +5 -0
  6. manim/_config/utils.py +29 -38
  7. manim/animation/animation.py +148 -8
  8. manim/animation/composition.py +16 -13
  9. manim/animation/creation.py +184 -8
  10. manim/animation/fading.py +5 -8
  11. manim/animation/indication.py +93 -26
  12. manim/animation/movement.py +21 -3
  13. manim/animation/rotation.py +2 -1
  14. manim/animation/specialized.py +3 -5
  15. manim/animation/speedmodifier.py +3 -3
  16. manim/animation/transform.py +4 -5
  17. manim/animation/updaters/mobject_update_utils.py +17 -14
  18. manim/camera/camera.py +2 -2
  19. manim/cli/__init__.py +17 -0
  20. manim/cli/cfg/group.py +52 -36
  21. manim/cli/checkhealth/checks.py +92 -76
  22. manim/cli/checkhealth/commands.py +12 -5
  23. manim/cli/default_group.py +148 -24
  24. manim/cli/init/commands.py +28 -23
  25. manim/cli/plugins/commands.py +13 -3
  26. manim/cli/render/commands.py +47 -42
  27. manim/cli/render/global_options.py +43 -9
  28. manim/cli/render/render_options.py +84 -19
  29. manim/constants.py +11 -4
  30. manim/mobject/frame.py +0 -1
  31. manim/mobject/geometry/arc.py +109 -75
  32. manim/mobject/geometry/boolean_ops.py +20 -17
  33. manim/mobject/geometry/labeled.py +300 -77
  34. manim/mobject/geometry/line.py +120 -60
  35. manim/mobject/geometry/polygram.py +109 -25
  36. manim/mobject/geometry/shape_matchers.py +35 -15
  37. manim/mobject/geometry/tips.py +36 -27
  38. manim/mobject/graph.py +48 -40
  39. manim/mobject/graphing/coordinate_systems.py +110 -45
  40. manim/mobject/graphing/functions.py +16 -10
  41. manim/mobject/graphing/number_line.py +23 -9
  42. manim/mobject/graphing/probability.py +2 -10
  43. manim/mobject/graphing/scale.py +6 -5
  44. manim/mobject/matrix.py +17 -19
  45. manim/mobject/mobject.py +149 -103
  46. manim/mobject/opengl/opengl_geometry.py +4 -8
  47. manim/mobject/opengl/opengl_mobject.py +506 -343
  48. manim/mobject/opengl/opengl_point_cloud_mobject.py +3 -7
  49. manim/mobject/opengl/opengl_surface.py +1 -2
  50. manim/mobject/opengl/opengl_vectorized_mobject.py +27 -65
  51. manim/mobject/svg/brace.py +61 -13
  52. manim/mobject/svg/svg_mobject.py +2 -1
  53. manim/mobject/table.py +11 -12
  54. manim/mobject/text/code_mobject.py +186 -550
  55. manim/mobject/text/numbers.py +7 -7
  56. manim/mobject/text/tex_mobject.py +22 -13
  57. manim/mobject/text/text_mobject.py +29 -20
  58. manim/mobject/three_d/polyhedra.py +98 -1
  59. manim/mobject/three_d/three_dimensions.py +59 -31
  60. manim/mobject/types/image_mobject.py +37 -23
  61. manim/mobject/types/point_cloud_mobject.py +103 -67
  62. manim/mobject/types/vectorized_mobject.py +387 -214
  63. manim/mobject/value_tracker.py +2 -1
  64. manim/mobject/vector_field.py +2 -4
  65. manim/opengl/__init__.py +3 -3
  66. manim/plugins/__init__.py +2 -3
  67. manim/plugins/plugins_flags.py +3 -3
  68. manim/renderer/cairo_renderer.py +11 -11
  69. manim/renderer/opengl_renderer.py +19 -20
  70. manim/renderer/shader.py +2 -3
  71. manim/renderer/shader_wrapper.py +3 -2
  72. manim/scene/moving_camera_scene.py +23 -0
  73. manim/scene/scene.py +72 -41
  74. manim/scene/scene_file_writer.py +313 -164
  75. manim/scene/section.py +15 -15
  76. manim/scene/three_d_scene.py +8 -15
  77. manim/scene/vector_space_scene.py +3 -6
  78. manim/typing.py +326 -66
  79. manim/utils/bezier.py +1658 -381
  80. manim/utils/caching.py +11 -5
  81. manim/utils/color/AS2700.py +2 -0
  82. manim/utils/color/BS381.py +2 -0
  83. manim/utils/color/DVIPSNAMES.py +96 -0
  84. manim/utils/color/SVGNAMES.py +179 -0
  85. manim/utils/color/X11.py +3 -0
  86. manim/utils/color/XKCD.py +2 -0
  87. manim/utils/color/__init__.py +8 -5
  88. manim/utils/color/core.py +818 -301
  89. manim/utils/color/manim_colors.py +7 -9
  90. manim/utils/commands.py +40 -19
  91. manim/utils/config_ops.py +18 -13
  92. manim/utils/debug.py +8 -6
  93. manim/utils/deprecation.py +92 -43
  94. manim/utils/docbuild/autoaliasattr_directive.py +45 -8
  95. manim/utils/docbuild/autocolor_directive.py +12 -13
  96. manim/utils/docbuild/manim_directive.py +35 -29
  97. manim/utils/docbuild/module_parsing.py +74 -27
  98. manim/utils/family.py +3 -3
  99. manim/utils/family_ops.py +12 -4
  100. manim/utils/file_ops.py +22 -16
  101. manim/utils/hashing.py +7 -7
  102. manim/utils/images.py +10 -4
  103. manim/utils/ipython_magic.py +12 -8
  104. manim/utils/iterables.py +161 -119
  105. manim/utils/module_ops.py +55 -19
  106. manim/utils/opengl.py +68 -23
  107. manim/utils/parameter_parsing.py +3 -2
  108. manim/utils/paths.py +11 -5
  109. manim/utils/polylabel.py +168 -0
  110. manim/utils/qhull.py +218 -0
  111. manim/utils/rate_functions.py +69 -32
  112. manim/utils/simple_functions.py +24 -15
  113. manim/utils/sounds.py +7 -1
  114. manim/utils/space_ops.py +48 -37
  115. manim/utils/testing/_frames_testers.py +13 -8
  116. manim/utils/testing/_show_diff.py +5 -3
  117. manim/utils/testing/_test_class_makers.py +33 -18
  118. manim/utils/testing/frames_comparison.py +20 -14
  119. manim/utils/tex.py +4 -2
  120. manim/utils/tex_file_writing.py +45 -45
  121. manim/utils/tex_templates.py +1 -1
  122. manim/utils/unit.py +6 -5
  123. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/METADATA +16 -9
  124. manim-0.19.0.dist-info/RECORD +221 -0
  125. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
  126. manim-0.18.1.dist-info/RECORD +0 -217
  127. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
  128. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE.community +0 -0
  129. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -178,7 +178,7 @@ def always_rotate(mobject: Mobject, rate: float = 20 * DEGREES, **kwargs) -> Mob
178
178
 
179
179
 
180
180
  def turn_animation_into_updater(
181
- animation: Animation, cycle: bool = False, **kwargs
181
+ animation: Animation, cycle: bool = False, delay: float = 0, **kwargs
182
182
  ) -> Mobject:
183
183
  """
184
184
  Add an updater to the animation's mobject which applies
@@ -187,6 +187,8 @@ def turn_animation_into_updater(
187
187
  If cycle is True, this repeats over and over. Otherwise,
188
188
  the updater will be popped upon completion
189
189
 
190
+ The ``delay`` parameter is the delay (in seconds) before the animation starts..
191
+
190
192
  Examples
191
193
  --------
192
194
 
@@ -206,21 +208,22 @@ def turn_animation_into_updater(
206
208
  mobject = animation.mobject
207
209
  animation.suspend_mobject_updating = False
208
210
  animation.begin()
209
- animation.total_time = 0
211
+ animation.total_time = -delay
210
212
 
211
213
  def update(m: Mobject, dt: float):
212
- run_time = animation.get_run_time()
213
- time_ratio = animation.total_time / run_time
214
- if cycle:
215
- alpha = time_ratio % 1
216
- else:
217
- alpha = np.clip(time_ratio, 0, 1)
218
- if alpha >= 1:
219
- animation.finish()
220
- m.remove_updater(update)
221
- return
222
- animation.interpolate(alpha)
223
- animation.update_mobjects(dt)
214
+ if animation.total_time >= 0:
215
+ run_time = animation.get_run_time()
216
+ time_ratio = animation.total_time / run_time
217
+ if cycle:
218
+ alpha = time_ratio % 1
219
+ else:
220
+ alpha = np.clip(time_ratio, 0, 1)
221
+ if alpha >= 1:
222
+ animation.finish()
223
+ m.remove_updater(update)
224
+ return
225
+ animation.interpolate(alpha)
226
+ animation.update_mobjects(dt)
224
227
  animation.total_time += dt
225
228
 
226
229
  mobject.add_updater(update)
manim/camera/camera.py CHANGED
@@ -8,8 +8,9 @@ import copy
8
8
  import itertools as it
9
9
  import operator as op
10
10
  import pathlib
11
+ from collections.abc import Iterable
11
12
  from functools import reduce
12
- from typing import Any, Callable, Iterable
13
+ from typing import Any, Callable
13
14
 
14
15
  import cairo
15
16
  import numpy as np
@@ -376,7 +377,6 @@ class Camera:
376
377
  np.array
377
378
  The pixel array which can then be passed to set_background.
378
379
  """
379
-
380
380
  logger.info("Starting set_background")
381
381
  coords = self.get_coords_of_all_pixels()
382
382
  new_background = np.apply_along_axis(coords_to_colors_func, 2, coords)
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
@@ -8,24 +8,26 @@ group.
8
8
 
9
9
  from __future__ import annotations
10
10
 
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
16
  import cloup
15
17
  from rich.errors import StyleSyntaxError
16
18
  from rich.style import Style
17
19
 
18
- from ... import cli_ctx_settings, console
19
- from ..._config.utils import config_file_paths, make_config_parser
20
- from ...constants import EPILOG
21
- 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
22
24
 
23
25
  RICH_COLOUR_INSTRUCTIONS: str = """
24
26
  [red]The default colour is used by the input statement.
25
27
  If left empty, the default colour will be used.[/red]
26
28
  [magenta] For a full list of styles, visit[/magenta] [green]https://rich.readthedocs.io/en/latest/style.html[/green]
27
29
  """
28
- RICH_NON_STYLE_ENTRIES: str = ["log.width", "log.height", "log.timestamps"]
30
+ RICH_NON_STYLE_ENTRIES: list[str] = ["log.width", "log.height", "log.timestamps"]
29
31
 
30
32
  __all__ = [
31
33
  "value_from_string",
@@ -40,7 +42,8 @@ __all__ = [
40
42
 
41
43
 
42
44
  def value_from_string(value: str) -> str | int | bool:
43
- """Extracts the literal of proper datatype from a string.
45
+ """Extract the literal of proper datatype from a ``value`` string.
46
+
44
47
  Parameters
45
48
  ----------
46
49
  value
@@ -48,51 +51,60 @@ def value_from_string(value: str) -> str | int | bool:
48
51
 
49
52
  Returns
50
53
  -------
51
- Union[:class:`str`, :class:`int`, :class:`bool`]
52
- Returns the literal of appropriate datatype.
54
+ :class:`str` | :class:`int` | :class:`bool`
55
+ The literal of appropriate datatype.
53
56
  """
54
- try:
57
+ with contextlib.suppress(SyntaxError, ValueError):
55
58
  value = literal_eval(value)
56
- except (SyntaxError, ValueError):
57
- pass
58
59
  return value
59
60
 
60
61
 
61
- def _is_expected_datatype(value: str, expected: str, style: bool = False) -> bool:
62
- """Checks whether `value` is the same datatype as `expected`,
63
- 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``.
64
68
 
65
69
  Parameters
66
70
  ----------
67
71
  value
68
- 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.
69
73
  expected
70
- The string of the literal datatype must be matched by `value`. Obtained from
71
- reading the cfg file.
72
- style
73
- 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``.
74
79
 
75
80
  Returns
76
81
  -------
77
82
  :class:`bool`
78
- 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``.
79
85
  """
80
- value = value_from_string(value)
81
- expected = type(value_from_string(expected))
86
+ value_literal = value_from_string(value)
87
+ ExpectedLiteralType = type(value_from_string(expected))
82
88
 
83
- 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
+ )
84
94
 
85
95
 
86
96
  def is_valid_style(style: str) -> bool:
87
- """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
+
88
99
  Parameters
89
100
  ----------
90
101
  style
91
102
  The style to check whether it is valid.
103
+
92
104
  Returns
93
105
  -------
94
- Boolean
95
- 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``.
96
108
  """
97
109
  try:
98
110
  Style.parse(style)
@@ -101,16 +113,20 @@ def is_valid_style(style: str) -> bool:
101
113
  return False
102
114
 
103
115
 
104
- def replace_keys(default: dict) -> dict:
105
- """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
+
106
120
  Parameters
107
121
  ----------
108
122
  default
109
- The dictionary to check and replace
123
+ The dictionary whose keys will be checked and replaced.
124
+
110
125
  Returns
111
126
  -------
112
127
  :class:`dict`
113
- 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.
114
130
  """
115
131
  for key in default:
116
132
  if "_" in key:
@@ -134,7 +150,7 @@ def replace_keys(default: dict) -> dict:
134
150
  help="Manages Manim configuration files.",
135
151
  )
136
152
  @cloup.pass_context
137
- def cfg(ctx):
153
+ def cfg(ctx: cloup.Context) -> None:
138
154
  """Responsible for the cfg subcommand."""
139
155
  pass
140
156
 
@@ -148,7 +164,7 @@ def cfg(ctx):
148
164
  help="Specify if this config is for user or the working directory.",
149
165
  )
150
166
  @cloup.option("-o", "--open", "openfile", is_flag=True)
151
- def write(level: str = None, openfile: bool = False) -> None:
167
+ def write(level: str | None = None, openfile: bool = False) -> None:
152
168
  config_paths = config_file_paths()
153
169
  console.print(
154
170
  "[yellow bold]Manim Configuration File Writer[/yellow bold]",
@@ -167,7 +183,7 @@ To save your config please save that file and place it in your current working d
167
183
  action = "save this as"
168
184
  for category in parser:
169
185
  console.print(f"{category}", style="bold green underline")
170
- default = parser[category]
186
+ default = cast(dict[str, Any], parser[category])
171
187
  if category == "logger":
172
188
  console.print(RICH_COLOUR_INSTRUCTIONS)
173
189
  default = replace_keys(default)
@@ -197,7 +213,7 @@ To save your config please save that file and place it in your current working d
197
213
  """Not enough values in input.
198
214
  You may have added a new entry to default.cfg, in which case you will have to
199
215
  modify write_cfg_subcmd_input to account for it.""",
200
- )
216
+ ) from None
201
217
  if temp:
202
218
  while temp and not _is_expected_datatype(
203
219
  temp,
@@ -250,7 +266,7 @@ modify write_cfg_subcmd_input to account for it.""",
250
266
 
251
267
 
252
268
  @cfg.command(context_settings=cli_ctx_settings)
253
- def show():
269
+ def show() -> None:
254
270
  parser = make_config_parser()
255
271
  rich_non_style_entries = [a.replace(".", "_") for a in RICH_NON_STYLE_ENTRIES]
256
272
  for category in parser:
@@ -270,7 +286,7 @@ def show():
270
286
  @cfg.command(context_settings=cli_ctx_settings)
271
287
  @cloup.option("-d", "--directory", default=Path.cwd())
272
288
  @cloup.pass_context
273
- def export(ctx, directory):
289
+ def export(ctx: cloup.Context, directory: str) -> None:
274
290
  directory_path = Path(directory)
275
291
  if directory_path.absolute == Path.cwd().absolute:
276
292
  console.print(
@@ -1,65 +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
10
-
11
- from ..._config import config
9
+ from typing import Callable, Protocol, cast
12
10
 
13
11
  __all__ = ["HEALTH_CHECKS"]
14
12
 
15
- 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] = []
16
25
 
17
26
 
18
27
  def healthcheck(
19
28
  description: str,
20
29
  recommendation: str,
21
- skip_on_failed: list[Callable | str] | None = None,
22
- post_fail_fix_hook: Callable | None = None,
23
- ):
30
+ skip_on_failed: list[HealthCheckFunction | str] | None = None,
31
+ post_fail_fix_hook: Callable[..., object] | None = None,
32
+ ) -> Callable[[Callable[[], bool]], HealthCheckFunction]:
24
33
  """Decorator used for declaring health checks.
25
34
 
26
- This decorator attaches some data to a function,
27
- 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.
28
37
 
29
38
  Parameters
30
39
  ----------
31
40
  description
32
- A brief description of this check, displayed when
33
- the checkhealth subcommand is run.
41
+ A brief description of this check, displayed when the ``checkhealth``
42
+ subcommand is run.
34
43
  recommendation
35
44
  Help text which is displayed in case the check fails.
36
45
  skip_on_failed
37
- A list of check functions which, if they fail, cause
38
- the current check to be skipped.
46
+ A list of check functions which, if they fail, cause the current check
47
+ to be skipped.
39
48
  post_fail_fix_hook
40
- A function that is supposed to (interactively) help
41
- to fix the detected problem, if possible. This is
42
- 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.
43
52
 
44
53
  Returns
45
54
  -------
46
- 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.
47
58
  """
59
+ new_skip_on_failed: list[str]
48
60
  if skip_on_failed is None:
49
- skip_on_failed = []
50
- skip_on_failed = [
51
- skip.__name__ if callable(skip) else skip for skip in skip_on_failed
52
- ]
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
+ ]
53
66
 
54
- def decorator(func):
55
- func.description = description
56
- func.recommendation = recommendation
57
- func.skip_on_failed = skip_on_failed
58
- func.post_fail_fix_hook = post_fail_fix_hook
59
- HEALTH_CHECKS.append(func)
60
- 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
61
75
 
62
- return decorator
76
+ return wrapper
63
77
 
64
78
 
65
79
  @healthcheck(
@@ -77,7 +91,14 @@ def healthcheck(
77
91
  "PATH variable."
78
92
  ),
79
93
  )
80
- 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
+ """
81
102
  path_to_manim = shutil.which("manim")
82
103
  return path_to_manim is not None
83
104
 
@@ -93,10 +114,30 @@ def is_manim_on_path():
93
114
  ),
94
115
  skip_on_failed=[is_manim_on_path],
95
116
  )
96
- 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
+ """
97
137
  path_to_manim = shutil.which("manim")
98
- with open(path_to_manim, "rb") as f:
99
- 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()
100
141
 
101
142
  # first condition below corresponds to the executable being
102
143
  # some sort of python script. second condition happens when
@@ -104,45 +145,6 @@ def is_manim_executable_associated_to_this_library():
104
145
  return b"manim.__main__" in manim_exec or b'"%~dp0\\manim"' in manim_exec
105
146
 
106
147
 
107
- @healthcheck(
108
- description="Checking whether ffmpeg is available",
109
- recommendation=(
110
- "Manim does not work without ffmpeg. Please follow our "
111
- "installation instructions "
112
- "at https://docs.manim.community/en/stable/installation.html "
113
- "to download ffmpeg. Then, either ...\n\n"
114
- "(a) ... make the ffmpeg executable available to your system's PATH,\n"
115
- "(b) or, alternatively, use <manim cfg write --open> to create a "
116
- "custom configuration and set the ffmpeg_executable variable to the "
117
- "full absolute path to the ffmpeg executable."
118
- ),
119
- )
120
- def is_ffmpeg_available():
121
- path_to_ffmpeg = shutil.which(config.ffmpeg_executable)
122
- return path_to_ffmpeg is not None and os.access(path_to_ffmpeg, os.X_OK)
123
-
124
-
125
- @healthcheck(
126
- description="Checking whether ffmpeg is working",
127
- recommendation=(
128
- "Your installed version of ffmpeg does not support x264 encoding, "
129
- "which manim requires. Please follow our installation instructions "
130
- "at https://docs.manim.community/en/stable/installation.html "
131
- "to download and install a newer version of ffmpeg."
132
- ),
133
- skip_on_failed=[is_ffmpeg_available],
134
- )
135
- def is_ffmpeg_working():
136
- ffmpeg_version = subprocess.run(
137
- [config.ffmpeg_executable, "-version"],
138
- stdout=subprocess.PIPE,
139
- ).stdout.decode()
140
- return (
141
- ffmpeg_version.startswith("ffmpeg version")
142
- and "--enable-libx264" in ffmpeg_version
143
- )
144
-
145
-
146
148
  @healthcheck(
147
149
  description="Checking whether latex is available",
148
150
  recommendation=(
@@ -155,7 +157,14 @@ def is_ffmpeg_working():
155
157
  "LaTeX distribution on your operating system."
156
158
  ),
157
159
  )
158
- 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
+ """
159
168
  path_to_latex = shutil.which("latex")
160
169
  return path_to_latex is not None and os.access(path_to_latex, os.X_OK)
161
170
 
@@ -170,6 +179,13 @@ def is_latex_available():
170
179
  ),
171
180
  skip_on_failed=[is_latex_available],
172
181
  )
173
- 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
+ """
174
190
  path_to_dvisvgm = shutil.which("dvisvgm")
175
191
  return path_to_dvisvgm is not None and os.access(path_to_dvisvgm, os.X_OK)
@@ -6,11 +6,12 @@ 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
14
15
 
15
16
  __all__ = ["checkhealth"]
16
17
 
@@ -18,13 +19,13 @@ __all__ = ["checkhealth"]
18
19
  @cloup.command(
19
20
  context_settings=None,
20
21
  )
21
- def checkhealth():
22
+ def checkhealth() -> None:
22
23
  """This subcommand checks whether Manim is installed correctly
23
24
  and has access to its required (and optional) system dependencies.
24
25
  """
25
26
  click.echo(f"Python executable: {sys.executable}\n")
26
27
  click.echo("Checking whether your installation of Manim Community is healthy...")
27
- failed_checks = []
28
+ failed_checks: list[HealthCheckFunction] = []
28
29
 
29
30
  for check in HEALTH_CHECKS:
30
31
  click.echo(f"- {check.description} ... ", nl=False)
@@ -62,7 +63,7 @@ def checkhealth():
62
63
  import manim as mn
63
64
 
64
65
  class CheckHealthDemo(mn.Scene):
65
- def construct(self):
66
+ def _inner_construct(self) -> None:
66
67
  banner = mn.ManimBanner().shift(mn.UP * 0.5)
67
68
  self.play(banner.create())
68
69
  self.wait(0.5)
@@ -79,5 +80,11 @@ def checkhealth():
79
80
  mn.FadeOut(text_tex_group, shift=mn.DOWN),
80
81
  )
81
82
 
83
+ def construct(self) -> None:
84
+ self.execution_time = timeit.timeit(self._inner_construct, number=1)
85
+
82
86
  with mn.tempconfig({"preview": True, "disable_caching": True}):
83
- CheckHealthDemo().render()
87
+ scene = CheckHealthDemo()
88
+ scene.render()
89
+
90
+ click.echo(f"Scene rendered in {scene.execution_time:.2f} seconds.")