Skip to content

Commit 0d67dcd

Browse files
Fix assertions and improve error messages when adding submobjects (#3756)
* Optimized AnimationGroup computation of start-end times with lag ratio * Added extra comment for init_run_time * Added full path to imports in composition.py * Optimized AnimationGroup.interpolate * Fixed final bugs * Removed accidental print * Final fix to AnimationGroup.interpolate * Fixed animations being skipped unintentionally * Fix and improve Mobject assertions when adding submobjects * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update examples in Mobject.add() and OpenGLMobject.add() docstrings * overriden -> overridden * Joined string in OpenGLMobject error message * Address requested changes * OpenGLVMObjects -> OpenGLVMobjects * Use tuplify in VGroup.__setitem__() --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7399585 commit 0d67dcd

File tree

8 files changed

+404
-82
lines changed

8 files changed

+404
-82
lines changed

manim/mobject/mobject.py

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,63 @@ def __init__(
116116
self.generate_points()
117117
self.init_colors()
118118

119+
def _assert_valid_submobjects(self, submobjects: Iterable[Mobject]) -> Self:
120+
"""Check that all submobjects are actually instances of
121+
:class:`Mobject`, and that none of them is ``self`` (a
122+
:class:`Mobject` cannot contain itself).
123+
124+
This is an auxiliary function called when adding Mobjects to the
125+
:attr:`submobjects` list.
126+
127+
This function is intended to be overridden by subclasses such as
128+
:class:`VMobject`, which should assert that only other VMobjects
129+
may be added into it.
130+
131+
Parameters
132+
----------
133+
submobjects
134+
The list containing values to validate.
135+
136+
Returns
137+
-------
138+
:class:`Mobject`
139+
The Mobject itself.
140+
141+
Raises
142+
------
143+
TypeError
144+
If any of the values in `submobjects` is not a :class:`Mobject`.
145+
ValueError
146+
If there was an attempt to add a :class:`Mobject` as its own
147+
submobject.
148+
"""
149+
return self._assert_valid_submobjects_internal(submobjects, Mobject)
150+
151+
def _assert_valid_submobjects_internal(
152+
self, submobjects: list[Mobject], mob_class: type[Mobject]
153+
) -> Self:
154+
for i, submob in enumerate(submobjects):
155+
if not isinstance(submob, mob_class):
156+
error_message = (
157+
f"Only values of type {mob_class.__name__} can be added "
158+
f"as submobjects of {type(self).__name__}, but the value "
159+
f"{submob} (at index {i}) is of type "
160+
f"{type(submob).__name__}."
161+
)
162+
# Intended for subclasses such as VMobject, which
163+
# cannot have regular Mobjects as submobjects
164+
if isinstance(submob, Mobject):
165+
error_message += (
166+
" You can try adding this value into a Group instead."
167+
)
168+
raise TypeError(error_message)
169+
if submob is self:
170+
raise ValueError(
171+
f"Cannot add {type(self).__name__} as a submobject of "
172+
f"itself (at index {i})."
173+
)
174+
return self
175+
119176
@classmethod
120177
def animation_override_for(
121178
cls,
@@ -414,12 +471,19 @@ def add(self, *mobjects: Mobject) -> Self:
414471
>>> len(outer.submobjects)
415472
1
416473
474+
Only Mobjects can be added::
475+
476+
>>> outer.add(3)
477+
Traceback (most recent call last):
478+
...
479+
TypeError: Only values of type Mobject can be added as submobjects of Mobject, but the value 3 (at index 0) is of type int.
480+
417481
Adding an object to itself raises an error::
418482
419483
>>> outer.add(outer)
420484
Traceback (most recent call last):
421485
...
422-
ValueError: Mobject cannot contain self
486+
ValueError: Cannot add Mobject as a submobject of itself (at index 0).
423487
424488
A given mobject cannot be added as a submobject
425489
twice to some parent::
@@ -433,12 +497,7 @@ def add(self, *mobjects: Mobject) -> Self:
433497
[child]
434498
435499
"""
436-
for m in mobjects:
437-
if not isinstance(m, Mobject):
438-
raise TypeError("All submobjects must be of type Mobject")
439-
if m is self:
440-
raise ValueError("Mobject cannot contain self")
441-
500+
self._assert_valid_submobjects(mobjects)
442501
unique_mobjects = remove_list_redundancies(mobjects)
443502
if len(mobjects) != len(unique_mobjects):
444503
logger.warning(
@@ -464,10 +523,7 @@ def insert(self, index: int, mobject: Mobject) -> None:
464523
mobject
465524
The mobject to be inserted.
466525
"""
467-
if not isinstance(mobject, Mobject):
468-
raise TypeError("All submobjects must be of type Mobject")
469-
if mobject is self:
470-
raise ValueError("Mobject cannot contain self")
526+
self._assert_valid_submobjects([mobject])
471527
self.submobjects.insert(index, mobject)
472528

473529
def __add__(self, mobject: Mobject):
@@ -520,13 +576,7 @@ def add_to_back(self, *mobjects: Mobject) -> Self:
520576
:meth:`add`
521577
522578
"""
523-
if self in mobjects:
524-
raise ValueError("A mobject shouldn't contain itself")
525-
526-
for mobject in mobjects:
527-
if not isinstance(mobject, Mobject):
528-
raise TypeError("All submobjects must be of type Mobject")
529-
579+
self._assert_valid_submobjects(mobjects)
530580
self.remove(*mobjects)
531581
# dict.fromkeys() removes duplicates while maintaining order
532582
self.submobjects = list(dict.fromkeys(mobjects)) + self.submobjects

manim/mobject/opengl/opengl_mobject.py

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,64 @@ def __init__(
165165

166166
self.should_render = should_render
167167

168+
def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLMobject]) -> Self:
169+
"""Check that all submobjects are actually instances of
170+
:class:`OpenGLMobject`, and that none of them is
171+
``self`` (an :class:`OpenGLMobject` cannot contain itself).
172+
173+
This is an auxiliary function called when adding OpenGLMobjects to the
174+
:attr:`submobjects` list.
175+
176+
This function is intended to be overridden by subclasses such as
177+
:class:`OpenGLVMobject`, which should assert that only other
178+
OpenGLVMobjects may be added into it.
179+
180+
Parameters
181+
----------
182+
submobjects
183+
The list containing values to validate.
184+
185+
Returns
186+
-------
187+
:class:`OpenGLMobject`
188+
The OpenGLMobject itself.
189+
190+
Raises
191+
------
192+
TypeError
193+
If any of the values in `submobjects` is not an
194+
:class:`OpenGLMobject`.
195+
ValueError
196+
If there was an attempt to add an :class:`OpenGLMobject` as its own
197+
submobject.
198+
"""
199+
return self._assert_valid_submobjects_internal(submobjects, OpenGLMobject)
200+
201+
def _assert_valid_submobjects_internal(
202+
self, submobjects: list[OpenGLMobject], mob_class: type[OpenGLMobject]
203+
) -> Self:
204+
for i, submob in enumerate(submobjects):
205+
if not isinstance(submob, mob_class):
206+
error_message = (
207+
f"Only values of type {mob_class.__name__} can be added "
208+
f"as submobjects of {type(self).__name__}, but the value "
209+
f"{submob} (at index {i}) is of type "
210+
f"{type(submob).__name__}."
211+
)
212+
# Intended for subclasses such as OpenGLVMobject, which
213+
# cannot have regular OpenGLMobjects as submobjects
214+
if isinstance(submob, OpenGLMobject):
215+
error_message += (
216+
" You can try adding this value into a Group instead."
217+
)
218+
raise TypeError(error_message)
219+
if submob is self:
220+
raise ValueError(
221+
f"Cannot add {type(self).__name__} as a submobject of "
222+
f"itself (at index {i})."
223+
)
224+
return self
225+
168226
@classmethod
169227
def __init_subclass__(cls, **kwargs):
170228
super().__init_subclass__(**kwargs)
@@ -734,28 +792,33 @@ def add(
734792
>>> len(outer.submobjects)
735793
1
736794
795+
Only OpenGLMobjects can be added::
796+
797+
>>> outer.add(3)
798+
Traceback (most recent call last):
799+
...
800+
TypeError: Only values of type OpenGLMobject can be added as submobjects of OpenGLMobject, but the value 3 (at index 0) is of type int.
801+
737802
Adding an object to itself raises an error::
738803
739804
>>> outer.add(outer)
740805
Traceback (most recent call last):
741806
...
742-
ValueError: OpenGLMobject cannot contain self
807+
ValueError: Cannot add OpenGLMobject as a submobject of itself (at index 0).
743808
744809
"""
745810
if update_parent:
746811
assert len(mobjects) == 1, "Can't set multiple parents."
747812
mobjects[0].parent = self
748813

749-
if self in mobjects:
750-
raise ValueError("OpenGLMobject cannot contain self")
814+
self._assert_valid_submobjects(mobjects)
815+
751816
if any(mobjects.count(elem) > 1 for elem in mobjects):
752817
logger.warning(
753818
"Attempted adding some Mobject as a child more than once, "
754819
"this is not possible. Repetitions are ignored.",
755820
)
756821
for mobject in mobjects:
757-
if not isinstance(mobject, OpenGLMobject):
758-
raise TypeError("All submobjects must be of type OpenGLMobject")
759822
if mobject not in self.submobjects:
760823
self.submobjects.append(mobject)
761824
if self not in mobject.parents:
@@ -784,11 +847,7 @@ def insert(self, index: int, mobject: OpenGLMobject, update_parent: bool = False
784847
if update_parent:
785848
mobject.parent = self
786849

787-
if mobject is self:
788-
raise ValueError("OpenGLMobject cannot contain self")
789-
790-
if not isinstance(mobject, OpenGLMobject):
791-
raise TypeError("All submobjects must be of type OpenGLMobject")
850+
self._assert_valid_submobjects([mobject])
792851

793852
if mobject not in self.submobjects:
794853
self.submobjects.insert(index, mobject)
@@ -880,10 +939,12 @@ def add_to_back(self, *mobjects: OpenGLMobject) -> OpenGLMobject:
880939
:meth:`add`
881940
882941
"""
942+
self._assert_valid_submobjects(mobjects)
883943
self.submobjects = list_update(mobjects, self.submobjects)
884944
return self
885945

886946
def replace_submobject(self, index, new_submob):
947+
self._assert_valid_submobjects([new_submob])
887948
old_submob = self.submobjects[index]
888949
if self in old_submob.parents:
889950
old_submob.parents.remove(self)
@@ -2734,8 +2795,6 @@ def throw_error_if_no_points(self):
27342795

27352796
class OpenGLGroup(OpenGLMobject):
27362797
def __init__(self, *mobjects, **kwargs):
2737-
if not all(isinstance(m, OpenGLMobject) for m in mobjects):
2738-
raise Exception("All submobjects must be of type OpenGLMobject")
27392798
super().__init__(**kwargs)
27402799
self.add(*mobjects)
27412800

manim/mobject/opengl/opengl_vectorized_mobject.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
)
2626
from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
2727
from manim.utils.config_ops import _Data
28-
from manim.utils.iterables import listify, make_even, resize_with_interpolation
28+
from manim.utils.iterables import make_even, resize_with_interpolation, tuplify
2929
from manim.utils.space_ops import (
3030
angle_between_vectors,
3131
cross2d,
@@ -161,6 +161,9 @@ def __init__(
161161
if stroke_color is not None:
162162
self.stroke_color = ManimColor.parse(stroke_color)
163163

164+
def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLVMobject]) -> Self:
165+
return self._assert_valid_submobjects_internal(submobjects, OpenGLVMobject)
166+
164167
def get_group_class(self):
165168
return OpenGLVGroup
166169

@@ -266,7 +269,7 @@ def set_stroke(
266269

267270
if width is not None:
268271
for mob in self.get_family(recurse):
269-
mob.stroke_width = np.array([[width] for width in listify(width)])
272+
mob.stroke_width = np.array([[width] for width in tuplify(width)])
270273

271274
if background is not None:
272275
for mob in self.get_family(recurse):
@@ -1659,8 +1662,6 @@ def construct(self):
16591662
"""
16601663

16611664
def __init__(self, *vmobjects, **kwargs):
1662-
if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
1663-
raise Exception("All submobjects must be of type OpenGLVMobject")
16641665
super().__init__(**kwargs)
16651666
self.add(*vmobjects)
16661667

@@ -1726,8 +1727,6 @@ def construct(self):
17261727
(gr-circle_red).animate.shift(RIGHT)
17271728
)
17281729
"""
1729-
if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
1730-
raise TypeError("All submobjects must be of type OpenGLVMobject")
17311730
return super().add(*vmobjects)
17321731

17331732
def __add__(self, vmobject):
@@ -1773,8 +1772,7 @@ def __setitem__(self, key: int, value: OpenGLVMobject | Sequence[OpenGLVMobject]
17731772
17741773
>>> config.renderer = original_renderer
17751774
"""
1776-
if not all(isinstance(m, OpenGLVMobject) for m in value):
1777-
raise TypeError("All submobjects must be of type OpenGLVMobject")
1775+
self._assert_valid_submobjects(tuplify(value))
17781776
self.submobjects[key] = value
17791777

17801778

manim/mobject/types/vectorized_mobject.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ def __init__(
172172
if stroke_color is not None:
173173
self.stroke_color = ManimColor.parse(stroke_color)
174174

175+
def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
176+
return self._assert_valid_submobjects_internal(submobjects, VMobject)
177+
175178
# OpenGL compatibility
176179
@property
177180
def n_points_per_curve(self) -> int:
@@ -2016,14 +2019,6 @@ def construct(self):
20162019
(gr-circle_red).animate.shift(RIGHT)
20172020
)
20182021
"""
2019-
for m in vmobjects:
2020-
if not isinstance(m, (VMobject, OpenGLVMobject)):
2021-
raise TypeError(
2022-
f"All submobjects of {self.__class__.__name__} must be of type VMobject. "
2023-
f"Got {repr(m)} ({type(m).__name__}) instead. "
2024-
"You can try using `Group` instead."
2025-
)
2026-
20272022
return super().add(*vmobjects)
20282023

20292024
def __add__(self, vmobject: VMobject) -> Self:
@@ -2061,8 +2056,7 @@ def __setitem__(self, key: int, value: VMobject | Sequence[VMobject]) -> None:
20612056
>>> new_obj = VMobject()
20622057
>>> vgroup[0] = new_obj
20632058
"""
2064-
if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in value):
2065-
raise TypeError("All submobjects must be of type VMobject")
2059+
self._assert_valid_submobjects(tuplify(value))
20662060
self.submobjects[key] = value
20672061

20682062

@@ -2390,8 +2384,7 @@ def add_key_value_pair(self, key: Hashable, value: VMobject) -> None:
23902384
self.add_key_value_pair('s', square_obj)
23912385
23922386
"""
2393-
if not isinstance(value, (VMobject, OpenGLVMobject)):
2394-
raise TypeError("All submobjects must be of type VMobject")
2387+
self._assert_valid_submobjects([value])
23952388
mob = value
23962389
if self.show_keys:
23972390
# This import is here and not at the top to avoid circular import

tests/module/mobject/mobject/test_mobject.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,26 @@ def test_mobject_add():
2626

2727
# check that Mobject.add() returns the Mobject (for chained calls)
2828
assert obj.add(Mobject()) is obj
29+
assert len(obj.submobjects) == 13
30+
2931
obj = Mobject()
3032

3133
# a Mobject cannot contain itself
32-
with pytest.raises(ValueError):
33-
obj.add(obj)
34+
with pytest.raises(ValueError) as add_self_info:
35+
obj.add(Mobject(), obj, Mobject())
36+
assert str(add_self_info.value) == (
37+
"Cannot add Mobject as a submobject of itself (at index 1)."
38+
)
39+
assert len(obj.submobjects) == 0
3440

3541
# can only add Mobjects
36-
with pytest.raises(TypeError):
37-
obj.add("foo")
42+
with pytest.raises(TypeError) as add_str_info:
43+
obj.add(Mobject(), Mobject(), "foo")
44+
assert str(add_str_info.value) == (
45+
"Only values of type Mobject can be added as submobjects of Mobject, "
46+
"but the value foo (at index 2) is of type str."
47+
)
48+
assert len(obj.submobjects) == 0
3849

3950

4051
def test_mobject_remove():

0 commit comments

Comments
 (0)