A common problem I run into when coding in Python in Maya, is the ability to undo: Sometimes, and often this happens when issuing button commands via a UI, when the user wants to undo the last operation, it will undo
ever step the function took in the operation. For example, if you had a function that iterated over a loop making spheres, if you wanted to undo this execution, you'd have to undo for every sphere created, rather than undo the whole operation at once.
I've found the way to make this behave properly is wrap the executed commands inside an
undoInfo
open\close chunk call, and inside that wrapper the code execution in try\finally clause: You need to make sure that no matter what happens, even if the command fails for some reason, you can get out and close your undo chunk.
At first I'd embed this
undoInfo
\try\finally clause directly in the function being called. But if there were many functions evoking this concept, this could lead to a lot of duplicated code. Enter the Python
decorator.
I consider decorators 'convenience wrappers' (that's how I think of them in my head) for your functions. I have docs explaining their usage and creation over on my Python Wiki
here.
In the below example, I create a Python class that will act as our decorator code. The great thing about Python and passing arguments to parameters, is that via
*args
and
**kwargs
our decorator will intercept all incoming arguments and pass them along properly to the function.
When the decorator runs, it first opens and undo chunk, then tries to execute the decorated function, catching any exceptions it may throw.
When it finishes, it closes the undo chunk, and if there was an exception, it raises it.
# python code
import maya.cmds as mc
class Undo(object):
"""
Create an undo decorator
"""
def __init__(self, f):
# Called when the Undo decorator is created
self.f = f
def __call__(self, *args, **kwargs):
# Called when the decorated function is executed
mc.undoInfo(openChunk=True)
try:
self.f(*args, **kwargs)
finally:
mc.undoInfo(closeChunk=True)
@Undo # This wraps the below function in the decorator
def sphere(rad=3):
# Some function that may have a problem undoing.
# This code shouldn't, just used as placeholder example.
mc.polySphere(radius=rad)
# Run our code. We can now easily undo it if we don't like the results.
sphere()
(try
\except
clauses cleaned up to try
\finally
thanks to suggestion Keir Rice)The irony of the above system is that I authored it to work with button-presses, but the
above system can't wrapper a class-based method with
@Undo
, it will get angry (due to the passing of
self
to the method..
Here is a different approach using a function-based decorator, rather than a class-based one that will work on class methods. Note how we use the
functools.wraps
as a decorator
inside our decorator? Chaos!
from functools import wraps
import maya.cmds as mc
def undo(fn):
"""
Create an undo decorator
"""
@wraps(fn)
def wrapper(*args, **kwargs):
mc.undoInfo(openChunk=True)
try:
fn(*args, **kwargs)
finally:
mc.undoInfo(closeChunk=True)
return wrapper
# Make an arbitrary class, with a method to decorate:
class Foo(object):
@undo # This wraps the below method in the decorator
def sphere(self, rad=3):
mc.polySphere(radius=rad)
# Now call to our method:
f = Foo()
f.sphere()
Also see: