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
@@ -0,0 +1,245 @@
1
+ """Read and parse all the Manim modules and extract documentation from them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import sys
7
+ from ast import Attribute, Name, Subscript
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from typing_extensions import TypeAlias
12
+
13
+ __all__ = ["parse_module_attributes"]
14
+
15
+
16
+ AliasInfo: TypeAlias = dict[str, str]
17
+ """Dictionary with a `definition` key containing the definition of
18
+ a :class:`TypeAlias` as a string, and optionally a `doc` key containing
19
+ the documentation for that alias, if it exists.
20
+ """
21
+
22
+ AliasCategoryDict: TypeAlias = dict[str, AliasInfo]
23
+ """Dictionary which holds an `AliasInfo` for every alias name in a same
24
+ category.
25
+ """
26
+
27
+ ModuleLevelAliasDict: TypeAlias = dict[str, AliasCategoryDict]
28
+ """Dictionary containing every :class:`TypeAlias` defined in a module,
29
+ classified by category in different `AliasCategoryDict` objects.
30
+ """
31
+
32
+ ModuleTypeVarDict: TypeAlias = dict[str, str]
33
+ """Dictionary containing every :class:`TypeVar` defined in a module."""
34
+
35
+
36
+ AliasDocsDict: TypeAlias = dict[str, ModuleLevelAliasDict]
37
+ """Dictionary which, for every module in Manim, contains documentation
38
+ about their module-level attributes which are explicitly defined as
39
+ :class:`TypeAlias`, separating them from the rest of attributes.
40
+ """
41
+
42
+ DataDict: TypeAlias = dict[str, list[str]]
43
+ """Type for a dictionary which, for every module, contains a list with
44
+ the names of all their DOCUMENTED module-level attributes (identified
45
+ by Sphinx via the ``data`` role, hence the name) which are NOT
46
+ explicitly defined as :class:`TypeAlias`.
47
+ """
48
+
49
+ TypeVarDict: TypeAlias = dict[str, ModuleTypeVarDict]
50
+ """A dictionary mapping module names to dictionaries of :class:`TypeVar` objects."""
51
+
52
+ ALIAS_DOCS_DICT: AliasDocsDict = {}
53
+ DATA_DICT: DataDict = {}
54
+ TYPEVAR_DICT: TypeVarDict = {}
55
+
56
+ MANIM_ROOT = Path(__file__).resolve().parent.parent.parent
57
+
58
+ # In the following, we will use ``type(xyz) is xyz_type`` instead of
59
+ # isinstance checks to make sure no subclasses of the type pass the
60
+ # check
61
+ # ruff: noqa: E721
62
+
63
+
64
+ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict, TypeVarDict]:
65
+ """Read all files, generate Abstract Syntax Trees from them, and
66
+ extract useful information about the type aliases defined in the
67
+ files: the category they belong to, their definition and their
68
+ description, separating them from the "regular" module attributes.
69
+
70
+ Returns
71
+ -------
72
+ ALIAS_DOCS_DICT : :class:`AliasDocsDict`
73
+ A dictionary containing the information from all the type
74
+ aliases in Manim. See :class:`AliasDocsDict` for more information.
75
+
76
+ DATA_DICT : :class:`DataDict`
77
+ A dictionary containing the names of all DOCUMENTED
78
+ module-level attributes which are not a :class:`TypeAlias`.
79
+
80
+ TYPEVAR_DICT : :class:`TypeVarDict`
81
+ A dictionary containing the definitions of :class:`TypeVar` objects,
82
+ organized by modules.
83
+ """
84
+ global ALIAS_DOCS_DICT
85
+ global DATA_DICT
86
+ global TYPEVAR_DICT
87
+
88
+ if ALIAS_DOCS_DICT or DATA_DICT or TYPEVAR_DICT:
89
+ return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT
90
+
91
+ for module_path in MANIM_ROOT.rglob("*.py"):
92
+ module_name_t1 = module_path.resolve().relative_to(MANIM_ROOT)
93
+ module_name_t2 = list(module_name_t1.parts)
94
+ module_name_t2[-1] = module_name_t2[-1].removesuffix(".py")
95
+ module_name = ".".join(module_name_t2)
96
+
97
+ module_content = module_path.read_text(encoding="utf-8")
98
+
99
+ # For storing TypeAliases
100
+ module_dict: ModuleLevelAliasDict = {}
101
+ category_dict: AliasCategoryDict | None = None
102
+ alias_info: AliasInfo | None = None
103
+
104
+ # For storing TypeVars
105
+ module_typevars: ModuleTypeVarDict = {}
106
+
107
+ # For storing regular module attributes
108
+ data_list: list[str] = []
109
+ data_name: str | None = None
110
+
111
+ for node in ast.iter_child_nodes(ast.parse(module_content)):
112
+ # If we encounter a string:
113
+ if (
114
+ type(node) is ast.Expr
115
+ and type(node.value) is ast.Constant
116
+ and type(node.value.value) is str
117
+ ):
118
+ string = node.value.value.strip()
119
+ # It can be the start of a category
120
+ section_str = "[CATEGORY]"
121
+ if string.startswith(section_str):
122
+ category_name = string[len(section_str) :].strip()
123
+ module_dict[category_name] = {}
124
+ category_dict = module_dict[category_name]
125
+ alias_info = None
126
+ # or a docstring of the alias defined before
127
+ elif alias_info:
128
+ alias_info["doc"] = string
129
+ # or a docstring of the module attribute defined before
130
+ elif data_name:
131
+ data_list.append(data_name)
132
+ continue
133
+
134
+ # if it's defined under if TYPE_CHECKING
135
+ # go through the body of the if statement
136
+ if (
137
+ # NOTE: This logic does not (and cannot)
138
+ # check if the comparison is against a
139
+ # variable called TYPE_CHECKING
140
+ # It also says that you cannot do the following
141
+ # import typing as foo
142
+ # if foo.TYPE_CHECKING:
143
+ # BAR: TypeAlias = ...
144
+ type(node) is ast.If
145
+ and (
146
+ (
147
+ # if TYPE_CHECKING
148
+ type(node.test) is ast.Name and node.test.id == "TYPE_CHECKING"
149
+ )
150
+ or (
151
+ # if typing.TYPE_CHECKING
152
+ type(node.test) is ast.Attribute
153
+ and type(node.test.value) is ast.Name
154
+ and node.test.value.id == "typing"
155
+ and node.test.attr == "TYPE_CHECKING"
156
+ )
157
+ )
158
+ ):
159
+ inner_nodes: list[Any] = node.body
160
+ else:
161
+ inner_nodes = [node]
162
+
163
+ for node in inner_nodes:
164
+ # Check if this node is a TypeAlias (type <name> = <value>)
165
+ # or an AnnAssign annotated as TypeAlias (<target>: TypeAlias = <value>).
166
+ is_type_alias = (
167
+ sys.version_info >= (3, 12) and type(node) is ast.TypeAlias
168
+ )
169
+ is_annotated_assignment_with_value = (
170
+ type(node) is ast.AnnAssign
171
+ and type(node.annotation) is ast.Name
172
+ and node.annotation.id == "TypeAlias"
173
+ and type(node.target) is ast.Name
174
+ and node.value is not None
175
+ )
176
+ if is_type_alias or is_annotated_assignment_with_value:
177
+ # TODO: ast.TypeAlias does not exist before Python 3.12, and that
178
+ # could be the reason why MyPy does not recognize these as
179
+ # attributes of node.
180
+ alias_name = node.name.id if is_type_alias else node.target.id # type: ignore[attr-defined]
181
+ definition_node = node.value # type: ignore[attr-defined]
182
+
183
+ # If the definition is a Union, replace with vertical bar notation.
184
+ # Instead of "Union[Type1, Type2]", we'll have "Type1 | Type2".
185
+ if (
186
+ type(definition_node) is ast.Subscript
187
+ and type(definition_node.value) is ast.Name
188
+ and definition_node.value.id == "Union"
189
+ ):
190
+ union_elements = definition_node.slice.elts # type: ignore[attr-defined]
191
+ definition = " | ".join(
192
+ ast.unparse(elem) for elem in union_elements
193
+ )
194
+ else:
195
+ definition = ast.unparse(definition_node)
196
+
197
+ definition = definition.replace("npt.", "")
198
+ if category_dict is None:
199
+ module_dict[""] = {}
200
+ category_dict = module_dict[""]
201
+ category_dict[alias_name] = {"definition": definition}
202
+ alias_info = category_dict[alias_name]
203
+ continue
204
+
205
+ # Check if it is a typing.TypeVar (<target> = TypeVar(...)).
206
+ elif (
207
+ type(node) is ast.Assign
208
+ and type(node.targets[0]) is ast.Name
209
+ and type(node.value) is ast.Call
210
+ and type(node.value.func) is ast.Name
211
+ and node.value.func.id.endswith("TypeVar")
212
+ ):
213
+ module_typevars[node.targets[0].id] = ast.unparse(
214
+ node.value
215
+ ).replace("_", r"\_")
216
+ continue
217
+
218
+ # If here, the node is not a TypeAlias definition
219
+ alias_info = None
220
+
221
+ # It could still be a module attribute definition.
222
+ # Does the assignment have a target of type Name? Then
223
+ # it could be considered a definition of a module attribute.
224
+ if type(node) is ast.AnnAssign:
225
+ target: Name | Attribute | Subscript | ast.expr | None = node.target
226
+ elif type(node) is ast.Assign and len(node.targets) == 1:
227
+ target = node.targets[0]
228
+ else:
229
+ target = None
230
+
231
+ if type(target) is ast.Name and not (
232
+ type(node) is ast.Assign and target.id not in module_typevars
233
+ ):
234
+ data_name = target.id
235
+ else:
236
+ data_name = None
237
+
238
+ if len(module_dict) > 0:
239
+ ALIAS_DOCS_DICT[module_name] = module_dict
240
+ if len(data_list) > 0:
241
+ DATA_DICT[module_name] = data_list
242
+ if module_typevars:
243
+ TYPEVAR_DICT[module_name] = module_typevars
244
+
245
+ return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT
manim/utils/exceptions.py CHANGED
@@ -1,5 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ __all__ = [
4
+ "EndSceneEarlyException",
5
+ "RerunSceneException",
6
+ "MultiAnimationOverrideException",
7
+ ]
8
+
3
9
 
4
10
  class EndSceneEarlyException(Exception):
5
11
  pass
manim/utils/family.py CHANGED
@@ -1,17 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import itertools as it
4
- from typing import Iterable
4
+ from collections.abc import Iterable
5
5
 
6
6
  from ..mobject.mobject import Mobject
7
7
  from ..utils.iterables import remove_list_redundancies
8
8
 
9
+ __all__ = ["extract_mobject_family_members"]
10
+
9
11
 
10
12
  def extract_mobject_family_members(
11
13
  mobjects: Iterable[Mobject],
12
- use_z_index=False,
14
+ use_z_index: bool = False,
13
15
  only_those_with_points: bool = False,
14
- ):
16
+ ) -> list[Mobject]:
15
17
  """Returns a list of the types of mobjects and their family members present.
16
18
  A "family" in this context refers to a mobject, its submobjects, and their
17
19
  submobjects, recursively.
manim/utils/family_ops.py CHANGED
@@ -2,15 +2,26 @@ from __future__ import annotations
2
2
 
3
3
  import itertools as it
4
4
 
5
+ from manim.mobject.mobject import Mobject
5
6
 
6
- def extract_mobject_family_members(mobject_list, only_those_with_points=False):
7
+ __all__ = [
8
+ "extract_mobject_family_members",
9
+ "restructure_list_to_exclude_certain_family_members",
10
+ ]
11
+
12
+
13
+ def extract_mobject_family_members(
14
+ mobject_list: list[Mobject], only_those_with_points: bool = False
15
+ ) -> list[Mobject]:
7
16
  result = list(it.chain(*(mob.get_family() for mob in mobject_list)))
8
17
  if only_those_with_points:
9
18
  result = [mob for mob in result if mob.has_points()]
10
19
  return result
11
20
 
12
21
 
13
- def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove):
22
+ def restructure_list_to_exclude_certain_family_members(
23
+ mobject_list: list[Mobject], to_remove: list[Mobject]
24
+ ) -> list[Mobject]:
14
25
  """
15
26
  Removes anything in to_remove from mobject_list, but in the event that one of
16
27
  the items to be removed is a member of the family of an item in mobject_list,
@@ -20,10 +31,12 @@ def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove):
20
31
  but one of its submobjects is removed, e.g. scene.remove(m1), it's useful
21
32
  for the list of mobject_list to be edited to contain other submobjects, but not m1.
22
33
  """
23
- new_list = []
34
+ new_list: list[Mobject] = []
24
35
  to_remove = extract_mobject_family_members(to_remove)
25
36
 
26
- def add_safe_mobjects_from_list(list_to_examine, set_to_remove):
37
+ def add_safe_mobjects_from_list(
38
+ list_to_examine: list[Mobject], set_to_remove: set[Mobject]
39
+ ) -> None:
27
40
  for mob in list_to_examine:
28
41
  if mob in set_to_remove:
29
42
  continue
manim/utils/file_ops.py CHANGED
@@ -28,6 +28,8 @@ from shutil import copyfile
28
28
  from typing import TYPE_CHECKING
29
29
 
30
30
  if TYPE_CHECKING:
31
+ from manim.typing import StrPath
32
+
31
33
  from ..scene.scene_file_writer import SceneFileWriter
32
34
 
33
35
  from manim import __version__, config, logger
@@ -45,7 +47,8 @@ def is_mp4_format() -> bool:
45
47
  ``True`` if format is set as mp4
46
48
 
47
49
  """
48
- return config["format"] == "mp4"
50
+ val: bool = config["format"] == "mp4"
51
+ return val
49
52
 
50
53
 
51
54
  def is_gif_format() -> bool:
@@ -58,7 +61,8 @@ def is_gif_format() -> bool:
58
61
  ``True`` if format is set as gif
59
62
 
60
63
  """
61
- return config["format"] == "gif"
64
+ val: bool = config["format"] == "gif"
65
+ return val
62
66
 
63
67
 
64
68
  def is_webm_format() -> bool:
@@ -71,7 +75,8 @@ def is_webm_format() -> bool:
71
75
  ``True`` if format is set as webm
72
76
 
73
77
  """
74
- return config["format"] == "webm"
78
+ val: bool = config["format"] == "webm"
79
+ return val
75
80
 
76
81
 
77
82
  def is_mov_format() -> bool:
@@ -84,7 +89,8 @@ def is_mov_format() -> bool:
84
89
  ``True`` if format is set as mov
85
90
 
86
91
  """
87
- return config["format"] == "mov"
92
+ val: bool = config["format"] == "mov"
93
+ return val
88
94
 
89
95
 
90
96
  def is_png_format() -> bool:
@@ -97,7 +103,8 @@ def is_png_format() -> bool:
97
103
  ``True`` if format is set as png
98
104
 
99
105
  """
100
- return config["format"] == "png"
106
+ val: bool = config["format"] == "png"
107
+ return val
101
108
 
102
109
 
103
110
  def write_to_movie() -> bool:
@@ -124,7 +131,7 @@ def write_to_movie() -> bool:
124
131
 
125
132
  def ensure_executable(path_to_exe: Path) -> bool:
126
133
  if path_to_exe.parent == Path("."):
127
- executable = shutil.which(path_to_exe.stem)
134
+ executable: StrPath | None = shutil.which(path_to_exe.stem)
128
135
  if executable is None:
129
136
  return False
130
137
  else:
@@ -159,7 +166,7 @@ def guarantee_empty_existence(path: Path) -> Path:
159
166
 
160
167
 
161
168
  def seek_full_path_from_defaults(
162
- file_name: str, default_dir: Path, extensions: list[str]
169
+ file_name: StrPath, default_dir: Path, extensions: list[str]
163
170
  ) -> Path:
164
171
  possible_paths = [Path(file_name).expanduser()]
165
172
  possible_paths += [
@@ -186,10 +193,12 @@ def modify_atime(file_path: str) -> None:
186
193
  os.utime(file_path, times=(time.time(), Path(file_path).stat().st_mtime))
187
194
 
188
195
 
189
- def open_file(file_path, in_browser=False):
196
+ def open_file(file_path: Path, in_browser: bool = False) -> None:
190
197
  current_os = platform.system()
191
198
  if current_os == "Windows":
192
- os.startfile(file_path if not in_browser else file_path.parent)
199
+ # The method os.startfile is only available in Windows,
200
+ # ignoring type error caused by this.
201
+ os.startfile(file_path if not in_browser else file_path.parent) # type: ignore[attr-defined]
193
202
  else:
194
203
  if current_os == "Linux":
195
204
  commands = ["xdg-open"]
@@ -198,14 +207,15 @@ def open_file(file_path, in_browser=False):
198
207
  commands = ["cygstart"]
199
208
  file_path = file_path if not in_browser else file_path.parent
200
209
  elif current_os == "Darwin":
201
- if is_gif_format():
202
- commands = ["ffplay", "-loglevel", config["ffmpeg_loglevel"].lower()]
203
- else:
204
- commands = ["open"] if not in_browser else ["open", "-R"]
210
+ commands = ["open"] if not in_browser else ["open", "-R"]
205
211
  else:
206
212
  raise OSError("Unable to identify your operating system...")
207
- commands.append(file_path)
208
- sp.Popen(commands)
213
+
214
+ # check after so that file path is set correctly
215
+ if config.preview_command:
216
+ commands = [config.preview_command]
217
+ commands.append(str(file_path))
218
+ sp.run(commands)
209
219
 
210
220
 
211
221
  def open_media_file(file_writer: SceneFileWriter) -> None:
@@ -248,7 +258,7 @@ def get_template_path() -> Path:
248
258
  return Path.resolve(Path(__file__).parent.parent / "templates")
249
259
 
250
260
 
251
- def add_import_statement(file: Path):
261
+ def add_import_statement(file: Path) -> None:
252
262
  """Prepends an import statement in a file
253
263
 
254
264
  Parameters
manim/utils/hashing.py CHANGED
@@ -14,15 +14,17 @@ from typing import Any
14
14
 
15
15
  import numpy as np
16
16
 
17
- from manim.animation.animation import Animation
18
- from manim.camera.camera import Camera
19
- from manim.mobject.mobject import Mobject
20
-
21
- from .. import config, logger
17
+ from manim._config import config, logger
22
18
 
23
19
  if typing.TYPE_CHECKING:
20
+ from manim.animation.animation import Animation
21
+ from manim.camera.camera import Camera
22
+ from manim.mobject.mobject import Mobject
23
+ from manim.opengl.opengl_renderer import OpenGLCamera
24
24
  from manim.scene.scene import Scene
25
25
 
26
+ __all__ = ["KEYS_TO_FILTER_OUT", "get_hash_from_play_call", "get_json"]
27
+
26
28
  # Sometimes there are elements that are not suitable for hashing (too long or
27
29
  # run-dependent). This is used to filter them out.
28
30
  KEYS_TO_FILTER_OUT = {
@@ -221,7 +223,7 @@ class _CustomEncoder(json.JSONEncoder):
221
223
  # We return the repr and not a list to avoid the JsonEncoder to iterate over it.
222
224
  return repr(obj)
223
225
  elif hasattr(obj, "__dict__"):
224
- temp = getattr(obj, "__dict__")
226
+ temp = obj.__dict__
225
227
  # MappingProxy is scene-caching nightmare. It contains all of the object methods and attributes. We skip it as the mechanism will at some point process the object, but instantiated.
226
228
  # Indeed, there is certainly no case where scene-caching will receive only a non instancied object, as this is never used in the library or encouraged to be used user-side.
227
229
  if isinstance(temp, MappingProxyType):
@@ -322,7 +324,7 @@ def get_json(obj: dict):
322
324
 
323
325
  def get_hash_from_play_call(
324
326
  scene_object: Scene,
325
- camera_object: Camera,
327
+ camera_object: Camera | OpenGLCamera,
326
328
  animations_list: typing.Iterable[Animation],
327
329
  current_mobjects_list: typing.Iterable[Mobject],
328
330
  ) -> str:
manim/utils/images.py CHANGED
@@ -9,16 +9,22 @@ __all__ = [
9
9
  "change_to_rgba_array",
10
10
  ]
11
11
 
12
- from pathlib import Path
12
+ from pathlib import Path, PurePath
13
+ from typing import TYPE_CHECKING
13
14
 
14
15
  import numpy as np
15
16
  from PIL import Image
16
17
 
18
+ from manim.typing import RGBPixelArray
19
+
17
20
  from .. import config
18
21
  from ..utils.file_ops import seek_full_path_from_defaults
19
22
 
23
+ if TYPE_CHECKING:
24
+ pass
25
+
20
26
 
21
- def get_full_raster_image_path(image_file_name: str) -> Path:
27
+ def get_full_raster_image_path(image_file_name: str | PurePath) -> Path:
22
28
  return seek_full_path_from_defaults(
23
29
  image_file_name,
24
30
  default_dir=config.get_dir("assets_dir"),
@@ -26,7 +32,7 @@ def get_full_raster_image_path(image_file_name: str) -> Path:
26
32
  )
27
33
 
28
34
 
29
- def get_full_vector_image_path(image_file_name: str) -> Path:
35
+ def get_full_vector_image_path(image_file_name: str | PurePath) -> Path:
30
36
  return seek_full_path_from_defaults(
31
37
  image_file_name,
32
38
  default_dir=config.get_dir("assets_dir"),
@@ -49,7 +55,7 @@ def invert_image(image: np.array) -> Image:
49
55
  return Image.fromarray(arr)
50
56
 
51
57
 
52
- def change_to_rgba_array(image, dtype="uint8"):
58
+ def change_to_rgba_array(image: RGBPixelArray, dtype: str = "uint8") -> RGBPixelArray:
53
59
  """Converts an RGB array into RGBA with the alpha value opacity maxed."""
54
60
  pa = image
55
61
  if len(pa.shape) == 2:
@@ -3,18 +3,19 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import mimetypes
6
- import os
7
6
  import shutil
8
7
  from datetime import datetime
9
8
  from pathlib import Path
10
9
  from typing import Any
11
10
 
12
- from manim import Group, config, logger, tempconfig
11
+ from manim import config, logger, tempconfig
13
12
  from manim.__main__ import main
14
13
  from manim.renderer.shader import shader_program_cache
15
14
 
16
15
  from ..constants import RendererType
17
16
 
17
+ __all__ = ["ManimMagic"]
18
+
18
19
  try:
19
20
  from IPython import get_ipython
20
21
  from IPython.core.interactiveshell import InteractiveShell
@@ -33,15 +34,15 @@ else:
33
34
  class ManimMagic(Magics):
34
35
  def __init__(self, shell: InteractiveShell) -> None:
35
36
  super().__init__(shell)
36
- self.rendered_files = {}
37
+ self.rendered_files: dict[Path, Path] = {}
37
38
 
38
39
  @needs_local_scope
39
40
  @line_cell_magic
40
41
  def manim(
41
42
  self,
42
43
  line: str,
43
- cell: str = None,
44
- local_ns: dict[str, Any] = None,
44
+ cell: str | None = None,
45
+ local_ns: dict[str, Any] | None = None,
45
46
  ) -> None:
46
47
  r"""Render Manim scenes contained in IPython cells.
47
48
  Works as a line or cell magic.
@@ -126,8 +127,9 @@ else:
126
127
 
127
128
  modified_args = self.add_additional_args(args)
128
129
  args = main(modified_args, standalone_mode=False, prog_name="manim")
130
+ assert isinstance(local_ns, dict)
129
131
  with tempconfig(local_ns.get("config", {})):
130
- config.digest_args(args)
132
+ config.digest_args(args) # type: ignore[arg-type]
131
133
 
132
134
  renderer = None
133
135
  if config.renderer == RendererType.OPENGL:
@@ -166,8 +168,9 @@ else:
166
168
  shutil.copy(local_path, tmpfile)
167
169
 
168
170
  file_type = mimetypes.guess_type(config["output_file"])[0]
171
+ assert isinstance(file_type, str)
169
172
  embed = config["media_embed"]
170
- if embed is None:
173
+ if not embed:
171
174
  # videos need to be embedded when running in google colab.
172
175
  # do this automatically in case config.media_embed has not been
173
176
  # set explicitly.
@@ -193,4 +196,7 @@ else:
193
196
 
194
197
 
195
198
  def _generate_file_name() -> str:
196
- return config["scene_names"][0] + "@" + datetime.now().strftime("%Y-%m-%d@%H-%M-%S")
199
+ val: str = (
200
+ config["scene_names"][0] + "@" + datetime.now().strftime("%Y-%m-%d@%H-%M-%S")
201
+ )
202
+ return val