Python Contexts in Maya

Python’s contexts are a powerful feature and if you’re writing any kind of pipeline or animation tools, you should know about them.

Maya has a number of useful applications for the context. If you’ve spent any time working in it, you have no doubt noticed the tendency of the global selectionList to change at inopportune times. Many Maya commands have flags to prevent the selection of new objects, but Python gives us a much nicer way. Consider the following:


from contextlib import contextmanager
import maya.OpenMaya as om
@contextmanager
def keepSelection():
    # setup
    sel = om.MSelectionList()
    om.MGlobal.getActiveSelectionList(sel)

    yield

    #cleanup
    om.MGlobal.setActiveSelectionList(sel)

The context is ready to maintain Maya’s selection when running any code in the block.


import maya.cmds as cmds
cube = cmds.polyCube()
cmds.select(cube)
with keepSelection():
    loc = cmds.spaceLocator()
    cmds.select(loc)

print(cmds.ls(sl=1))

If all has gone well, you should see the current selection printed in the scriptEditor: [u’pCube1′, u’polyCube1′]. The original selection is restored when the code within the ‘with’ block completes.

Background Info

If you’ve ever used the ‘with’ keyword, you’ve used a context. If you want to know why they’re cool, you need to know a bit about resource acquisition is initialization. The key concept is the acquisition and release of resources tied to object lifetimes. The resources in question are things like memory, file descriptors – anything that is limited and used by the computer. File descriptors are the classic example, as operating systems have limited numbers of descriptors that are allocated when files are opened for reading and writing. In Python, files are opened with the open keyword:


def getData(fileName):
    fl = open('myFile.txt')
    data = fl.read()
    fl.close()
    return data

Open and shut, except for a couple of things. If there is an error when reading the data, fl.close may not get called – or one could forget to call close(). This means that the file descriptor will remain open until the Python application space is shut down. If you’re running this code in Maya, that could be quite a while. If the function returns before close() is called, the file remains open.

The RAII idiom ties the acquisition of the file descriptor to the lifetime of an object. This works great in C++, where objects live and die in very predictable ways. For example, in C++ you can open a file with std::ifstream:


#include <string>
#include <fstream>
#include <streambuf>

int accessFile(const std::string& name, std::string& data) {
    data.clear();
    std::ifstream ip(name.c_str());
    if(ip) {
        data.assign((std::istreambuf_iterator<char>(t)),
                 std::istreambuf_iterator<char>());
        return 1;
    }
    return 0;
}

When accessFile goes out of scope, ip is deleted, and it’s deletion causes the file handle to be released. Even if an exception occurs, when the ip instance is destroyed by any means, the file closes.

In Python and other garbage collected languages, the lifetime of ip is not clear. When the Python function returns, ip is unbound, but the data structure it pointed to exists until the Python garbage collector deletes it. As a result, even if the deletion of the Python file object released the file descriptor, it would do so at an unpredictable time compared to the C++ version.

Enter the Context

The context is an object that implements an __enter__() and an __exit__() method – these methods provide initialize and cleanup functions when the code enters the scope of a ‘with’ statement. As these are functions that get called, it is known where and when they get called.

Now the proper way to open a file object for reading or writing is to use the ‘with’ statement:


with open('myFile.txt') as ip:
    data = ip.read()

The file object context protects against exceptions – so the file handle is guaranteed to close when the context closes no matter what.

The contextlib.contextmanager decorator, as used in the keepSelection context above. The decorator defines an iterator that performs the setup work, then ‘yields’ a result (save selection yields None). When the ‘with’ statement is exited, the code after the yield statement is executed.

For a simpler, non-Maya example, you can try the following:


@contextmanager
def testContext():
    print('Enter')
    yield
    print('Exit')

with testContext():
    print(' Printing')

# Result
Enter
 Printing
Exit

Handling Exceptions

The contextmanager generator is not exception-proof by default, but often it needs to be able to handle itself. Consider the previous example, but an exception is thrown in the ‘with’ block:


@contextmanager
def keepSelection():
    # setup
    sel = om.MSelectionList()
    om.MGlobal.getActiveSelectionList(sel)
    raise RuntimeError
    yield

    #cleanup
    om.MGlobal.setActiveSelectionList(sel)

Run this with the cube/locator code from earlier, and the runtime error will show up in the scriptEditor, and the locator will be selected, meaning the cleanup code did not run. This may or may not be desirable, but here’s how to handle exceptions so you can make the call:


@contextmanager
def keepSelection():
    # setup
    sel = om.MSelectionList()
    om.MGlobal.getActiveSelectionList(sel)
    raise RuntimeError
    try:
        yield
    finally:
        pass

    #cleanup
    om.MGlobal.setActiveSelectionList(sel)

Now the cleanup will run no matter what exception is encountered. The exception will still raise – code can be added to catch the exception based on need.

I am planning to do a follow up post with some more info and hopefully useful decorators. In the meantime, I’ll leave you with one I find interesting – it’s not a full implementation, but a decent start. This one makes raising exceptions optional, should they occur:


@contextmanager
def tempNamespace(ns, stopOnError=False):
    cur = cmds.namespaceInfo(cur=1)
    cmds.namespace(set=':')
    cmds.namespace(add=ns)
    cmds.namespace(set=ns)
    try:
        yield
    except Exception,e:
        if stopOnError:
            raise
    cmds.namespace(set=':')
    cmds.namespace(mv=(ns, ':'), f=1)
    cmds.namespace(rm=ns, f=1)

 

Advertisements
Python Contexts in Maya