Skip to content

Commit e73b659

Browse files
Add classes MethodWithArgs, SceneInteractContinue and SceneInteractRerun inside new module manim.data_structures (#4315)
* Add classes MethodWithArgs, SceneInteractRerun and SceneInteractExit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add missing manim.data_structures file * Rename SceneInteractExit to SceneInteractContinue and use dataclasses * Revert using @DataClass(slots=True), because Python 3.9 does not support it * Change order of dataclasses * Add references to Scene.queue in docstrings * Include data_structures in utilities_misc.rst * Use qualified names and mark strings as code in data_structures.py * Add module docstring to data_structures.py * Move Scene.interact() objects from data_structures.py into scene.py * Use qualified name in SceneInteractAction docstring --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent fd8ab62 commit e73b659

File tree

6 files changed

+120
-46
lines changed

6 files changed

+120
-46
lines changed

docs/source/reference_index/utilities_misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Module Index
1515
~utils.commands
1616
~utils.config_ops
1717
constants
18+
data_structures
1819
~utils.debug
1920
~utils.deprecation
2021
~utils.docbuild

manim/animation/transform.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import numpy as np
3535

36+
from manim.data_structures import MethodWithArgs
3637
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
3738

3839
from .. import config
@@ -438,13 +439,13 @@ def check_validity_of_input(self, mobject: Mobject) -> None:
438439

439440

440441
class _MethodAnimation(MoveToTarget):
441-
def __init__(self, mobject, methods):
442+
def __init__(self, mobject: Mobject, methods: list[MethodWithArgs]) -> None:
442443
self.methods = methods
443444
super().__init__(mobject)
444445

445446
def finish(self) -> None:
446-
for method, method_args, method_kwargs in self.methods:
447-
method.__func__(self.mobject, *method_args, **method_kwargs)
447+
for item in self.methods:
448+
item.method.__func__(self.mobject, *item.args, **item.kwargs)
448449
super().finish()
449450

450451

manim/data_structures.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Data classes and other necessary data structures for use in Manim."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Iterable
6+
from dataclasses import dataclass
7+
from types import MethodType
8+
from typing import Any
9+
10+
11+
@dataclass
12+
class MethodWithArgs:
13+
"""Object containing a :attr:`method` which is intended to be called later
14+
with the positional arguments :attr:`args` and the keyword arguments
15+
:attr:`kwargs`.
16+
17+
Attributes
18+
----------
19+
method : MethodType
20+
A callable representing a method of some class.
21+
args : Iterable[Any]
22+
Positional arguments for :attr:`method`.
23+
kwargs : dict[str, Any]
24+
Keyword arguments for :attr:`method`.
25+
"""
26+
27+
__slots__ = ["method", "args", "kwargs"]
28+
29+
method: MethodType
30+
args: Iterable[Any]
31+
kwargs: dict[str, Any]

manim/mobject/mobject.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import numpy as np
2323

24+
from manim.data_structures import MethodWithArgs
2425
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
2526

2627
from .. import config, logger
@@ -3232,7 +3233,7 @@ def __init__(self, mobject) -> None:
32323233

32333234
self.overridden_animation = None
32343235
self.is_chaining = False
3235-
self.methods = []
3236+
self.methods: list[MethodWithArgs] = []
32363237

32373238
# Whether animation args can be passed
32383239
self.cannot_pass_args = False
@@ -3267,7 +3268,7 @@ def update_target(*method_args, **method_kwargs):
32673268
**method_kwargs,
32683269
)
32693270
else:
3270-
self.methods.append([method, method_args, method_kwargs])
3271+
self.methods.append(MethodWithArgs(method, method_args, method_kwargs))
32713272
method(*method_args, **method_kwargs)
32723273
return self
32733274

manim/mobject/opengl/opengl_mobject.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from manim import config, logger
1818
from manim.constants import *
19+
from manim.data_structures import MethodWithArgs
1920
from manim.renderer.shader_wrapper import get_colormap_code
2021
from manim.utils.bezier import integer_interpolate, interpolate
2122
from manim.utils.color import (
@@ -2938,7 +2939,7 @@ def __init__(self, mobject: OpenGLMobject):
29382939

29392940
self.overridden_animation = None
29402941
self.is_chaining = False
2941-
self.methods = []
2942+
self.methods: list[MethodWithArgs] = []
29422943

29432944
# Whether animation args can be passed
29442945
self.cannot_pass_args = False
@@ -2973,7 +2974,7 @@ def update_target(*method_args, **method_kwargs):
29732974
**method_kwargs,
29742975
)
29752976
else:
2976-
self.methods.append([method, method_args, method_kwargs])
2977+
self.methods.append(MethodWithArgs(method, method_args, method_kwargs))
29772978
method(*method_args, **method_kwargs)
29782979
return self
29792980

manim/scene/scene.py

Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import random
1616
import threading
1717
import time
18+
from dataclasses import dataclass
1819
from pathlib import Path
1920
from queue import Queue
2021

@@ -38,6 +39,7 @@
3839
from watchdog.observers import Observer
3940

4041
from manim import __version__
42+
from manim.data_structures import MethodWithArgs
4143
from manim.mobject.mobject import Mobject
4244
from manim.mobject.opengl.opengl_mobject import OpenGLPoint
4345

@@ -59,28 +61,70 @@
5961
if TYPE_CHECKING:
6062
from collections.abc import Iterable, Sequence
6163
from types import FrameType
62-
from typing import Any, Callable, TypeAlias
64+
from typing import Any, Callable, TypeAlias, Union
6365

6466
from typing_extensions import Self
6567

6668
from manim.typing import Point3D
6769

68-
SceneInteractAction: TypeAlias = tuple[str, Iterable[Any], dict[str, Any]]
70+
SceneInteractAction: TypeAlias = Union[
71+
MethodWithArgs, "SceneInteractContinue", "SceneInteractRerun"
72+
]
73+
"""The SceneInteractAction type alias is used for elements in the queue
74+
used by :meth:`.Scene.interact()`.
75+
76+
The elements can be one of the following three:
77+
78+
- a :class:`~.MethodWithArgs` object, which represents a :class:`Scene`
79+
method to be called along with its args and kwargs,
80+
- a :class:`~.SceneInteractContinue` object, indicating that the scene
81+
interaction is over and the scene will continue rendering after that, or
82+
- a :class:`~.SceneInteractRerun` object, indicating that the scene should
83+
render again.
6984
"""
70-
The SceneInteractAction type alias is used for elements in the queue
71-
used by Scene.interact().
72-
The elements consist consist of:
73-
74-
- a string, which is either the name of a Scene method or some special keyword
75-
starting with "rerun" or "exit",
76-
- a list of args for the Scene method (only used if the first string actually
77-
corresponds to a method) and
78-
- a dict of kwargs for the Scene method (if the first string corresponds to one.
79-
Otherwise, currently Scene.interact() extracts a possible "from_animation_number" from it if the first string starts with "rerun"),
80-
as seen around the source code where it's common to use self.queue.put((method_name, [], {})) and similar items.
8185

86+
87+
@dataclass
88+
class SceneInteractContinue:
89+
"""Object which, when encountered in :meth:`.Scene.interact`, triggers
90+
the end of the scene interaction, continuing with the rest of the
91+
animations, if any. This object can be queued in :attr:`.Scene.queue`
92+
for later use in :meth:`.Scene.interact`.
93+
94+
Attributes
95+
----------
96+
sender : str
97+
The name of the entity which issued the end of the scene interaction,
98+
such as ``"gui"`` or ``"keyboard"``.
8299
"""
83100

101+
__slots__ = ["sender"]
102+
103+
sender: str
104+
105+
106+
class SceneInteractRerun:
107+
"""Object which, when encountered in :meth:`.Scene.interact`, triggers
108+
the rerun of the scene. This object can be queued in :attr:`.Scene.queue`
109+
for later use in :meth:`.Scene.interact`.
110+
111+
Attributes
112+
----------
113+
sender : str
114+
The name of the entity which issued the rerun of the scene, such as
115+
``"gui"``, ``"keyboard"``, ``"play"`` or ``"file"``.
116+
kwargs : dict[str, Any]
117+
Additional keyword arguments when rerunning the scene. Currently,
118+
only ``"from_animation_number"`` is being used, which determines the
119+
animation from which to start rerunning the scene.
120+
"""
121+
122+
__slots__ = ["sender", "kwargs"]
123+
124+
def __init__(self, sender: str, **kwargs: Any) -> None:
125+
self.sender = sender
126+
self.kwargs = kwargs
127+
84128

85129
class RerunSceneHandler(FileSystemEventHandler):
86130
"""A class to handle rerunning a Scene after the input file is modified."""
@@ -90,7 +134,7 @@ def __init__(self, queue: Queue[SceneInteractAction]) -> None:
90134
self.queue = queue
91135

92136
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
93-
self.queue.put(("rerun_file", [], {}))
137+
self.queue.put(SceneInteractRerun("file"))
94138

95139

96140
class Scene:
@@ -1106,20 +1150,15 @@ def play(
11061150
and config.renderer == RendererType.OPENGL
11071151
and threading.current_thread().name != "MainThread"
11081152
):
1153+
# TODO: are these actually being used?
11091154
kwargs.update(
11101155
{
11111156
"subcaption": subcaption,
11121157
"subcaption_duration": subcaption_duration,
11131158
"subcaption_offset": subcaption_offset,
11141159
}
11151160
)
1116-
self.queue.put(
1117-
(
1118-
"play",
1119-
args,
1120-
kwargs,
1121-
)
1122-
)
1161+
self.queue.put(SceneInteractRerun("play", **kwargs))
11231162
return
11241163

11251164
start_time = self.time
@@ -1363,17 +1402,19 @@ def load_module_into_namespace(
13631402
load_module_into_namespace(manim.opengl, namespace)
13641403

13651404
def embedded_rerun(*args: Any, **kwargs: Any) -> None:
1366-
self.queue.put(("rerun_keyboard", args, kwargs))
1405+
self.queue.put(SceneInteractRerun("keyboard"))
13671406
shell.exiter()
13681407

13691408
namespace["rerun"] = embedded_rerun
13701409

13711410
shell(local_ns=namespace)
1372-
self.queue.put(("exit_keyboard", [], {}))
1411+
self.queue.put(SceneInteractContinue("keyboard"))
13731412

13741413
def get_embedded_method(method_name: str) -> Callable[..., None]:
1414+
method = getattr(self, method_name)
1415+
13751416
def embedded_method(*args: Any, **kwargs: Any) -> None:
1376-
self.queue.put((method_name, args, kwargs))
1417+
self.queue.put(MethodWithArgs(method, args, kwargs))
13771418

13781419
return embedded_method
13791420

@@ -1437,34 +1478,33 @@ def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None:
14371478
last_time = time.time()
14381479
while not (self.renderer.window.is_closing or self.quit_interaction):
14391480
if not self.queue.empty():
1440-
tup = self.queue.get_nowait()
1441-
if tup[0].startswith("rerun"):
1481+
action = self.queue.get_nowait()
1482+
if isinstance(action, SceneInteractRerun):
14421483
# Intentionally skip calling join() on the file thread to save time.
1443-
if not tup[0].endswith("keyboard"):
1484+
if action.sender != "keyboard":
14441485
if shell.pt_app:
14451486
shell.pt_app.app.exit(exception=EOFError)
14461487
file_observer.unschedule_all()
14471488
raise RerunSceneException
14481489
keyboard_thread.join()
14491490

1450-
kwargs = tup[2]
1451-
if "from_animation_number" in kwargs:
1452-
config["from_animation_number"] = kwargs[
1491+
if "from_animation_number" in action.kwargs:
1492+
config["from_animation_number"] = action.kwargs[
14531493
"from_animation_number"
14541494
]
14551495
# # TODO: This option only makes sense if interactive_embed() is run at the
14561496
# # end of a scene by default.
1457-
# if "upto_animation_number" in kwargs:
1458-
# config["upto_animation_number"] = kwargs[
1497+
# if "upto_animation_number" in action.kwargs:
1498+
# config["upto_animation_number"] = action.kwargs[
14591499
# "upto_animation_number"
14601500
# ]
14611501

14621502
keyboard_thread.join()
14631503
file_observer.unschedule_all()
14641504
raise RerunSceneException
1465-
elif tup[0].startswith("exit"):
1505+
elif isinstance(action, SceneInteractContinue):
14661506
# Intentionally skip calling join() on the file thread to save time.
1467-
if not tup[0].endswith("keyboard") and shell.pt_app:
1507+
if action.sender != "keyboard" and shell.pt_app:
14681508
shell.pt_app.app.exit(exception=EOFError)
14691509
keyboard_thread.join()
14701510
# Remove exit_keyboard from the queue if necessary.
@@ -1473,8 +1513,7 @@ def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None:
14731513
keyboard_thread_needs_join = False
14741514
break
14751515
else:
1476-
method, args, kwargs = tup
1477-
getattr(self, method)(*args, **kwargs)
1516+
action.method(*action.args, **action.kwargs)
14781517
else:
14791518
self.renderer.animation_start_time = 0
14801519
dt = time.time() - last_time
@@ -1561,14 +1600,14 @@ def _configure_pygui(self, update: bool = True) -> None:
15611600
dpg.set_viewport_height(540)
15621601

15631602
def rerun_callback(sender: Any, data: Any) -> None:
1564-
self.queue.put(("rerun_gui", [], {}))
1603+
self.queue.put(SceneInteractRerun("gui"))
15651604

15661605
def continue_callback(sender: Any, data: Any) -> None:
1567-
self.queue.put(("exit_gui", [], {}))
1606+
self.queue.put(SceneInteractContinue("gui"))
15681607

15691608
def scene_selection_callback(sender: Any, data: Any) -> None:
15701609
config["scene_names"] = (dpg.get_value(sender),)
1571-
self.queue.put(("rerun_gui", [], {}))
1610+
self.queue.put(SceneInteractRerun("gui"))
15721611

15731612
scene_classes = scene_classes_from_file(
15741613
Path(config["input_file"]), full_list=True

0 commit comments

Comments
 (0)