Python 'undo' decorator does not raise exception in Maya 2017

I wrote a decorator that I was using to wrap function calls in undoInfo chunks. The decorator works correctly in Maya 2015. In Maya 2017, when the decorator raises an exception, the custom UI (PySide2 used) that called the decorated function locks up and the exception is not raised. If I run any Python command in the script editor or command line, the exception is then raised correctly the UI becomes responsive. It’s as though the exception gets trapped in the UI and I have to ‘kick’ the Python event loop to release it.

Here’s the decorator (I named the Python file ‘undo_decorator.py’ for importing later in this example):

from functools import wraps
from maya import cmds

def undo_func(func):
    @wraps(func)
    def func_wrapper(*args, **kwargs):
        cmds.undoInfo(openChunk=True, chunkName=func.__name__)
        try:
            result = func(*args, **kwargs)
            cmds.undoInfo(closeChunk=True)
            return result
        except:
            cmds.undoInfo(closeChunk=True)
            if cmds.undoInfo(query=True, undoName=True) == func.__name__:
                cmds.undo()
            raise  # this doesn't raise the exception
    return func_wrapper

In Maya create a nurbsSphere. Lock the translates for this example.

cmds.nurbsSphere()
cmds.setAttr('nurbsSphere1.translate', lock=True)

Here’s an example usage (Maya 2017, PySide2). It assumes that you have a nurbs sphere in the scene named ‘nurbsSphere1’ with its translate channels locked to invoke the exception.

from PySide2 import QtCore, QtWidgets
from shiboken2 import wrapInstance

from maya import OpenMayaUI
from maya import cmds

import undo_decorator
my_undo = undo_decorator.undo_func


WIN = None

def maya_main_window():
    ptr = OpenMayaUI.MQtUtil.mainWindow()
    return wrapInstance(long(ptr), QtWidgets.QWidget)


def main():
    global WIN
    if not WIN:
        WIN = BaseWindow(maya_main_window())
        WIN.showNormal()
    else:
        WIN.activateWindow()
        WIN.showNormal()

    return WIN


class BaseWindow(QtWidgets.QWidget):
    def __init__(self, parent):
        super(BaseWindow, self).__init__(parent)

        self.setWindowFlags(QtCore.Qt.Window)
        self.setWindowTitle('Base Window')

        self.init_ui()

    def init_ui(self):
        # layout
        main_layout = QtWidgets.QVBoxLayout()
        self.setLayout(main_layout)

        # buttons
        class_btn = QtWidgets.QPushButton("Class Method")
        class_btn.clicked.connect(cls_inst)
        main_layout.addWidget(class_btn)

        func_btn = QtWidgets.QPushButton("Function")
        func_btn.clicked.connect(move_it_callback)
        main_layout.addWidget(func_btn)

def cls_inst():
    fc = TestClass()
    fc.move_it('nurbsSphere1')


class TestClass(object):
    @my_undo
    def move_it(self, obj):
        """A test method."""
        cmds.polyTorus()  # something to prove that undo is working
        cmds.setAttr('{}.translate'.format(obj), *(0, 0, 10))


@my_undo
def move_it(obj):
    """A test function."""
    cmds.polyTorus()  # something to prove that undo is working
    cmds.setAttr('{}.translate'.format(obj), *(0, 0, 10))


def move_it_callback():
    move_it('nurbsSphere1')

With ‘nurbsSphere1’ in a scene and its translate channels locked, launch the UI:

ui = main()

When either button is pressed, the decorator works and performs an undo (cmds.polyTorus() is undone), but the exception is not raised and the UI locks up.

What is the best practice for raising an exception from a decorator in Maya 2017 with PySide2? Is there a special technique to it?

1 Like

It sound almost like the signal is getting called outside of the main thread, which always has weird and unusual results.

Try wrapping your call to move_it_callback inside maya.utils.executeDeferred, like func_btn.clicked.connect(maya.utils.executeDeferred(move_it_callback)).

Thanks for your suggestion. Very much appreciated.

Your suggestion unfortunately executes the function instead of connecting it to the button, but I understood what you were going for, so I used lamba with executeDeferred for the button signal connection to my function and it works now.

Corrected code:

class_btn.clicked.connect(lambda: utils.executeDeferred(cls_inst))
func_btn.clicked.connect(lambda: utils.executeDeferred(move_it_callback))

I’m guessing functools.partial could work as well. I am also wondering if I can fix it somehow using the undo_decorator. Unless the problem is solely with the Pyside2 UI. I will do some more testing. Thanks again.

1 Like

Oh yeah good catch. Was clearly asleep at the keyboard yesterday.

functools.partial should work in place of a lambda.

I think this properly combines the undo chunk stuff with executeDeferred, but only tested it briefly in the script editor, not from a gui or anything.

import functools

from maya import cmds
from maya.utils import executeDeferred

def undo_chunk(func):
    @functools.wraps(func)
    def wrap(*args, **kwargs):
        try:
            cmds.undoInfo(openChunk=True)
            return func(*args, **kwargs)
        finally:
            cmds.undoInfo(closeChunk=True)
    return lambda *args, **kwargs: executeDeferred(wrap, *args, **kwargs)
2 Likes

Nice one. That did it. Works in the gui.
You just saved me a lot of editing. Cheers!