28 August 2011

Python in Maya - How to make commands repeatable and undoable

Two of the problems you may have noticed using python in Maya are:

1) that your function calls are not repeatable with the "G" hotkey the way they are with MEL commands and shelf buttons.

2) python functions are not by default undoable with a single undo. If you make 10 connections between 10 nodes, you will need to undo 10 times to get back to the state your scene was in before you ran the function.

One way you can get around this is to build a decorator function to wrap your regular functions and add that functionality to them. So that when you are done, it will look like:


@undoable
@repeatable
def myAwesomeFunction(valueA='totalyWickedMode'):
    ....doAwesomeStuff


The nice thing about the format of the code above is that it is easy to add or remove from any function, and we can edit the functionality of these two decorators in 1 place. So lets define our decorators starting with the easiest of the two, undoable.




def undoable(function):
    '''A decorator that will make commands undoable in maya'''


    def decoratorCode(*args, **kwargs):
        cmds.undoInfo(openChunk=True)
        functionReturn = None
        try:
            functionReturn = function(*args, **kwargs)
           
        except:
            print sys.exc_info()[1]

        finally:
            cmds.undoInfo(closeChunk=True)
            return functionReturn
           
    return decoratorCode




The above function will automaticly be called in place of the function its decorating (in this case myAwesomeFunction), passing as a variable the function being called (again, in this case it is myAwesomeFunction). Now the function inside the function 'undoable', can be named whatever you want, and it will be the function collecting our arguments and keyword arguments (if there are any). In this inner function is where we can openChunck in maya's undoInfo (which defines the start of our undo "chunk"). Then, we run the original function in a try statement, followed by a finally statement so that we can close the undo chunk regardless of whether or not our original command produces and error. Now if we run myAwesomeFunction() it should undo the whole function in a single undo.


Next is the somewhat tricky problem of making our functions repeatable. The reason this is harder is because we have to use a command "cmds.repeatLast()" that is undocumented. luckily if you do a help() on the mel version of the command, we can see the list of flags for this command. The two flags we will need are: "ac" and "acl". 
"ac" being the command (which even tho we are using the python version of repeat last, we will need to feed a MEL command to it".
"acl" is just the label of the command, in this case we will label it with the name of our command.




def repeatable(function):
    '''A decorator that will make commands repeatable in maya'''
    def decoratorCode(*args, **kwargs):
        functionReturn = None
        argString = ''
        if args:
            for each in args:
                argString += str(each)+', '
               
        if kwargs:
            for key, item in kwargs.iteritems():
                argString += str(key)+'='+str(item)+', '

        commandToRepeat = 'python("'+__name__+'.'+function.__name__+'('+argString+')")'
       
        functionReturn = function(*args, **kwargs)
        try:
            cmds.repeatLast(ac=commandToRepeat, acl=function.__name__)
        except:
            pass
       
        return functionReturn
       
    return decoratorCode





The main difference here is that we need to construct our command into a mel command, understanding that our python command could be named anything (because many different commands will be using this decorator) . When we are done, our command string should look like (in this case):

python("awesomeTools.myAwesomeFunction(valueA=\"totalyWickedMode\"")

So the first thing we do is to turn our arguments and keyword arguments into a string in the same format as we would type to call it. Then we need to find the name of the module that contains this function (assuming we are not defining it in the script editor), the __name__ attribute in this case will give us that info, as it is inherited from the module. Now we just need the name of the function being decorated, again, we can use the __name__ attribute of the function (function.__name__) to get that info. Now we just stick them all together, and feed it as the mel command arg in cmds.repeatLast().


Hope this answers more questions than it creates for you,
Cheers,
Kris Andrews

6 comments:

  1. Have used this previously. but having problems using it from within a class


    # Error: line 1: invalid syntax
    # File "", line 2
    # __main__.db_edgeLoopDeleteMirror(<__main__.db_Tools object at 0x90b8090>, )
    # ^
    # SyntaxError: invalid syntax #


    any ideas?



    ReplyDelete
    Replies
    1. Yea I have run into this issue as well. Typically in python you can not use decorators on methods in classes without making them static methods. It may be possible but I couldn't find any easy way to do it. This limitation can be crippling if your method needs access to self.
      If your method does not need access to self, it should be possible. For most cases for myself, I just stick to functions for smaller tools, and use global variables for any information they might want to share (this may not be possible depending on what your trying to do).

      Delete
    2. Hi there!

      I just found a more generic approach that should work in any case, even with classes, objects etc. I use a global variable to hold the function and the args/kwargs. In this case, the function, args and kwargs can be of anything and it would still work.



      _function_to_repeat = None
      _args = None
      _kwargs = None


      def _do_repeat():
      if _function_to_repeat is not None:
      _function_to_repeat(*_args, **_kwargs)


      def repeatable(function):
      """
      A decorator that will make commands repeatable in maya.
      """
      def decoratorCode(*args, **kwargs):
      global _function_to_repeat
      global _args
      global _kwargs
      _function_to_repeat = function
      _args = args
      _kwargs = kwargs

      commandToRepeat = ('python("' + __name__ + '._do_repeat()")')

      functionReturn = function(*args, **kwargs)
      try:
      cmds.repeatLast(addCommand=commandToRepeat,
      addCommandLabel=function.__name__)
      except:
      pass

      return functionReturn

      return decoratorCode

      Delete
    3. Oups, the indentations are all wrong, but I cannot write proper code in those replies. Can someone write this in a proper way to make copy-pastable?

      Thanks,
      Carlo

      Delete
    4. cản trở xuống tới, thực lực năm sao Đấu Thánh, Minh La cũng không phải là người dễ đối phó.

      - Ca ca ca ca.

      Ngay khi Minh La công kích cột nước, cùng lúc đó thủy vực cũng hóa thành băng hải thật dầy, cả không gian đều bị tầng băng thật dầy bao phủ.

      Ở nơi này băng dầy, hơi thở lạnh thấu xương trực tiếp đâm thấu vào thể nội Minh La, hàn băng thấu xương này đấu khí Minh La căn bản không cách nào ngăn cản, theo hơi thở hàn băng, Minh La tựa hồ cảm giác mình cũng bị đóng băng.

      - Phá cho ta.
      dongtam
      mu private
      tim phong tro
      http://nhatroso.com/
      nhac san cuc manh
      tổng đài tư vấn luật
      http://dichvu.tuvanphapluattructuyen.com/
      văn phòng luật
      tổng đài tư vấn luật
      dịch vụ thành lập công ty
      http://we-cooking.com/
      chém gió
      trung tâm ngoại ngữ
      Minh La hét lớn một tiếng, đấu khí toàn thân điên cuồng lay động, một đạo ánh sáng chói mắt hung hăng đánh ra, đồng thời băng hải lại bắt đầu nứt ra một cái khe.

      - Xoẹt.

      Thân thể Minh La mượn cơ hội đột phá băng hải.

      - Vào bên trong Thiên Long Phục Ma Trận,, sinh tử do ta, Thập phương

      Delete
    5. Nice work! That helped me heaps. Thanks

      Delete