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>
This commit is contained in:
Francisco Manríquez Novoa 2025-07-24 18:37:14 -04:00 committed by GitHub
commit e73b6590dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 120 additions and 46 deletions

View file

@ -15,6 +15,7 @@ Module Index
~utils.commands
~utils.config_ops
constants
data_structures
~utils.debug
~utils.deprecation
~utils.docbuild

View file

@ -33,6 +33,7 @@ from typing import TYPE_CHECKING, Any, Callable
import numpy as np
from manim.data_structures import MethodWithArgs
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
from .. import config
@ -438,13 +439,13 @@ class MoveToTarget(Transform):
class _MethodAnimation(MoveToTarget):
def __init__(self, mobject, methods):
def __init__(self, mobject: Mobject, methods: list[MethodWithArgs]) -> None:
self.methods = methods
super().__init__(mobject)
def finish(self) -> None:
for method, method_args, method_kwargs in self.methods:
method.__func__(self.mobject, *method_args, **method_kwargs)
for item in self.methods:
item.method.__func__(self.mobject, *item.args, **item.kwargs)
super().finish()

31
manim/data_structures.py Normal file
View file

@ -0,0 +1,31 @@
"""Data classes and other necessary data structures for use in Manim."""
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
from types import MethodType
from typing import Any
@dataclass
class MethodWithArgs:
"""Object containing a :attr:`method` which is intended to be called later
with the positional arguments :attr:`args` and the keyword arguments
:attr:`kwargs`.
Attributes
----------
method : MethodType
A callable representing a method of some class.
args : Iterable[Any]
Positional arguments for :attr:`method`.
kwargs : dict[str, Any]
Keyword arguments for :attr:`method`.
"""
__slots__ = ["method", "args", "kwargs"]
method: MethodType
args: Iterable[Any]
kwargs: dict[str, Any]

View file

@ -21,6 +21,7 @@ from typing import TYPE_CHECKING
import numpy as np
from manim.data_structures import MethodWithArgs
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from .. import config, logger
@ -3232,7 +3233,7 @@ class _AnimationBuilder:
self.overridden_animation = None
self.is_chaining = False
self.methods = []
self.methods: list[MethodWithArgs] = []
# Whether animation args can be passed
self.cannot_pass_args = False
@ -3267,7 +3268,7 @@ class _AnimationBuilder:
**method_kwargs,
)
else:
self.methods.append([method, method_args, method_kwargs])
self.methods.append(MethodWithArgs(method, method_args, method_kwargs))
method(*method_args, **method_kwargs)
return self

View file

@ -16,6 +16,7 @@ import numpy as np
from manim import config, logger
from manim.constants import *
from manim.data_structures import MethodWithArgs
from manim.renderer.shader_wrapper import get_colormap_code
from manim.utils.bezier import integer_interpolate, interpolate
from manim.utils.color import (
@ -2938,7 +2939,7 @@ class _AnimationBuilder:
self.overridden_animation = None
self.is_chaining = False
self.methods = []
self.methods: list[MethodWithArgs] = []
# Whether animation args can be passed
self.cannot_pass_args = False
@ -2973,7 +2974,7 @@ class _AnimationBuilder:
**method_kwargs,
)
else:
self.methods.append([method, method_args, method_kwargs])
self.methods.append(MethodWithArgs(method, method_args, method_kwargs))
method(*method_args, **method_kwargs)
return self

View file

@ -15,6 +15,7 @@ import platform
import random
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from queue import Queue
@ -38,6 +39,7 @@ from watchdog.events import DirModifiedEvent, FileModifiedEvent, FileSystemEvent
from watchdog.observers import Observer
from manim import __version__
from manim.data_structures import MethodWithArgs
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_mobject import OpenGLPoint
@ -59,29 +61,71 @@ from ..utils.module_ops import scene_classes_from_file
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from types import FrameType
from typing import Any, Callable, TypeAlias
from typing import Any, Callable, TypeAlias, Union
from typing_extensions import Self
from manim.typing import Point3D
SceneInteractAction: TypeAlias = tuple[str, Iterable[Any], dict[str, Any]]
"""
The SceneInteractAction type alias is used for elements in the queue
used by Scene.interact().
The elements consist consist of:
SceneInteractAction: TypeAlias = Union[
MethodWithArgs, "SceneInteractContinue", "SceneInteractRerun"
]
"""The SceneInteractAction type alias is used for elements in the queue
used by :meth:`.Scene.interact()`.
- a string, which is either the name of a Scene method or some special keyword
starting with "rerun" or "exit",
- a list of args for the Scene method (only used if the first string actually
corresponds to a method) and
- a dict of kwargs for the Scene method (if the first string corresponds to one.
Otherwise, currently Scene.interact() extracts a possible "from_animation_number" from it if the first string starts with "rerun"),
as seen around the source code where it's common to use self.queue.put((method_name, [], {})) and similar items.
The elements can be one of the following three:
- a :class:`~.MethodWithArgs` object, which represents a :class:`Scene`
method to be called along with its args and kwargs,
- a :class:`~.SceneInteractContinue` object, indicating that the scene
interaction is over and the scene will continue rendering after that, or
- a :class:`~.SceneInteractRerun` object, indicating that the scene should
render again.
"""
@dataclass
class SceneInteractContinue:
"""Object which, when encountered in :meth:`.Scene.interact`, triggers
the end of the scene interaction, continuing with the rest of the
animations, if any. This object can be queued in :attr:`.Scene.queue`
for later use in :meth:`.Scene.interact`.
Attributes
----------
sender : str
The name of the entity which issued the end of the scene interaction,
such as ``"gui"`` or ``"keyboard"``.
"""
__slots__ = ["sender"]
sender: str
class SceneInteractRerun:
"""Object which, when encountered in :meth:`.Scene.interact`, triggers
the rerun of the scene. This object can be queued in :attr:`.Scene.queue`
for later use in :meth:`.Scene.interact`.
Attributes
----------
sender : str
The name of the entity which issued the rerun of the scene, such as
``"gui"``, ``"keyboard"``, ``"play"`` or ``"file"``.
kwargs : dict[str, Any]
Additional keyword arguments when rerunning the scene. Currently,
only ``"from_animation_number"`` is being used, which determines the
animation from which to start rerunning the scene.
"""
__slots__ = ["sender", "kwargs"]
def __init__(self, sender: str, **kwargs: Any) -> None:
self.sender = sender
self.kwargs = kwargs
class RerunSceneHandler(FileSystemEventHandler):
"""A class to handle rerunning a Scene after the input file is modified."""
@ -90,7 +134,7 @@ class RerunSceneHandler(FileSystemEventHandler):
self.queue = queue
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
self.queue.put(("rerun_file", [], {}))
self.queue.put(SceneInteractRerun("file"))
class Scene:
@ -1106,6 +1150,7 @@ class Scene:
and config.renderer == RendererType.OPENGL
and threading.current_thread().name != "MainThread"
):
# TODO: are these actually being used?
kwargs.update(
{
"subcaption": subcaption,
@ -1113,13 +1158,7 @@ class Scene:
"subcaption_offset": subcaption_offset,
}
)
self.queue.put(
(
"play",
args,
kwargs,
)
)
self.queue.put(SceneInteractRerun("play", **kwargs))
return
start_time = self.time
@ -1363,17 +1402,19 @@ class Scene:
load_module_into_namespace(manim.opengl, namespace)
def embedded_rerun(*args: Any, **kwargs: Any) -> None:
self.queue.put(("rerun_keyboard", args, kwargs))
self.queue.put(SceneInteractRerun("keyboard"))
shell.exiter()
namespace["rerun"] = embedded_rerun
shell(local_ns=namespace)
self.queue.put(("exit_keyboard", [], {}))
self.queue.put(SceneInteractContinue("keyboard"))
def get_embedded_method(method_name: str) -> Callable[..., None]:
method = getattr(self, method_name)
def embedded_method(*args: Any, **kwargs: Any) -> None:
self.queue.put((method_name, args, kwargs))
self.queue.put(MethodWithArgs(method, args, kwargs))
return embedded_method
@ -1437,34 +1478,33 @@ class Scene:
last_time = time.time()
while not (self.renderer.window.is_closing or self.quit_interaction):
if not self.queue.empty():
tup = self.queue.get_nowait()
if tup[0].startswith("rerun"):
action = self.queue.get_nowait()
if isinstance(action, SceneInteractRerun):
# Intentionally skip calling join() on the file thread to save time.
if not tup[0].endswith("keyboard"):
if action.sender != "keyboard":
if shell.pt_app:
shell.pt_app.app.exit(exception=EOFError)
file_observer.unschedule_all()
raise RerunSceneException
keyboard_thread.join()
kwargs = tup[2]
if "from_animation_number" in kwargs:
config["from_animation_number"] = kwargs[
if "from_animation_number" in action.kwargs:
config["from_animation_number"] = action.kwargs[
"from_animation_number"
]
# # TODO: This option only makes sense if interactive_embed() is run at the
# # end of a scene by default.
# if "upto_animation_number" in kwargs:
# config["upto_animation_number"] = kwargs[
# if "upto_animation_number" in action.kwargs:
# config["upto_animation_number"] = action.kwargs[
# "upto_animation_number"
# ]
keyboard_thread.join()
file_observer.unschedule_all()
raise RerunSceneException
elif tup[0].startswith("exit"):
elif isinstance(action, SceneInteractContinue):
# Intentionally skip calling join() on the file thread to save time.
if not tup[0].endswith("keyboard") and shell.pt_app:
if action.sender != "keyboard" and shell.pt_app:
shell.pt_app.app.exit(exception=EOFError)
keyboard_thread.join()
# Remove exit_keyboard from the queue if necessary.
@ -1473,8 +1513,7 @@ class Scene:
keyboard_thread_needs_join = False
break
else:
method, args, kwargs = tup
getattr(self, method)(*args, **kwargs)
action.method(*action.args, **action.kwargs)
else:
self.renderer.animation_start_time = 0
dt = time.time() - last_time
@ -1561,14 +1600,14 @@ class Scene:
dpg.set_viewport_height(540)
def rerun_callback(sender: Any, data: Any) -> None:
self.queue.put(("rerun_gui", [], {}))
self.queue.put(SceneInteractRerun("gui"))
def continue_callback(sender: Any, data: Any) -> None:
self.queue.put(("exit_gui", [], {}))
self.queue.put(SceneInteractContinue("gui"))
def scene_selection_callback(sender: Any, data: Any) -> None:
config["scene_names"] = (dpg.get_value(sender),)
self.queue.put(("rerun_gui", [], {}))
self.queue.put(SceneInteractRerun("gui"))
scene_classes = scene_classes_from_file(
Path(config["input_file"]), full_list=True