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
@@ -12,17 +12,16 @@ from manim.utils.docbuild.module_parsing import parse_module_attributes
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from sphinx.application import Sphinx
15
- from typing_extensions import TypeAlias
16
15
 
17
16
  __all__ = ["AliasAttrDocumenter"]
18
17
 
19
18
 
20
- ALIAS_DOCS_DICT, DATA_DICT = parse_module_attributes()
19
+ ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT = parse_module_attributes()
21
20
  ALIAS_LIST = [
22
21
  alias_name
23
22
  for module_dict in ALIAS_DOCS_DICT.values()
24
23
  for category_dict in module_dict.values()
25
- for alias_name in category_dict.keys()
24
+ for alias_name in category_dict
26
25
  ]
27
26
 
28
27
 
@@ -45,11 +44,12 @@ def smart_replace(base: str, alias: str, substitution: str) -> str:
45
44
  str
46
45
  The new string after the alias substitution.
47
46
  """
48
-
49
47
  occurrences = []
50
48
  len_alias = len(alias)
51
49
  len_base = len(base)
52
- condition = lambda char: (not char.isalnum()) and char != "_"
50
+
51
+ def condition(char: str) -> bool:
52
+ return not char.isalnum() and char != "_"
53
53
 
54
54
  start = 0
55
55
  i = 0
@@ -99,10 +99,11 @@ class AliasAttrDocumenter(Directive):
99
99
 
100
100
  def run(self) -> list[nodes.Element]:
101
101
  module_name = self.arguments[0]
102
- # Slice module_name[6:] to remove the "manim." prefix which is
103
102
  # not present in the keys of the DICTs
104
- module_alias_dict = ALIAS_DOCS_DICT.get(module_name[6:], None)
105
- module_attrs_list = DATA_DICT.get(module_name[6:], None)
103
+ module_name = module_name.removeprefix("manim.")
104
+ module_alias_dict = ALIAS_DOCS_DICT.get(module_name, None)
105
+ module_attrs_list = DATA_DICT.get(module_name, None)
106
+ module_typevars = TYPEVAR_DICT.get(module_name, None)
106
107
 
107
108
  content = nodes.container()
108
109
 
@@ -160,6 +161,11 @@ class AliasAttrDocumenter(Directive):
160
161
  for A in ALIAS_LIST:
161
162
  alias_doc = alias_doc.replace(f"`{A}`", f":class:`~.{A}`")
162
163
 
164
+ # also hyperlink the TypeVars from that module
165
+ if module_typevars is not None:
166
+ for T in module_typevars:
167
+ alias_doc = alias_doc.replace(f"`{T}`", f":class:`{T}`")
168
+
163
169
  # Add all the lines with 4 spaces behind, to consider all the
164
170
  # documentation as a paragraph INSIDE the `.. class::` block
165
171
  doc_lines = alias_doc.split("\n")
@@ -171,6 +177,37 @@ class AliasAttrDocumenter(Directive):
171
177
  self.state.nested_parse(unparsed, 0, alias_container)
172
178
  category_alias_container += alias_container
173
179
 
180
+ # then add the module TypeVars section
181
+ if module_typevars is not None:
182
+ module_typevars_section = nodes.section(ids=[f"{module_name}.typevars"])
183
+ content += module_typevars_section
184
+
185
+ # Use a rubric (title-like), just like in `module.rst`
186
+ module_typevars_section += nodes.rubric(text="TypeVar's")
187
+
188
+ # name: str
189
+ # definition: TypeVarDict = dict[str, str]
190
+ for name, definition in module_typevars.items():
191
+ # Using the `.. class::` directive is CRUCIAL, since
192
+ # function/method parameters are always annotated via
193
+ # classes - therefore Sphinx expects a class
194
+ unparsed = ViewList(
195
+ [
196
+ f".. class:: {name}",
197
+ "",
198
+ " .. parsed-literal::",
199
+ "",
200
+ f" {definition}",
201
+ "",
202
+ ]
203
+ )
204
+
205
+ # Parse the reST text into a fresh container
206
+ # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest
207
+ typevar_container = nodes.container()
208
+ self.state.nested_parse(unparsed, 0, typevar_container)
209
+ module_typevars_section += typevar_container
210
+
174
211
  # Then, add the traditional "Module Attributes" section
175
212
  if module_attrs_list is not None:
176
213
  module_attrs_section = nodes.section(ids=[f"{module_name}.data"])
@@ -26,7 +26,9 @@ class ManimColorModuleDocumenter(Directive):
26
26
  has_content = True
27
27
 
28
28
  def add_directive_header(self, sig: str) -> None:
29
- super().add_directive_header(sig)
29
+ # TODO: The Directive class has no method named
30
+ # add_directive_header.
31
+ super().add_directive_header(sig) # type: ignore[misc]
30
32
 
31
33
  def run(self) -> list[nodes.Element]:
32
34
  module_name = self.arguments[0]
@@ -37,8 +39,8 @@ class ManimColorModuleDocumenter(Directive):
37
39
  except ImportError:
38
40
  return [
39
41
  nodes.error(
40
- None,
41
- nodes.paragraph(text="Failed to import module '%s'" % module_name),
42
+ None, # type: ignore[arg-type]
43
+ nodes.paragraph(text=f"Failed to import module '{module_name}'"),
42
44
  )
43
45
  ]
44
46
 
@@ -53,13 +55,13 @@ class ManimColorModuleDocumenter(Directive):
53
55
 
54
56
  # Create header rows for the table
55
57
  thead = nodes.thead()
56
- row = nodes.row()
58
+ header_row = nodes.row()
57
59
  for _ in range(num_color_cols):
58
- col1 = nodes.paragraph(text="Color Name")
59
- col2 = nodes.paragraph(text="RGB Hex Code")
60
- row += nodes.entry("", col1)
61
- row += nodes.entry("", col2)
62
- thead += row
60
+ header_col1 = nodes.paragraph(text="Color Name")
61
+ header_col2 = nodes.paragraph(text="RGB Hex Code")
62
+ header_row += nodes.entry("", header_col1)
63
+ header_row += nodes.entry("", header_col2)
64
+ thead += header_row
63
65
  tgroup += thead
64
66
 
65
67
  color_elements = []
@@ -69,10 +71,7 @@ class ManimColorModuleDocumenter(Directive):
69
71
  luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
70
72
 
71
73
  # Choose the font color based on the background luminance
72
- if luminance > 0.5:
73
- font_color = "black"
74
- else:
75
- font_color = "white"
74
+ font_color = "black" if luminance > 0.5 else "white"
76
75
 
77
76
  color_elements.append((member_name, member_obj.to_hex(), font_color))
78
77
 
@@ -82,18 +82,17 @@ from __future__ import annotations
82
82
 
83
83
  import csv
84
84
  import itertools as it
85
- import os
86
85
  import re
87
86
  import shutil
88
87
  import sys
89
88
  import textwrap
90
89
  from pathlib import Path
91
90
  from timeit import timeit
92
- from typing import TYPE_CHECKING, Any
91
+ from typing import TYPE_CHECKING, Any, TypedDict
93
92
 
94
93
  import jinja2
95
94
  from docutils import nodes
96
- from docutils.parsers.rst import Directive, directives # type: ignore
95
+ from docutils.parsers.rst import Directive, directives
97
96
  from docutils.statemachine import StringList
98
97
 
99
98
  from manim import QUALITIES
@@ -102,12 +101,18 @@ from manim import __version__ as manim_version
102
101
  if TYPE_CHECKING:
103
102
  from sphinx.application import Sphinx
104
103
 
104
+
105
105
  __all__ = ["ManimDirective"]
106
106
 
107
107
 
108
108
  classnamedict: dict[str, int] = {}
109
109
 
110
110
 
111
+ class SetupMetadata(TypedDict):
112
+ parallel_read_safe: bool
113
+ parallel_write_safe: bool
114
+
115
+
111
116
  class SkipManimNode(nodes.Admonition, nodes.Element):
112
117
  """Auxiliary node class that is used when the ``skip-manim`` tag is present
113
118
  or ``.pot`` files are being built.
@@ -119,13 +124,15 @@ class SkipManimNode(nodes.Admonition, nodes.Element):
119
124
 
120
125
 
121
126
  def visit(self: SkipManimNode, node: nodes.Element, name: str = "") -> None:
122
- self.visit_admonition(node, name)
127
+ # TODO: Parent classes don't have a visit_admonition() method.
128
+ self.visit_admonition(node, name) # type: ignore[attr-defined]
123
129
  if not isinstance(node[0], nodes.title):
124
130
  node.insert(0, nodes.title("skip-manim", "Example Placeholder"))
125
131
 
126
132
 
127
133
  def depart(self: SkipManimNode, node: nodes.Element) -> None:
128
- self.depart_admonition(node)
134
+ # TODO: Parent classes don't have a depart_admonition() method.
135
+ self.depart_admonition(node) # type: ignore[attr-defined]
129
136
 
130
137
 
131
138
  def process_name_list(option_input: str, reference_type: str) -> list[str]:
@@ -175,7 +182,7 @@ class ManimDirective(Directive):
175
182
  # Rendering is skipped if the tag skip-manim is present,
176
183
  # or if we are making the pot-files
177
184
  should_skip = (
178
- "skip-manim" in self.state.document.settings.env.app.builder.tags.tags
185
+ "skip-manim" in self.state.document.settings.env.app.builder.tags
179
186
  or self.state.document.settings.env.app.builder.name == "gettext"
180
187
  )
181
188
  if should_skip:
@@ -226,14 +233,10 @@ class ManimDirective(Directive):
226
233
  + self.options.get("ref_functions", [])
227
234
  + self.options.get("ref_methods", [])
228
235
  )
229
- if ref_content:
230
- ref_block = "References: " + " ".join(ref_content)
231
-
232
- else:
233
- ref_block = ""
236
+ ref_block = "References: " + " ".join(ref_content) if ref_content else ""
234
237
 
235
238
  if "quality" in self.options:
236
- quality = f'{self.options["quality"]}_quality'
239
+ quality = f"{self.options['quality']}_quality"
237
240
  else:
238
241
  quality = "example_quality"
239
242
  frame_rate = QUALITIES[quality]["frame_rate"]
@@ -244,13 +247,13 @@ class ManimDirective(Directive):
244
247
  document = state_machine.document
245
248
 
246
249
  source_file_name = Path(document.attributes["source"])
247
- source_rel_name = source_file_name.relative_to(setup.confdir)
250
+ source_rel_name = source_file_name.relative_to(setup.confdir) # type: ignore[attr-defined]
248
251
  source_rel_dir = source_rel_name.parents[0]
249
- dest_dir = Path(setup.app.builder.outdir, source_rel_dir).absolute()
252
+ dest_dir = Path(setup.app.builder.outdir, source_rel_dir).absolute() # type: ignore[attr-defined]
250
253
  if not dest_dir.exists():
251
254
  dest_dir.mkdir(parents=True, exist_ok=True)
252
255
 
253
- source_block = [
256
+ source_block_in = [
254
257
  ".. code-block:: python",
255
258
  "",
256
259
  " from manim import *\n",
@@ -263,13 +266,13 @@ class ManimDirective(Directive):
263
266
  "",
264
267
  " </pre>",
265
268
  ]
266
- source_block = "\n".join(source_block)
269
+ source_block = "\n".join(source_block_in)
267
270
 
268
- config.media_dir = (Path(setup.confdir) / "media").absolute()
271
+ config.media_dir = (Path(setup.confdir) / "media").absolute() # type: ignore[attr-defined,assignment]
269
272
  config.images_dir = "{media_dir}/images"
270
273
  config.video_dir = "{media_dir}/videos/{quality}"
271
274
  output_file = f"{clsname}-{classnamedict[clsname]}"
272
- config.assets_dir = Path("_static")
275
+ config.assets_dir = Path("_static") # type: ignore[assignment]
273
276
  config.progress_bar = "none"
274
277
  config.verbosity = "WARNING"
275
278
 
@@ -287,7 +290,7 @@ class ManimDirective(Directive):
287
290
  if save_as_gif:
288
291
  example_config["format"] = "gif"
289
292
 
290
- user_code = self.content
293
+ user_code = list(self.content)
291
294
  if user_code[0].startswith(">>> "): # check whether block comes from doctest
292
295
  user_code = [
293
296
  line[4:] for line in user_code if line.startswith((">>> ", "... "))
@@ -331,7 +334,7 @@ class ManimDirective(Directive):
331
334
  clsname=clsname,
332
335
  clsname_lowercase=clsname.lower(),
333
336
  hide_source=hide_source,
334
- filesrc_rel=Path(filesrc).relative_to(setup.confdir).as_posix(),
337
+ filesrc_rel=Path(filesrc).relative_to(setup.confdir).as_posix(), # type: ignore[attr-defined]
335
338
  no_autoplay=no_autoplay,
336
339
  output_file=output_file,
337
340
  save_last_frame=save_last_frame,
@@ -350,13 +353,13 @@ class ManimDirective(Directive):
350
353
  rendering_times_file_path = Path("../rendering_times.csv")
351
354
 
352
355
 
353
- def _write_rendering_stats(scene_name: str, run_time: str, file_name: str) -> None:
356
+ def _write_rendering_stats(scene_name: str, run_time: float, file_name: str) -> None:
354
357
  with rendering_times_file_path.open("a") as file:
355
358
  csv.writer(file).writerow(
356
359
  [
357
360
  re.sub(r"^(reference\/)|(manim\.)", "", file_name),
358
361
  scene_name,
359
- "%.3f" % run_time,
362
+ f"{run_time:.3f}",
360
363
  ],
361
364
  )
362
365
 
@@ -374,9 +377,9 @@ def _log_rendering_times(*args: tuple[Any]) -> None:
374
377
  data = [row for row in data if row]
375
378
 
376
379
  max_file_length = max(len(row[0]) for row in data)
377
- for key, group in it.groupby(data, key=lambda row: row[0]):
380
+ for key, group_iter in it.groupby(data, key=lambda row: row[0]):
378
381
  key = key.ljust(max_file_length + 1, ".")
379
- group = list(group)
382
+ group = list(group_iter)
380
383
  if len(group) == 1:
381
384
  row = group[0]
382
385
  print(f"{key}{row[2].rjust(7, '.')}s {row[1]}")
@@ -395,12 +398,12 @@ def _delete_rendering_times(*args: tuple[Any]) -> None:
395
398
  rendering_times_file_path.unlink()
396
399
 
397
400
 
398
- def setup(app: Sphinx) -> dict[str, Any]:
401
+ def setup(app: Sphinx) -> SetupMetadata:
399
402
  app.add_node(SkipManimNode, html=(visit, depart))
400
403
 
401
- setup.app = app
402
- setup.config = app.config
403
- setup.confdir = app.confdir
404
+ setup.app = app # type: ignore[attr-defined]
405
+ setup.config = app.config # type: ignore[attr-defined]
406
+ setup.confdir = app.confdir # type: ignore[attr-defined]
404
407
 
405
408
  app.add_directive("manim", ManimDirective)
406
409
 
@@ -417,7 +420,10 @@ def setup(app: Sphinx) -> dict[str, Any]:
417
420
  ).strip(),
418
421
  )
419
422
 
420
- metadata = {"parallel_read_safe": False, "parallel_write_safe": True}
423
+ metadata: SetupMetadata = {
424
+ "parallel_read_safe": False,
425
+ "parallel_write_safe": True,
426
+ }
421
427
  return metadata
422
428
 
423
429
 
@@ -3,7 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import ast
6
+ import sys
7
+ from ast import Attribute, Name, Subscript
6
8
  from pathlib import Path
9
+ from typing import Any
7
10
 
8
11
  from typing_extensions import TypeAlias
9
12
 
@@ -26,6 +29,10 @@ ModuleLevelAliasDict: TypeAlias = dict[str, AliasCategoryDict]
26
29
  classified by category in different `AliasCategoryDict` objects.
27
30
  """
28
31
 
32
+ ModuleTypeVarDict: TypeAlias = dict[str, str]
33
+ """Dictionary containing every :class:`TypeVar` defined in a module."""
34
+
35
+
29
36
  AliasDocsDict: TypeAlias = dict[str, ModuleLevelAliasDict]
30
37
  """Dictionary which, for every module in Manim, contains documentation
31
38
  about their module-level attributes which are explicitly defined as
@@ -39,17 +46,22 @@ by Sphinx via the ``data`` role, hence the name) which are NOT
39
46
  explicitly defined as :class:`TypeAlias`.
40
47
  """
41
48
 
49
+ TypeVarDict: TypeAlias = dict[str, ModuleTypeVarDict]
50
+ """A dictionary mapping module names to dictionaries of :class:`TypeVar` objects."""
51
+
42
52
  ALIAS_DOCS_DICT: AliasDocsDict = {}
43
53
  DATA_DICT: DataDict = {}
54
+ TYPEVAR_DICT: TypeVarDict = {}
44
55
 
45
56
  MANIM_ROOT = Path(__file__).resolve().parent.parent.parent
46
57
 
47
58
  # In the following, we will use ``type(xyz) is xyz_type`` instead of
48
59
  # isinstance checks to make sure no subclasses of the type pass the
49
60
  # check
61
+ # ruff: noqa: E721
50
62
 
51
63
 
52
- def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
64
+ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict, TypeVarDict]:
53
65
  """Read all files, generate Abstract Syntax Trees from them, and
54
66
  extract useful information about the type aliases defined in the
55
67
  files: the category they belong to, their definition and their
@@ -57,25 +69,30 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
57
69
 
58
70
  Returns
59
71
  -------
60
- ALIAS_DOCS_DICT : `AliasDocsDict`
72
+ ALIAS_DOCS_DICT : :class:`AliasDocsDict`
61
73
  A dictionary containing the information from all the type
62
- aliases in Manim. See `AliasDocsDict` for more information.
74
+ aliases in Manim. See :class:`AliasDocsDict` for more information.
63
75
 
64
- DATA_DICT : `DataDict`
76
+ DATA_DICT : :class:`DataDict`
65
77
  A dictionary containing the names of all DOCUMENTED
66
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.
67
83
  """
68
84
  global ALIAS_DOCS_DICT
69
85
  global DATA_DICT
86
+ global TYPEVAR_DICT
70
87
 
71
- if ALIAS_DOCS_DICT or DATA_DICT:
72
- return ALIAS_DOCS_DICT, DATA_DICT
88
+ if ALIAS_DOCS_DICT or DATA_DICT or TYPEVAR_DICT:
89
+ return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT
73
90
 
74
91
  for module_path in MANIM_ROOT.rglob("*.py"):
75
- module_name = module_path.resolve().relative_to(MANIM_ROOT)
76
- module_name = list(module_name.parts)
77
- module_name[-1] = module_name[-1].removesuffix(".py")
78
- module_name = ".".join(module_name)
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)
79
96
 
80
97
  module_content = module_path.read_text(encoding="utf-8")
81
98
 
@@ -84,6 +101,9 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
84
101
  category_dict: AliasCategoryDict | None = None
85
102
  alias_info: AliasInfo | None = None
86
103
 
104
+ # For storing TypeVars
105
+ module_typevars: ModuleTypeVarDict = {}
106
+
87
107
  # For storing regular module attributes
88
108
  data_list: list[str] = []
89
109
  data_name: str | None = None
@@ -125,8 +145,7 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
125
145
  and (
126
146
  (
127
147
  # if TYPE_CHECKING
128
- type(node.test) is ast.Name
129
- and node.test.id == "TYPE_CHECKING"
148
+ type(node.test) is ast.Name and node.test.id == "TYPE_CHECKING"
130
149
  )
131
150
  or (
132
151
  # if typing.TYPE_CHECKING
@@ -137,32 +156,43 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
137
156
  )
138
157
  )
139
158
  ):
140
- inner_nodes = node.body
159
+ inner_nodes: list[Any] = node.body
141
160
  else:
142
161
  inner_nodes = [node]
143
162
 
144
163
  for node in inner_nodes:
145
- # If we encounter an assignment annotated as "TypeAlias":
146
- if (
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 = (
147
170
  type(node) is ast.AnnAssign
148
171
  and type(node.annotation) is ast.Name
149
172
  and node.annotation.id == "TypeAlias"
150
173
  and type(node.target) is ast.Name
151
174
  and node.value is not None
152
- ):
153
- alias_name = node.target.id
154
- def_node = node.value
155
- # If it's an Union, replace it with vertical bar notation
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".
156
185
  if (
157
- type(def_node) is ast.Subscript
158
- and type(def_node.value) is ast.Name
159
- and def_node.value.id == "Union"
186
+ type(definition_node) is ast.Subscript
187
+ and type(definition_node.value) is ast.Name
188
+ and definition_node.value.id == "Union"
160
189
  ):
190
+ union_elements = definition_node.slice.elts # type: ignore[attr-defined]
161
191
  definition = " | ".join(
162
- ast.unparse(elem) for elem in def_node.slice.elts
192
+ ast.unparse(elem) for elem in union_elements
163
193
  )
164
194
  else:
165
- definition = ast.unparse(def_node)
195
+ definition = ast.unparse(definition_node)
166
196
 
167
197
  definition = definition.replace("npt.", "")
168
198
  if category_dict is None:
@@ -172,6 +202,19 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
172
202
  alias_info = category_dict[alias_name]
173
203
  continue
174
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
+
175
218
  # If here, the node is not a TypeAlias definition
176
219
  alias_info = None
177
220
 
@@ -179,13 +222,15 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
179
222
  # Does the assignment have a target of type Name? Then
180
223
  # it could be considered a definition of a module attribute.
181
224
  if type(node) is ast.AnnAssign:
182
- target = node.target
225
+ target: Name | Attribute | Subscript | ast.expr | None = node.target
183
226
  elif type(node) is ast.Assign and len(node.targets) == 1:
184
227
  target = node.targets[0]
185
228
  else:
186
229
  target = None
187
230
 
188
- if type(target) is ast.Name:
231
+ if type(target) is ast.Name and not (
232
+ type(node) is ast.Assign and target.id not in module_typevars
233
+ ):
189
234
  data_name = target.id
190
235
  else:
191
236
  data_name = None
@@ -194,5 +239,7 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
194
239
  ALIAS_DOCS_DICT[module_name] = module_dict
195
240
  if len(data_list) > 0:
196
241
  DATA_DICT[module_name] = data_list
242
+ if module_typevars:
243
+ TYPEVAR_DICT[module_name] = module_typevars
197
244
 
198
- return ALIAS_DOCS_DICT, DATA_DICT
245
+ return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT
manim/utils/family.py CHANGED
@@ -1,7 +1,7 @@
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
@@ -11,9 +11,9 @@ __all__ = ["extract_mobject_family_members"]
11
11
 
12
12
  def extract_mobject_family_members(
13
13
  mobjects: Iterable[Mobject],
14
- use_z_index=False,
14
+ use_z_index: bool = False,
15
15
  only_those_with_points: bool = False,
16
- ):
16
+ ) -> list[Mobject]:
17
17
  """Returns a list of the types of mobjects and their family members present.
18
18
  A "family" in this context refers to a mobject, its submobjects, and their
19
19
  submobjects, recursively.
manim/utils/family_ops.py CHANGED
@@ -2,20 +2,26 @@ from __future__ import annotations
2
2
 
3
3
  import itertools as it
4
4
 
5
+ from manim.mobject.mobject import Mobject
6
+
5
7
  __all__ = [
6
8
  "extract_mobject_family_members",
7
9
  "restructure_list_to_exclude_certain_family_members",
8
10
  ]
9
11
 
10
12
 
11
- def extract_mobject_family_members(mobject_list, only_those_with_points=False):
13
+ def extract_mobject_family_members(
14
+ mobject_list: list[Mobject], only_those_with_points: bool = False
15
+ ) -> list[Mobject]:
12
16
  result = list(it.chain(*(mob.get_family() for mob in mobject_list)))
13
17
  if only_those_with_points:
14
18
  result = [mob for mob in result if mob.has_points()]
15
19
  return result
16
20
 
17
21
 
18
- 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]:
19
25
  """
20
26
  Removes anything in to_remove from mobject_list, but in the event that one of
21
27
  the items to be removed is a member of the family of an item in mobject_list,
@@ -25,10 +31,12 @@ def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove):
25
31
  but one of its submobjects is removed, e.g. scene.remove(m1), it's useful
26
32
  for the list of mobject_list to be edited to contain other submobjects, but not m1.
27
33
  """
28
- new_list = []
34
+ new_list: list[Mobject] = []
29
35
  to_remove = extract_mobject_family_members(to_remove)
30
36
 
31
- 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:
32
40
  for mob in list_to_examine:
33
41
  if mob in set_to_remove:
34
42
  continue