Skip to content

Commit a7ff7bb

Browse files
authored
New Feature: Add generic set method and compatibility layer between properties and get_*/set_* methods (#995)
* Add generic set method, compatibilty layer between properties and get_* and set_* methods * Fix doctest * Add docs for the compatibility layer, make sure set_* methods from the layer are indeed methods * Add newline after :: * Add DeprecationWarning to the compatibility layer, make DeprecationWarnings show
1 parent 554f829 commit a7ff7bb

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

manim/mobject/mobject.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import operator as op
1111
import random
1212
import sys
13+
import types
14+
import warnings
1315

1416
from pathlib import Path
1517
from colour import Color
@@ -34,6 +36,10 @@
3436
class Mobject(Container):
3537
"""Mathematical Object: base class for objects that can be displayed on screen.
3638
39+
There is a compatibility layer that allows for
40+
getting and setting generic attributes with ``get_*``
41+
and ``set_*`` methods. See :meth:`set` for more details.
42+
3743
Attributes
3844
----------
3945
submobjects : :class:`list`
@@ -248,6 +254,106 @@ def __sub__(self, other):
248254
def __isub__(self, other):
249255
raise NotImplementedError
250256

257+
def set(self, **kwargs):
258+
"""Sets attributes.
259+
260+
Mainly to be used along with :attr:`animate` to
261+
animate setting attributes.
262+
263+
In addition to this method, there is a compatibility
264+
layer that allows ``get_*`` and ``set_*`` methods to
265+
get and set generic attributes. For instance::
266+
267+
>>> mob = Mobject()
268+
>>> mob.set_foo(0)
269+
Mobject
270+
>>> mob.get_foo()
271+
0
272+
>>> mob.foo
273+
0
274+
275+
This compatibility layer does not interfere with any
276+
``get_*`` or ``set_*`` methods that are explicitly
277+
defined.
278+
279+
.. warning::
280+
281+
This compatibility layer is for backwards compatibility
282+
and is not guaranteed to stay around. Where applicable,
283+
please prefer getting/setting attributes normally or with
284+
the :meth:`set` method.
285+
286+
Parameters
287+
----------
288+
**kwargs
289+
The attributes and corresponding values to set.
290+
291+
Returns
292+
-------
293+
:class:`Mobject`
294+
``self``
295+
296+
Examples
297+
--------
298+
::
299+
300+
>>> mob = Mobject()
301+
>>> mob.set(foo=0)
302+
Mobject
303+
>>> mob.foo
304+
0
305+
"""
306+
307+
for attr, value in kwargs.items():
308+
setattr(self, attr, value)
309+
310+
return self
311+
312+
def __getattr__(self, attr):
313+
# Add automatic compatibility layer
314+
# between properties and get_* and set_*
315+
# methods.
316+
#
317+
# In python 3.9+ we could change this
318+
# logic to use str.remove_prefix instead.
319+
320+
if attr.startswith("get_"):
321+
# Remove the "get_" prefix
322+
to_get = attr[4:]
323+
324+
def getter(self):
325+
warnings.warn(
326+
"This method is not guaranteed to stay around. Please prefer getting the attribute normally.",
327+
DeprecationWarning,
328+
stacklevel=2,
329+
)
330+
331+
return getattr(self, to_get)
332+
333+
# Return a bound method
334+
return types.MethodType(getter, self)
335+
336+
if attr.startswith("set_"):
337+
# Remove the "set_" prefix
338+
to_set = attr[4:]
339+
340+
def setter(self, value):
341+
warnings.warn(
342+
"This method is not guaranteed to stay around. Please prefer setting the attribute normally or with Mobject.set().",
343+
DeprecationWarning,
344+
stacklevel=2,
345+
)
346+
347+
setattr(self, to_set, value)
348+
349+
return self
350+
351+
# Return a bound method
352+
return types.MethodType(setter, self)
353+
354+
# Unhandled attribute, therefore error
355+
raise AttributeError
356+
251357
def get_array_attrs(self):
252358
return ["points"]
253359

manim/utils/module_ops.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import types
88
import re
9+
import warnings
910

1011

1112
def get_module(file_name: Path):
@@ -33,6 +34,11 @@ def get_module(file_name: Path):
3334
if ext != ".py":
3435
raise ValueError(f"{file_name} is not a valid Manim python script.")
3536
module_name = ext.replace(os.sep, ".").split(".")[-1]
37+
38+
warnings.filterwarnings(
39+
"default", category=DeprecationWarning, module=module_name
40+
)
41+
3642
spec = importlib.util.spec_from_file_location(module_name, file_name)
3743
module = importlib.util.module_from_spec(spec)
3844
sys.modules[module_name] = module

tests/test_get_set.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import types
2+
import pytest
3+
4+
from manim.mobject.mobject import Mobject
5+
6+
7+
def test_generic_set():
8+
m = Mobject()
9+
m.set(test=0)
10+
11+
assert m.test == 0
12+
13+
14+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
15+
def test_get_compat_layer():
16+
m = Mobject()
17+
18+
assert isinstance(m.get_test, types.MethodType)
19+
with pytest.raises(AttributeError):
20+
m.get_test()
21+
22+
m.test = 0
23+
assert m.get_test() == 0
24+
25+
26+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
27+
def test_set_compat_layer():
28+
m = Mobject()
29+
30+
assert isinstance(m.set_test, types.MethodType)
31+
m.set_test(0)
32+
33+
assert m.test == 0
34+
35+
36+
def test_nonexistent_attr():
37+
m = Mobject()
38+
39+
with pytest.raises(AttributeError):
40+
m.test

0 commit comments

Comments
 (0)