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
@@ -6,68 +6,192 @@ In particular, this class is what allows ``manim`` to act as ``manim render``.
6
6
  This is a vendored version of https://github.com/click-contrib/click-default-group/
7
7
  under the BSD 3-Clause "New" or "Revised" License.
8
8
 
9
- This library isn't used as a dependency as we need to inherit from ``cloup.Group`` instead
10
- of ``click.Group``.
9
+ This library isn't used as a dependency, as we need to inherit from
10
+ :class:`cloup.Group` instead of :class:`click.Group`.
11
11
  """
12
12
 
13
+ from __future__ import annotations
14
+
13
15
  import warnings
16
+ from typing import TYPE_CHECKING, Any, Callable
14
17
 
15
18
  import cloup
16
19
 
20
+ from manim.utils.deprecation import deprecated
21
+
17
22
  __all__ = ["DefaultGroup"]
18
23
 
24
+ if TYPE_CHECKING:
25
+ from click import Command, Context
26
+
19
27
 
20
28
  class DefaultGroup(cloup.Group):
21
- """Invokes a subcommand marked with ``default=True`` if any subcommand not
29
+ """Invokes a subcommand marked with ``default=True`` if any subcommand is not
22
30
  chosen.
31
+
32
+ Parameters
33
+ ----------
34
+ *args
35
+ Positional arguments to forward to :class:`cloup.Group`.
36
+ **kwargs
37
+ Keyword arguments to forward to :class:`cloup.Group`. The keyword
38
+ ``ignore_unknown_options`` must be set to ``False``.
39
+
40
+ Attributes
41
+ ----------
42
+ default_cmd_name : str | None
43
+ The name of the default command, if specified through the ``default``
44
+ keyword argument. Otherwise, this is set to ``None``.
45
+ default_if_no_args : bool
46
+ Whether to include or not the default command, if no command arguments
47
+ are supplied. This can be specified through the ``default_if_no_args``
48
+ keyword argument. Default is ``False``.
23
49
  """
24
50
 
25
- def __init__(self, *args, **kwargs):
51
+ def __init__(self, *args: Any, **kwargs: Any):
26
52
  # To resolve as the default command.
27
53
  if not kwargs.get("ignore_unknown_options", True):
28
54
  raise ValueError("Default group accepts unknown options")
29
55
  self.ignore_unknown_options = True
30
- self.default_cmd_name = kwargs.pop("default", None)
31
- self.default_if_no_args = kwargs.pop("default_if_no_args", False)
56
+ self.default_cmd_name: str | None = kwargs.pop("default", None)
57
+ self.default_if_no_args: bool = kwargs.pop("default_if_no_args", False)
32
58
  super().__init__(*args, **kwargs)
33
59
 
34
- def set_default_command(self, command):
35
- """Sets a command function as the default command."""
60
+ def set_default_command(self, command: Command) -> None:
61
+ """Sets a command function as the default command.
62
+
63
+ Parameters
64
+ ----------
65
+ command
66
+ The command to set as default.
67
+ """
36
68
  cmd_name = command.name
37
69
  self.add_command(command)
38
70
  self.default_cmd_name = cmd_name
39
71
 
40
- def parse_args(self, ctx, args):
41
- if not args and self.default_if_no_args:
72
+ def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
73
+ """Parses the list of ``args`` by forwarding it to
74
+ :meth:`cloup.Group.parse_args`. Before doing so, if
75
+ :attr:`default_if_no_args` is set to ``True`` and ``args`` is empty,
76
+ this function appends to it the name of the default command specified
77
+ by :attr:`default_cmd_name`.
78
+
79
+ Parameters
80
+ ----------
81
+ ctx
82
+ The Click context.
83
+ args
84
+ A list of arguments. If it's empty and :attr:`default_if_no_args`
85
+ is ``True``, append the name of the default command to it.
86
+
87
+ Returns
88
+ -------
89
+ list[str]
90
+ The parsed arguments.
91
+ """
92
+ if not args and self.default_if_no_args and self.default_cmd_name:
42
93
  args.insert(0, self.default_cmd_name)
43
- return super().parse_args(ctx, args)
44
-
45
- def get_command(self, ctx, cmd_name):
46
- if cmd_name not in self.commands:
94
+ parsed_args: list[str] = super().parse_args(ctx, args)
95
+ return parsed_args
96
+
97
+ def get_command(self, ctx: Context, cmd_name: str) -> Command | None:
98
+ """Get a command function by its name, by forwarding the arguments to
99
+ :meth:`cloup.Group.get_command`. If ``cmd_name`` does not match any of
100
+ the command names in :attr:`commands`, attempt to get the default command
101
+ instead.
102
+
103
+ Parameters
104
+ ----------
105
+ ctx
106
+ The Click context.
107
+ cmd_name
108
+ The name of the command to get.
109
+
110
+ Returns
111
+ -------
112
+ :class:`click.Command` | None
113
+ The command, if found. Otherwise, ``None``.
114
+ """
115
+ if cmd_name not in self.commands and self.default_cmd_name:
47
116
  # No command name matched.
48
- ctx.arg0 = cmd_name
117
+ ctx.meta["arg0"] = cmd_name
49
118
  cmd_name = self.default_cmd_name
50
119
  return super().get_command(ctx, cmd_name)
51
120
 
52
- def resolve_command(self, ctx, args):
53
- base = super()
54
- cmd_name, cmd, args = base.resolve_command(ctx, args)
55
- if hasattr(ctx, "arg0"):
56
- args.insert(0, ctx.arg0)
57
- cmd_name = cmd.name
121
+ def resolve_command(
122
+ self, ctx: Context, args: list[str]
123
+ ) -> tuple[str | None, Command | None, list[str]]:
124
+ """Given a list of ``args`` given by a CLI, find a command which
125
+ matches the first element, and return its name (``cmd_name``), the
126
+ command function itself (``cmd``) and the rest of the arguments which
127
+ shall be passed to the function (``cmd_args``). If not found, return
128
+ ``None``, ``None`` and the rest of the arguments.
129
+
130
+ After resolving the command, if the Click context given by ``ctx``
131
+ contains an ``arg0`` attribute in its :attr:`click.Context.meta`
132
+ dictionary, insert it as the first element of the returned
133
+ ``cmd_args``.
134
+
135
+ Parameters
136
+ ----------
137
+ ctx
138
+ The Click context.
139
+ cmd_name
140
+ The name of the command to get.
141
+
142
+ Returns
143
+ -------
144
+ cmd_name : str | None
145
+ The command name, if found. Otherwise, ``None``.
146
+ cmd : :class:`click.Command` | None
147
+ The command, if found. Otherwise, ``None``.
148
+ cmd_args : list[str]
149
+ The rest of the arguments to be passed to ``cmd``.
150
+ """
151
+ cmd_name, cmd, args = super().resolve_command(ctx, args)
152
+ if "arg0" in ctx.meta:
153
+ args.insert(0, ctx.meta["arg0"])
154
+ if cmd is not None:
155
+ cmd_name = cmd.name
58
156
  return cmd_name, cmd, args
59
157
 
60
- def command(self, *args, **kwargs):
158
+ @deprecated
159
+ def command(
160
+ self, *args: Any, **kwargs: Any
161
+ ) -> Callable[[Callable[..., object]], Command]:
162
+ """Return a decorator which converts any function into the default
163
+ subcommand for this :class:`DefaultGroup`.
164
+
165
+ .. warning::
166
+ This method is deprecated. Use the ``default`` parameter of
167
+ :class:`DefaultGroup` or :meth:`set_default_command` instead.
168
+
169
+ Parameters
170
+ ----------
171
+ *args
172
+ Positional arguments to pass to :meth:`cloup.Group.command`.
173
+ **kwargs
174
+ Keyword arguments to pass to :meth:`cloup.Group.command`.
175
+
176
+ Returns
177
+ -------
178
+ Callable[[Callable[..., object]], click.Command]
179
+ A decorator which transforms its input into this
180
+ :class:`DefaultGroup`'s default subcommand.
181
+ """
61
182
  default = kwargs.pop("default", False)
62
- decorator = super().command(*args, **kwargs)
183
+ decorator: Callable[[Callable[..., object]], Command] = super().command(
184
+ *args, **kwargs
185
+ )
63
186
  if not default:
64
187
  return decorator
65
188
  warnings.warn(
66
189
  "Use default param of DefaultGroup or set_default_command() instead",
67
190
  DeprecationWarning,
191
+ stacklevel=1,
68
192
  )
69
193
 
70
- def _decorator(f):
194
+ def _decorator(f: Callable) -> Command:
71
195
  cmd = decorator(f)
72
196
  self.set_default_command(cmd)
73
197
  return cmd
@@ -10,13 +10,14 @@ from __future__ import annotations
10
10
 
11
11
  import configparser
12
12
  from pathlib import Path
13
+ from typing import Any
13
14
 
14
15
  import click
15
16
  import cloup
16
17
 
17
- from ... import console
18
- from ...constants import CONTEXT_SETTINGS, EPILOG, QUALITIES
19
- from ...utils.file_ops import (
18
+ from manim._config import console
19
+ from manim.constants import CONTEXT_SETTINGS, EPILOG, QUALITIES
20
+ from manim.utils.file_ops import (
20
21
  add_import_statement,
21
22
  copy_template_files,
22
23
  get_template_names,
@@ -34,15 +35,15 @@ CFG_DEFAULTS = {
34
35
  __all__ = ["select_resolution", "update_cfg", "project", "scene"]
35
36
 
36
37
 
37
- def select_resolution():
38
+ def select_resolution() -> tuple[int, int]:
38
39
  """Prompts input of type click.Choice from user. Presents options from QUALITIES constant.
39
40
 
40
41
  Returns
41
42
  -------
42
- :class:`tuple`
43
- Tuple containing height and width.
43
+ tuple[int, int]
44
+ Tuple containing height and width.
44
45
  """
45
- resolution_options = []
46
+ resolution_options: list[tuple[int, int]] = []
46
47
  for quality in QUALITIES.items():
47
48
  resolution_options.append(
48
49
  (quality[1]["pixel_height"], quality[1]["pixel_width"]),
@@ -54,18 +55,21 @@ def select_resolution():
54
55
  show_default=False,
55
56
  default="480p",
56
57
  )
57
- return [res for res in resolution_options if f"{res[0]}p" == choice][0]
58
+ matches = [res for res in resolution_options if f"{res[0]}p" == choice]
59
+ return matches[0]
58
60
 
59
61
 
60
- def update_cfg(cfg_dict: dict, project_cfg_path: Path):
61
- """Updates the manim.cfg file after reading it from the project_cfg_path.
62
+ def update_cfg(cfg_dict: dict[str, Any], project_cfg_path: Path) -> None:
63
+ """Update the ``manim.cfg`` file after reading it from the specified
64
+ ``project_cfg_path``.
62
65
 
63
66
  Parameters
64
67
  ----------
65
68
  cfg_dict
66
- values used to update manim.cfg found project_cfg_path.
69
+ Values used to update ``manim.cfg`` which is found in
70
+ ``project_cfg_path``.
67
71
  project_cfg_path
68
- Path of manim.cfg file.
72
+ Path of the ``manim.cfg`` file.
69
73
  """
70
74
  config = configparser.ConfigParser()
71
75
  config.read(project_cfg_path)
@@ -85,7 +89,7 @@ def update_cfg(cfg_dict: dict, project_cfg_path: Path):
85
89
  context_settings=CONTEXT_SETTINGS,
86
90
  epilog=EPILOG,
87
91
  )
88
- @cloup.argument("project_name", type=Path, required=False)
92
+ @cloup.argument("project_name", type=cloup.Path(path_type=Path), required=False)
89
93
  @cloup.option(
90
94
  "-d",
91
95
  "--default",
@@ -94,13 +98,14 @@ def update_cfg(cfg_dict: dict, project_cfg_path: Path):
94
98
  help="Default settings for project creation.",
95
99
  nargs=1,
96
100
  )
97
- def project(default_settings, **args):
101
+ def project(default_settings: bool, **kwargs: Any) -> None:
98
102
  """Creates a new project.
99
103
 
100
104
  PROJECT_NAME is the name of the folder in which the new project will be initialized.
101
105
  """
102
- if args["project_name"]:
103
- project_name = args["project_name"]
106
+ project_name: Path
107
+ if kwargs["project_name"]:
108
+ project_name = kwargs["project_name"]
104
109
  else:
105
110
  project_name = click.prompt("Project Name", type=Path)
106
111
 
@@ -117,7 +122,7 @@ def project(default_settings, **args):
117
122
  )
118
123
  else:
119
124
  project_name.mkdir()
120
- new_cfg = {}
125
+ new_cfg: dict[str, Any] = {}
121
126
  new_cfg_path = Path.resolve(project_name / "manim.cfg")
122
127
 
123
128
  if not default_settings:
@@ -145,23 +150,23 @@ def project(default_settings, **args):
145
150
  )
146
151
  @cloup.argument("scene_name", type=str, required=True)
147
152
  @cloup.argument("file_name", type=str, required=False)
148
- def scene(**args):
153
+ def scene(**kwargs: Any) -> None:
149
154
  """Inserts a SCENE to an existing FILE or creates a new FILE.
150
155
 
151
156
  SCENE is the name of the scene that will be inserted.
152
157
 
153
158
  FILE is the name of file in which the SCENE will be inserted.
154
159
  """
155
- template_name = click.prompt(
160
+ template_name: str = click.prompt(
156
161
  "template",
157
162
  type=click.Choice(get_template_names(), False),
158
163
  default="Default",
159
164
  )
160
165
  scene = (get_template_path() / f"{template_name}.mtp").resolve().read_text()
161
- scene = scene.replace(template_name + "Template", args["scene_name"], 1)
166
+ scene = scene.replace(template_name + "Template", kwargs["scene_name"], 1)
162
167
 
163
- if args["file_name"]:
164
- file_name = Path(args["file_name"])
168
+ if kwargs["file_name"]:
169
+ file_name = Path(kwargs["file_name"])
165
170
 
166
171
  if file_name.suffix != ".py":
167
172
  file_name = file_name.with_suffix(file_name.suffix + ".py")
@@ -190,7 +195,7 @@ def scene(**args):
190
195
  help="Create a new project or insert a new scene.",
191
196
  )
192
197
  @cloup.pass_context
193
- def init(ctx):
198
+ def init(ctx: cloup.Context) -> None:
194
199
  pass
195
200
 
196
201
 
@@ -10,8 +10,8 @@ from __future__ import annotations
10
10
 
11
11
  import cloup
12
12
 
13
- from ...constants import CONTEXT_SETTINGS, EPILOG
14
- from ...plugins.plugins_flags import list_plugins
13
+ from manim.constants import CONTEXT_SETTINGS, EPILOG
14
+ from manim.plugins.plugins_flags import list_plugins
15
15
 
16
16
  __all__ = ["plugins"]
17
17
 
@@ -29,6 +29,16 @@ __all__ = ["plugins"]
29
29
  is_flag=True,
30
30
  help="List available plugins.",
31
31
  )
32
- def plugins(list_available):
32
+ def plugins(list_available: bool) -> None:
33
+ """Print a list of all available plugins when calling ``manim plugins -l``
34
+ or ``manim plugins --list``.
35
+
36
+ Parameters
37
+ ----------
38
+ list_available
39
+ If the ``-l`` or ``-list`` option is passed to ``manim plugins``, this
40
+ parameter will be set to ``True``, which will print a list of all
41
+ available plugins.
42
+ """
33
43
  if list_available:
34
44
  list_plugins()
@@ -13,78 +13,83 @@ import json
13
13
  import sys
14
14
  import urllib.error
15
15
  import urllib.request
16
+ from argparse import Namespace
16
17
  from pathlib import Path
17
- from typing import cast
18
+ from typing import Any, cast
18
19
 
19
20
  import cloup
20
21
 
21
- from ... import __version__, config, console, error_console, logger
22
- from ..._config import tempconfig
23
- from ...constants import EPILOG, RendererType
24
- from ...utils.module_ops import scene_classes_from_file
25
- from .ease_of_access_options import ease_of_access_options
26
- from .global_options import global_options
27
- from .output_options import output_options
28
- from .render_options import render_options
22
+ from manim import __version__
23
+ from manim._config import (
24
+ config,
25
+ console,
26
+ error_console,
27
+ logger,
28
+ tempconfig,
29
+ )
30
+ from manim.cli.render.ease_of_access_options import ease_of_access_options
31
+ from manim.cli.render.global_options import global_options
32
+ from manim.cli.render.output_options import output_options
33
+ from manim.cli.render.render_options import render_options
34
+ from manim.constants import EPILOG, RendererType
35
+ from manim.utils.module_ops import scene_classes_from_file
29
36
 
30
37
  __all__ = ["render"]
31
38
 
32
39
 
40
+ class ClickArgs(Namespace):
41
+ def __init__(self, args: dict[str, Any]) -> None:
42
+ for name in args:
43
+ setattr(self, name, args[name])
44
+
45
+ def _get_kwargs(self) -> list[tuple[str, Any]]:
46
+ return list(self.__dict__.items())
47
+
48
+ def __eq__(self, other: object) -> bool:
49
+ if not isinstance(other, ClickArgs):
50
+ return NotImplemented
51
+ return vars(self) == vars(other)
52
+
53
+ def __contains__(self, key: str) -> bool:
54
+ return key in self.__dict__
55
+
56
+ def __repr__(self) -> str:
57
+ return str(self.__dict__)
58
+
59
+
33
60
  @cloup.command(
34
61
  context_settings=None,
35
62
  no_args_is_help=True,
36
63
  epilog=EPILOG,
37
64
  )
38
- @cloup.argument("file", type=Path, required=True)
65
+ @cloup.argument("file", type=cloup.Path(path_type=Path), required=True)
39
66
  @cloup.argument("scene_names", required=False, nargs=-1)
40
67
  @global_options
41
68
  @output_options
42
- @render_options # type: ignore
69
+ @render_options
43
70
  @ease_of_access_options
44
- def render(
45
- **args,
46
- ):
71
+ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
47
72
  """Render SCENE(S) from the input FILE.
48
73
 
49
74
  FILE is the file path of the script or a config file.
50
75
 
51
76
  SCENES is an optional list of scenes in the file.
52
77
  """
53
-
54
- if args["save_as_gif"]:
78
+ if kwargs["save_as_gif"]:
55
79
  logger.warning("--save_as_gif is deprecated, please use --format=gif instead!")
56
- args["format"] = "gif"
80
+ kwargs["format"] = "gif"
57
81
 
58
- if args["save_pngs"]:
82
+ if kwargs["save_pngs"]:
59
83
  logger.warning("--save_pngs is deprecated, please use --format=png instead!")
60
- args["format"] = "png"
84
+ kwargs["format"] = "png"
61
85
 
62
- if args["show_in_file_browser"]:
86
+ if kwargs["show_in_file_browser"]:
63
87
  logger.warning(
64
88
  "The short form of show_in_file_browser is deprecated and will be moved to support --format.",
65
89
  )
66
90
 
67
- class ClickArgs:
68
- def __init__(self, args):
69
- for name in args:
70
- setattr(self, name, args[name])
71
-
72
- def _get_kwargs(self):
73
- return list(self.__dict__.items())
74
-
75
- def __eq__(self, other):
76
- if not isinstance(other, ClickArgs):
77
- return NotImplemented
78
- return vars(self) == vars(other)
79
-
80
- def __contains__(self, key):
81
- return key in self.__dict__
82
-
83
- def __repr__(self):
84
- return str(self.__dict__)
85
-
86
- click_args = ClickArgs(args)
87
- if args["jupyter"]:
91
+ click_args = ClickArgs(kwargs)
92
+ if kwargs["jupyter"]:
88
93
  return click_args
89
94
 
90
95
  config.digest_args(click_args)
@@ -153,4 +158,4 @@ def render(
153
158
  "You should consider upgrading via [yellow]pip install -U manim[/yellow]",
154
159
  )
155
160
 
156
- return args
161
+ return kwargs
@@ -1,22 +1,56 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import re
5
+ import sys
6
+ from typing import TYPE_CHECKING
4
7
 
5
8
  from cloup import Choice, option, option_group
6
9
 
7
- from ... import logger
10
+ if TYPE_CHECKING:
11
+ from click import Context, Option
8
12
 
9
13
  __all__ = ["global_options"]
10
14
 
15
+ logger = logging.getLogger("manim")
11
16
 
12
- def validate_gui_location(ctx, param, value):
13
- if value:
14
- try:
15
- x_offset, y_offset = map(int, re.split(r"[;,\-]", value))
16
- return (x_offset, y_offset)
17
- except Exception:
18
- logger.error("GUI location option is invalid.")
19
- exit()
17
+
18
+ def validate_gui_location(
19
+ ctx: Context, param: Option, value: str | None
20
+ ) -> tuple[int, int] | None:
21
+ """If the ``value`` string is given, extract from it the GUI location,
22
+ which should be in any of these formats: 'x;y', 'x,y' or 'x-y'.
23
+
24
+ Parameters
25
+ ----------
26
+ ctx
27
+ The Click context.
28
+ param
29
+ A Click option.
30
+ value
31
+ The optional string which will be parsed.
32
+
33
+ Returns
34
+ -------
35
+ tuple[int, int] | None
36
+ If ``value`` is ``None``, the return value is ``None``. Otherwise, it's
37
+ the ``(x, y)`` location for the GUI.
38
+
39
+ Raises
40
+ ------
41
+ ValueError
42
+ If ``value`` has an invalid format.
43
+ """
44
+ if value is None:
45
+ return None
46
+
47
+ try:
48
+ x_offset, y_offset = map(int, re.split(r"[;,\-]", value))
49
+ except Exception:
50
+ logger.error("GUI location option is invalid.")
51
+ sys.exit()
52
+
53
+ return (x_offset, y_offset)
20
54
 
21
55
 
22
56
  global_options = option_group(