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?