, dev
From Monkey Patching the Maya Python API to Gorilla and Bananas
Or how I went from patching the Autodesk’s Maya Python API to creating a more generic solution for extending any library in Python.
Coming from Softimage and having had previous experiences with various libraries, it was a shock when I first came face-to-face with the API of Maya.
Many like to think of it as being flexible but I mainly see it as a pain to use. It’s clunky and inconsistent at best, and lacks basic methods that would make everyone’s life easier.
Fortunately, I don’t even have the words to describe its port to Python but I thought more than once about hanging the one who believed that using the MScriptUtil
abomination to retrieve values such as the scale of a MFnTransform
object would be a good idea.
As a result I quickly started to hate my job until I thought that I needed to find a way to make this thing more usable. It ended up becoming the perfect playground for experimenting some monkey patching techniques in Python.
Extending the Maya API?
In C++, if we want to extend an external object-oriented library such as the Maya API, there’s often no much choices offered to us. Because of the static nature of the language we’ve got no way to directly modify the built-in classes. All we can do is add some patches on the side rather than in-between.
Patching on the side equals here in extending the classes through inheritance. This approach wouldn’t play so well in our case. Indeed, we would have many castings to do from the original types to our own derived types, and we would end up losing the object-oriented benefits anyways: each method that we would add to a certain class would only be available in the scope of that class and won’t be inherited by any of the built-in classes.
For an analogy, imagine that you have a system of pipes that you want to modify to provide hot water. All you can do is to derivate from what’s already out there without being able to link your extensions back onto the original system. Only your new system will have hot water, not the existing one.
If the API doesn’t provide a way to insert new methods into the existing classes at runtime, then what are our options?
We’re left with creating global functions taking as first argument a pointer or a reference to the class we want to extend. It does the job, but it’s not so object-oriented anymore even though it preserves polymorphism. Wrapping the original API into a new one could be another solution but a colossal and hard to maintain work too.
But that’s C++ and we want to use Python.
Python to the Rescue
Unlike with C++, everything in Python is an object. Even classes, functions and numbers are objects. We can assign those objects to variables, they most likely have attributes, and we can inspect and modify them at runtime.
This concept is at the same time both incredibly powerful and dangerous.
In our case, it gives us the chance to improve the Maya Python API on the surface by allowing us to sneakily insert new methods but it also makes it easy to fuck up everything if we’re not so wise. Welcome to the monkey patching world.
Monkey Patching ABC
Let’s check out the basics of the technique with a serie of examples.
from maya import OpenMaya
dummy = OpenMaya.MObject()
print(dummy.apiTypeStr())
print(dummy.isNull())
Here we initialize a dummy
variable containing an instance of the class MObject
. As it is right now, this dummy
variable doesn’t hold any valid Maya object internally but that’s enough for us to play around and have access to the methods defined within the MObject
class.
You’d better off checking the Maya documentation to know what methods are available to call but we can also list them at runtime with a Python’s built-in function: dir()
.
from maya import OpenMaya
print(dir(OpenMaya.MObject))
As per Python’s documentation, the dir()
function tries to list all the attributes of an object. Simply put, this will print here a whole bunch of names that correspond to the methods and data variables defined for the class MObject
. The ones surrounded with double underscores are the internal “magic” attributes mostly generated by Python that we’re not supposed to touch—for now.
Since we said that everything in Python is an object, nothing stops us from getting the value assigned to those attributes listed previously. There’s another function built-in in Python to do just that: getattr()
.
from maya import OpenMaya
for attribute in dir(OpenMaya.MObject):
print('%s: %s' % (attribute, getattr(OpenMaya.MObject, attribute)))
The attribute apiTypeStr
for example is assigned with an unbound method object. That unbound method object is a legacy from Python 2 to represent methods that haven’t been called from a class instance. But this is just an unimportant detail for this scope.
What matters is that we can indeed retrieve those attributes at runtime, and that if there’s a getattr()
function, then there must be a setattr()
one.
setattr()
allows us to assign new values to existing attributes but also to define new ones if they don’t exist yet. That’s precisely what we need to insert a new awesome method directly within the MObject
class.
from maya import OpenMaya
def awesomeFn(self):
return "Everything is awesome! Even this %s thing." % self.apiTypeStr()
setattr(OpenMaya.MObject, 'awesome', awesomeFn)
dummy = OpenMaya.MObject()
print(dummy.awesome())
That’s all it takes. Thanks to this one liner, we now have access to the awesome()
method as if it has always been part of the MObject
class.
But that’s also where the danger is. By inadvertance—or madness—, you could replace an existing method. This is allowed in Python since once again it is considered as a simple reassignment of a variable with another object. Of course you’ve changed the expected behavior of that method, causing a subtle bug showing up only late in the production. Congratulations, you’ve broken everything and are about to get fired.
A first option would be to prefix all your own methods with something unlikely to be ever used by the developer team of Maya. The method awesome()
could for example be called bnnAwesome()
.[1]
Secondly, you could check if a method already exists, just to make sure. And if it does, then just give up. It’s better to preserve the original behavior rather than inserting your broken method that you couldn’t even figure out how to name properly.
Quizz on how to check if a method already exists? The global function hasattr()
.
Now, there’s a last option if really, really, you have a good reason, you know what you’re doing at 2000%, and you don’t mind getting fired. Should I say that it’s highly unrecommended? Well, it is. Use at your own risks. You’ve been warned.
That last option consists in storing the existing method under a new name such as _original_awesome()
. This way, the overidden method remains callable within our code if we wanted to preserve its original behavior as we should.
Inheritance
Since we’re inserting methods into an existing class hierarchy, this means that inheritance is automagically preserved.
In other words, if we insert a method bnnAwesome()
in MFnTransform
, then it will also be available in the all the classes defined on the right of MFnTransform
from the graph above.
from maya import OpenMaya, OpenMayaAnim
def awesomeFn(self):
return "Everything is awesome!"
# Patch the `MFnTransform` class.
setattr(OpenMaya.MFnTransform, 'awesome', awesomeFn)
# Use the method in the `MFnIkJoint` class.
dummy = OpenMayaAnim.MFnIkJoint()
print(dummy.awesome())
And that’s awesome.
A Naive Implementation
As effective as this code can be, this won’t take us every far and it would quickly become cumbersome if we had to manually repeat this process for each method that we’d like to patch.
The original approach that I used in production was to create a bunch of Python modules, each named as per the OpenMaya class that I wanted to patch, and each containing a list of functions to insert.
A function could be inserted either as a normal method, a class method, or a static method depending if self
, cls
or nothing was defined as first keyword.
It’s been really easy to use but it felt a bit hacky.
That’s why I recently came up with the idea of building a library to provide a convenient—and flexible—approach to monkey patching.
The Real Deal: Gorilla
With the library Gorilla, you can patch anything as soon as it makes sense.
Marking our awesome method as being an extension for the target class OpenMaya.MFnTransform
becomes as easy as:
import gorilla
from maya import OpenMaya
@gorilla.patch(OpenMaya.MFnTransform)
def bnnAwesome(self):
return "Everything is awesome!"
If many methods are to be patched into a same target class, it can be quicker to define them into a class and mark that class as an extension itself.
import gorilla
from maya import OpenMaya
@gorilla.patch(OpenMaya)
class MFnTransform(object):
def bnnAwesome(self):
return "Everything is awesome!"
def bnnMoreAwesome(self):
return "Everything is even more awesome!"
Our MFnTransform
class is here to be directly patched into the module OpenMaya
. Since a class with that name already exists in the target module, it’s each method of our class that are going to be individually inserted into the OpenMaya.MFnTransform
class while preserving the ones built-in.
To apply the actual patching, a function is provided to recursively look into all the modules from a given package for any extension marked with the gorilla.patch()
decorator. The extensions found are then applied.
Properties, class methods, static methods, and whatsoever are also supported as extensions. Other features include the possibility to define what needs to be patched at runtime and how. But really, the documentation of Gorilla does explain all the details more nicely.
The best in all that? The code is relatively straightforward and works for both Python 2.6+ and 3.3+. Have a look by yourself, get the source on GitHub.
A Banana for Maya
As a proof of concept for the library Gorilla, I’ve made a set of extensions for the Maya Python API v1.0.
In its current state, it’s not very furnished but it still provides a good starting point with a set of methods to easily retrieve and iterate through the nodes of a scene.
It’s—logically?—called Bana and can also be found on GitHub. Here again, I’ve put some decent efforts in the documentation if you’re curious.
Final Note
When I decided to adopt this approach, it was back in 2010 and I had my own—not necessarily valid—reasons to do so. PyMEL wasn’t shipped with Maya and I refused to use the Python command layer of Maya which is a simple 1:1 of the atrocity that MEL is.[2]
Even today I would most probably go the same way as it served me quite well and made me proud of successfully taking on the challenge of using that Maya Python API in production.
Now, by publishing this article, I do not intend to say that monkey patching the Maya Python API is the way to go. There’s other alternatives out there that are probably better suited for the vast majority of the use cases and that won’t require any extra work on your end.
Whatever path you choose, do it because you feel comfortable using it on a daily basis and make sure that you can get your things done efficiently.
Having the monkey patching technique in hands simply gives you the bonus option of being able to extend any Python library to your wishing if you need to, and as a last resort kind of things.
Use it wisely.