Maya and Python are both excellent and elegant tools that can together achieve amazing results. And while it may be tempting to dive in and start wielding this power, it is prudent to understand some basic things first. Knowledge of the fundamentals will provide the platform from which we will grow great skills and conquer our obstacles throughout the rest of this book.
In this chapter, we will look at Python as a language, Maya as a program, and PyMEL as a framework. We will begin by briefly going over how to use the standard Python interpreter, the Maya Python interpreter, the Script Editor in Maya, and your Integrated Development Environment (IDE) or text editor in which you will do the majority of your development. Our goal for the chapter is to build a small library that can easily link us to documentation about Python and PyMEL objects. Building this library will illuminate how Maya, Python and PyMEL are designed, and demonstrate why PyMEL is superior to
maya.cmds. We will use the powerful technique of
type introspection to teach us more about Maya's node-based design than any Hypergraph or static documentation can. Along the way we will explore some core concepts that will reoccur throughout later chapters.
There are generally three different modes you will be developing in while programming Python in Maya: using the mayapy interpreter to evaluate short bits of code and explore ideas, using your Integrated Development Environment to work on the bulk of the code, and using Maya's Script Editor to help iterate and test your work. In this section, we'll start learning how to use all three tools to create a very simple library.
The first thing we must do is find your mayapy interpreter. It should be next to your Maya executable, named
mayapy.exe. It is a Python interpreter that can run Python code as if it were being run in a normal Maya session. When you launch it, it will start up the interpreter in
interactive mode, which means you enter commands and it gives you results, interactively. The
... characters in code blocks indicate something you should enter at the interactive prompt; the code listing in the book and your prompt should look basically the same. In later listings, long output lines will be elided with
... to save on space.
Most of the interactive samples can be run as code through
doctest. See Appendix, Python Best Practices, for more information.
mayapy process by double clicking or calling it from the command line, and enter the following code:
>>> print 'Hello, Maya!' Hello, Maya! >>> def hello(): ... return 'Hello, Maya!' ... >>> hello() 'Hello, Maya!'
The first statement prints a string, which shows up under the prompting line. The second statement is a multiline function definition. The
... indicates the line is part of the preceding line. The blank line following the
... indicates the end of the function. For brevity, we will leave out empty
... lines in other code listings. After we define our
hello function, we invoke it. It returns the string
"Hello, Maya!", which is printed out beneath the invocation.
Now, we need to find a place to put our library file. In order for Python to load the file as a module, it needs to be on some path where Python can find it. We can see all available paths by looking at the path list on the
>>> import sys >>> for p in sys.path: ... print p C:\Program Files\Autodesk\Maya2013\bin\python26.zip C:\Program Files\Autodesk\Maya2013\Python\DLLs C:\Program Files\Autodesk\Maya2013\Python\lib C:\Program Files\Autodesk\Maya2013\Python\lib\plat-win C:\Program Files\Autodesk\Maya2013\Python\lib\lib-tk C:\Program Files\Autodesk\Maya2013\bin C:\Program Files\Autodesk\Maya2013\Python C:\Program Files\Autodesk\Maya2013\Python\lib\site-packages
A number of paths will print out; I've replicated what's on my Windows system, but yours will almost definitely be different. Unfortunately, the default paths don't give us a place to put custom code. They are application installation directories, which we should not modify. Instead, we should be doing our coding outside of all the application installation directories. In fact, it's a good practice to avoid editing anything in the application installation directories entirely.
Let's decide where we will do our coding. We'll call this location the development root for the rest of the book. To be concise, I'll choose
C:\mayapybook\pylib to house all of our Python code, but it can be anywhere. You'll need to choose something appropriate if you are on OS X or Linux; we will use
~/mayapybook/pylib as our path on these systems, but I'll refer only to the Windows path except where more clarity is needed. Create the development root folder, and inside of it create an empty file named
Now, we need to get
C:\mayapybook\pylib onto Python's
sys.path so it can be imported. The easiest way to do this is to use the
PYTHONPATH environment variable. From a Windows command line you can run the following to add the path, and ensure it worked:
> set PYTHONPATH=%PYTHONPATH%;C:\mayapybook\pylib > mayapy.exe >>> import sys >>> 'C:\\mayapybook\\pylib' in sys.path True >>> import minspect >>> minspect <module 'minspect' from '...\minspect.py'>
The following is the equivalent commands on OS X or Linux:
$ export PYTHONPATH=$PYTHONPATH:~/mayapybook/pylib $ mayapy >>> import sys >>> '~/mayapybook/pylib' in sys.path True >>> import minspect >>> minspect <module 'minspect' from '.../minspect.py'>
There are actually a number of ways to get your development root onto Maya's path. The option presented here (using environment variables before starting Maya or mayapy) is just one of the more straightforward choices, and it works for mayapy as well as normal Maya. Calling
sys.path.append('C:\\mayapybook\\pylib') inside your
userSetup.py file, for example, would work for Maya but not mayapy (you would need to use
maya.standalone.initialize to register user paths, as we will do later).
export to set environment variables only works for the current process and any new children. If you want it to work for unrelated processes, you may need to modify your global or user environment. Each OS is different, so you should refer to your operating system's documentation or a Google search. Some possibilities are
setx from the Windows command line, editing
/etc/environment in Linux, or editing
/etc/launchd.conf on OS X. If you are in a studio environment and don't want to make changes to people's machines, you should consider an alternative such as using a script to launch Maya which will set up the
PYTHONPATH, instead of launching the
maya executable directly.
Now it is time to use our IDE to do some programming. We'll start by turning the path printing code we wrote at the interactive prompt into a function in our file. Open
C:\mayapybook\pylib\minspect.py in your IDE and type the following code:
import sys def syspath(): print 'sys.path:' for p in sys.path: print ' ' + p
Save the file, and bring up your mayapy interpreter. If you've closed down the one from the last session, make sure
C:\mayapybook\pylib (or whatever you are using as your development root) is present on your
sys.path or the following code will not work! See the preceding section for making sure your development root is on your
>>> import minspect >>> reload(minspect) <module 'minspect' from '...\minspect.py'> >>> minspect.syspath() C:\Program Files\Autodesk\Maya2013\bin\python26.zip C:\Program Files\Autodesk\Maya2013\Python\DLLs C:\Program Files\Autodesk\Maya2013\Python\lib C:\Program Files\Autodesk\Maya2013\Python\lib\plat-win C:\Program Files\Autodesk\Maya2013\Python\lib\lib-tk C:\Program Files\Autodesk\Maya2013\bin C:\Program Files\Autodesk\Maya2013\Python C:\Program Files\Autodesk\Maya2013\Python\lib\site-packages
First, we import the
minspect module. It may already be imported if this was an old mayapy session. That is fine, as importing an already-imported module is fast in Python and causes no side effects. We then use the
reload function, which we will explore in the next section, to make sure the most up-to-date code is loaded. Finally, we call the
syspath function, and its output is printed. Your actual paths will likely vary.
It is very common as you develop that you'll make changes to some code and want to immediately try out the changed code without restarting Maya or mayapy. You can do that with Python's built-in
reload function. The
reload function takes a module object and reloads it from disk so that the new code will be used.
When we jump between our IDE and the interactive interpreter (or the Maya application) as we did earlier, we will usually reload the code to see the effect of our changes. I will usually write out the
reload lines, but occasionally will only mention them in text preceding the code.
Keep in mind that reload is not a magic bullet. When you are dealing with simple data and functions as we are here, it is usually fine. But as you start building class hierarchies, decorators, and other things that have dependencies or state, the situation can quickly get out of control. Always test your code in a fresh version of Maya before declaring it done to be sure it does not have some lingering defect hidden by reloading.
Though once you are a master Pythonista you can ignore these warnings and figure out how to reload just about anything!
Now we will start digging into Maya and PyMEL. Let's begin by initializing Maya in the mayapy interpreter so we can use more than just standard Python functionality. We do this by calling
maya.standalone.initialize, as shown in the following code:
>>> import maya.standalone >>> maya.standalone.initialize() >>> import pymel.core as pmc >>> xform, shape = pmc.polySphere()
The import of
pymel.core will implicitly call
maya.standalone.initialize automatically, but I do it explicitly here so it's clear what's going on. In the future, you can generally skip the call to
maya.standalone.initialize and just import
There is a lot we can discover about these PyMEL objects, which represent Maya nodes, using basic Python. For example, to see the type
of either of our objects, we can use the built-in
type function (we will dig much deeper into types later in this chapter).
>>> type(xform) <class 'pymel.core.nodetypes.Transform'> >>> type(shape) <class 'pymel.core.nodetypes.PolySphere'>
To see the names of all the attributes on our shape, we can use the built-in
>>> dir(xform) ['LimitType', 'MAttrClass', ..., 'zeroTransformPivots']
>>> getattr(xform, 'getShape') <bound method Transform.getShape of nt.Transform(u'pSphere1')> >>> getattr(xform, 'translate') Attribute(u'pSphere1.translate')
Note that we are using the
getattr built-in Python function, not to be confused with the
maya.cmds.getAttr function or its equivalent PyMEL version. We will not use the
getAttr function at all in this book. While using
getAttr may be more familiar if you are coming from the world of MEL and
getattr is familiar if you are coming from the much saner world of Python. We should take full advantage of Python and do things the Python, not MEL, way.
We can combine all of these techniques and use the
inspect module from Python's standard library (commonly referred to as the
stdlib) to filter for interesting information about our object.
>>> import inspect >>> methods =  >>> for a in dir(xform): ... attr = getattr(xform, a) ... if inspect.ismethod(attr): ... methods.append(attr) >>> attrs = xform.listAttr() >>> methods [<bound method Transform.__add__ of nt.Transform(u'pSphere1')>, ...] >>> attrs [Attribute(u'pSphere1.message'), ...]
In the preceding code, we use the
dir function to get every Python attribute from our PyMEL transform instance, and then filter them into a list of methods. We then use the
listAttr method to get a list of Maya attributes on the transform. Based on this data, we can begin to see how the node is structured. Maya attributes are represented by instances of
Attribute, and methods are usually helpers.
for loops and
if statements that append to a list is not a good practice. We'll replace them with a much nicer syntax called list comprehensions in Chapter 2, Writing Composable Code.
We'll create a new function in
minspect.py that will print out interesting information about the object we pass it. Open
C:\mayapybook\pylib\minspect.py in your IDE or text editor. First, we will add an import to the top of the file (
import sys should already be there). As a matter of style, imports should always go at the top of the file, one per line, divided into groups. Refer to Python's style guide, called
PEP8, at http://www.python.org/dev/peps/pep-0008/#imports. We will refer back to PEP8 several times throughout this book.
import pymel.core as pmc import sys
Now let's use our earlier techniques and our understanding of Maya to print some information about an object. We can use the
name method on
PyNode to provide a simple string representation for a node.
def info(obj): """Prints information about the object.""" lines = ['Info for %s' % obj.name(), 'Attributes:'] # Get the name of all attributes for a in obj.listAttr(): lines.append(' ' + a.name()) result = '\n'.join(lines) print result
You'll notice that instead of repeatedly printing, we put our data into a list and print it only once at the end. We do this for three reasons.
First, appending substrings to a list of strings is faster than incrementally concatenating (adding) them into a large string. Never concatenate many strings together in Python. For example, consider the following expression.
'Hello, ' + username + ', it is ' + now + ' right now.'
Second, joining a list of strings is a well-known pattern. It is said to be more Pythonic than string concatenations.
There is no formal definition for what Pythonic means, but here is an attempt: something is said to be Pythonic when enough people have said it is Pythonic, or it is very similar to something that is Pythonic. You should learn to use Pythonic idioms wherever possible, and I will point them out when we run across them.
Finally and most importantly, building a list and printing at the end is more maintainable and easier to test because we are separating decisions (creating the result string) from dependencies (the output stream we print to). If this were production code we'd remove the
info. We facilitate that future change by putting
Note the use of the
% operator in the
info function. We are using
string formatting. If you are not familiar with string formatting in Python, refer to Appendix, Python Best Practices, for more information about it.
We can continue adding to this function to express more information about a node. Try adding support for printing relatives by using
obj.listRelatives() if you are comfortable.
>>> type() <type 'list'> >>> type(type()) <type 'type'> >>> type(xform) <class 'pymel.core.nodetypes.Transform'>
You may also be familiar with MEL's type strings. Because MEL does not possess a rich type system like Python, typing (if it can be called that) is done with strings, as we can see in the following example. The usages of MEL type strings are highlighted.
>>> pmc.joint() nt.Joint(u'joint1') >>> pmc.polySphere() [nt.Transform(u'pSphere2'), nt.PolySphere(u'polySphere2')] >>> pmc.ls(type='joint') [nt.Joint(u'joint1')] >>> pmc.ls(type='transform') [...nt.Joint(u'joint1'), nt.Transform(u'pSphere1'), ...] >>> pmc.ls(type='shape') [...nt.Mesh(u'pSphereShape1'), ...]
This MEL type, as we'll call it, is very useful while scripting, but not very descriptive. For example, we need to know in advance that a joint is a specific type of transform, and thus returned from invoking
pmc.ls(type='transform'). This relationship is not clearly expressed.
In contrast, these taxonomic relationships are much better expressed through Python's type system. If we go to the PyMEL documentation for its
Joint class, we can see the following diagram of its type hierarchy:
This type hierarchy mimics Maya's underlying object oriented architecture, and we can use it to understand things about nodes we may not be totally familiar with. For example, we can know that a
translate/rotate/scale attributes because it is a subclass of
Transform. A subclass inherits the behavior, such as the attributes and methods, of its base class. A subclass is commonly called a
child class, and a base class is commonly called a parent class
Notice in the example below how the
__bases__ attribute indicates that
Transform is the base class of the
>>> j = pmc.joint() >>> j.type() u'joint' >>> type(j) <class 'pymel.core.nodetypes.Joint'> >>> type(j).__bases__ (<class 'pymel.core.nodetypes.Transform'>,) >>> j.translate, j.rotate (Attribute(u'joint2.translate'), Attribute(u'joint2.rotate'))
We will look into PyMEL's type hierarchies for the next few sections as a means of understanding how PyMEL nodes work. Don't get intimidated if these concepts are new. We won't be creating any of our own types in this section, and if you are familiar with Maya's nodes, the type hierarchies we are going to examine should be rather intuitive. Later in the book, once we are more familiar with Python and PyMEL, several exercises will require creating our own types.
Even more useful than the
__bases__ attribute is the
__mro__ attribute. Method Resolution Order (MRO) is the order Python visits different types so it can figure out what to actually call. You generally don't need to understand the MRO mechanisms (they can be complex), but looking at the MRO will help you understand all the type information about an object. Let's look at the MRO for the
>>> type(j).__mro__ (<class 'pymel.core.nodetypes.Joint'>, <class 'pymel.core.nodetypes.Transform'>, <class 'pymel.core.nodetypes.DagNode'>, <class 'pymel.core.nodetypes.Entity'>, <class 'pymel.core.nodetypes.ContainerBase'>, <class 'pymel.core.nodetypes.DependNode'>, <class 'pymel.core.general.PyNode'>, <class 'pymel.util.utilitytypes.ProxyUnicode'>, <type 'object'>)
This makes sense: a joint node is a special type of transform node, which is a type of DAG node, which is a type of dependency node, and so on, mirroring the inheritance diagram we previously saw. The fact that reality meets expectation here is a great testament to Maya and PyMEL.
When a call to
j.name() is invoked, Python will walk along the MRO looking for the first appropriate method to call. In the case of PyMEL, looking at the MRO often tells us a lot about how an object behaves. This is not always the case, however. Python allows much more dynamic resolution mechanisms. We will not use these mechanisms, such as
__getattribute__, much in this book, but you should be aware that the MRO may not always tell the whole story.
Let's add the collection of type and MRO information into the
def info(obj): """Prints information about the object.""" lines = ['Info for %s' % obj.name(), 'Attributes:'] # Get the name of all attributes for a in obj.listAttr(): lines.append(' ' + a.name()) lines.append('MEL type: %s' % obj.type()) lines.append('MRO:') lines.extend([' ' + t.__name__ for t in type(obj).__mro__]) result = '\n'.join(lines) print result
In Python, there's a saying, "Its objects all the way down." This means that everything in Python, including numbers, strings, modules, functions, types, and so on, are all just objects. In MEL and
maya.cmds I like to say, "Its strings all the way down." Because the type system in MEL and
maya.cmds is so rudimentary, many things must be handled through strings. And in PyMEL, I like to say, "It's PyNodes all the way down."
The saying is adapted from "Its turtles all the way down." It is left as an exercise to the reader to uncover the origin of this quote. It may also be said in Python that, "Its dicts all the way down." Python is a language of many clever sayings.
Let's look at our PyMEL transform node to better understand how it is "PyNodes all the way down."
>>> type(xform).__mro__ (<class 'pymel.core.nodetypes.Transform'>, <class 'pymel.core.nodetypes.DagNode'>, <class 'pymel.core.nodetypes.Entity'>, <class 'pymel.core.nodetypes.ContainerBase'>, <class 'pymel.core.nodetypes.DependNode'>, <class 'pymel.core.general.PyNode'>, <class 'pymel.util.utilitytypes.ProxyUnicode'>, <type 'object'>) >>> type(xform.translate).__mro__ (<class 'pymel.core.general.Attribute'>, <class 'pymel.core.general.PyNode'>, <class 'pymel.util.utilitytypes.ProxyUnicode'>, <type 'object'>)
There are a number of very interesting things going on in this short listing.
First, our two types—along with all PyMEL types, in fact—inherit from both
ProxyUnicode (as well as
object, which all Python types inherit from). The
PyNode type represents any Maya node (DAG/dependency nodes, attributes, Maya windows, and so on). Vitally,
Attributes are also
PyNodes. If we look at the PyMEL help for
PyNode, we can see the distinguishing features of
PyNodes are that they have a name/identity, connections, and history.
Transform is also a
DagNode. We can use our knowledge of Maya (or graph theory, if you're into that) to infer that this means the object can have a parent, children, instances, and so on. This is a great example where our knowledge of Maya maps directly onto PyMEL, and we aren't required to learn a new paradigm to understand how the PyMEL framework works.
We build custom Maya nodes in Chapter 7, Taming the Maya API.
Finally, if we look at the
a Joint, we will see the following information:
>>> type(pmc.joint()).__mro__ (<class 'pymel.core.nodetypes.Joint'>, <class 'pymel.core.nodetypes.Transform'>, ..., <type 'object'>)
We can immediately understand much of how a PyMEL
Joint works if we already understand
Transform. In fact, everything about
Transform is also true for every
Joint. The deduction that every
Joint behaves like a
Transform is known as the
Liskov substitution principle. It states, roughly, that if S is a subclass of T, then a program can use an instance of S instead of T without a change in behavior. It is a fundamental principle of good object-oriented design and manifests itself in well-designed frameworks such as PyMEL.
The fact that types inherit the behavior of their parents is important to keep in mind as you go through the rest of this book and program with PyMEL. Don't worry if you don't fully understand how inheritance works or how to best leverage it. It will become clearer as we proceed on our journey.
ProxyUnicode class should be treated as an implementation detail. The only important user-facing detail is that it allows PyNodes to have string methods on them (
.strip, and so on). As of writing this book, I've never used the string methods on a
PyNode. Maybe there are valid uses but I can't imagine them. There are always better, more explicit ways of dealing with the node. If you need to deal with its name, call a name-returning method (
longName(), and the like) and manipulate that. Use the
rename method to rename the node.
PyMEL's intuitive use of type hierarchies does not end with Maya node types. It also provides a very useful wrapper around Maya's mathematical data types, including vectors, quaternions, and matrices. These types are located in the
Let's take a closer look at
xform's transform information.
>>> xform.translate Attribute(u'pSphere1.translate') >>> t = xform.translate.get() >>> print t [0.0, 0.0, 0.0]
The translation value of the sphere transform, which is highlighted, appears to be a list. It isn't. The translation value is an instance of
pymel.core.datatypes.Vector. Sometimes we need to more aggressively introspect objects. I think this is one of the few areas where PyMEL made a mistake. Calling
str(t) returns a string that looks like it came from a list, instead of looking like it came from a
Vector. Make sure you have the correct type. I've spent hours hunting down bugs where I was using a
Vector instead of a
list, or vice versa.
>>> vect = xform.translate.get() >>> lst = [0.0, 0.0, 0.0] >>> str(vect) '[0.0, 0.0, 0.0]' >>> str(lst) '[0.0, 0.0, 0.0]' >>> print t, lst # The print implicitly calls str(t) [0.0, 0.0, 0.0] [0.0, 0.0, 0.0] >>> repr(t) # repr returns a more detailed string for an object 'dt.Vector([0.0, 0.0, 0.0])' >>> repr(lst) '[0.0, 0.0, 0.0]'
repr as highlighted in the preceding code shows us that
vect is not a list. It is one of PyMEL's special data types. This has a number of benefits, despite its bad string representation.
First of all,
Vector and other data types are list-like objects. The
__iter__ method means we can iterate over it, just like a list.
>>> t = xform.translate.get() >>> for c in t: ... print c 0.0 0.0 0.0
__getitem__ method means we can look up an index.
>>> t, t, t (0.0, 0.0, 0.0)
>>> [1, 2, 3] + [4, 5, 6] # Regular Python lists [1, 2, 3, 4, 5, 6] >>> repr(t + [1, 2, 3]) 'dt.Vector([1.0, 2.0, 3.0])'
Vector has several useful methods on it, including name-based accessors and helper methods.
>>> t.x += 5 # Familiar name-based access >>> t.y += 2 >>> t.x 5.0 >>> t.length() # And helpers! 5.385...
This sort of design, where a custom type implements several different interfaces or protocols, is powerful. For example, if you wanted to move a transform by some vector, you can just write the following code:
>>> def move_along_x(xform, vec): ... t = xform.translate.get() ... t += vec ... xform.translate.set(t) >>> j = pmc.joint() >>> move_along_x(j, [1, 0, 0]) >>> j.translate.get() dt.Vector([1.0, 0.0, 0.0]) >>> move_along_x(j, j.translate.get()) >>> j.translate.get() dt.Vector([2.0, 0.0, 0.0])
Notice that at no point did we need to check whether
vec was an instance of list or of
Vector. We just require it to implement
__getitem__ so we can access an index. Think about how much more natural this pattern makes using
Matrix, and the other data types in
info function we've been building isn't just a useful learning exercise. It can be helpful in everyday programming. A major advantage of dynamic, interpreted languages such as Python has been their REPL: Read-Evaluate-Print Loop (REPL). We've actually been using a REPL all throughout this chapter.
Let's write some simple code in the interpreter to demonstrate the REPL:
>>> pmc.joint <function joint at 0x0...> >>>
Now let's see how this fits into the REPL flow:
The interpreter prompts for input, indicated by the
pmc.jointand hit Enter.
The input string
pmc.jointis parsed into a data structure. This is the read.
The interpreter evaluates the data structure, finding PyMEL's
The interpreter prints the result to the output stream.
The interpreter prompts for more input (the loop).
The alternative to the REPL is the Edit-Compile-Run Loop of compiled languages. This is a much longer process, often lasting minutes instead of seconds and requiring a full restart of the application. In recent years, several compiled languages have created interpreters or REPL environments, but this is unavailable to C++ in Maya right now.
It stands to reason that anything we can do to improve our REPL experience will help us learn and explore our language and environment more effectively. Whereas
minspect.info allows us to see runtime information about some object, next we'll write a function to bridge the gap from runtime information to static documentation.
>>> help Type help() for interactive help, or help(object) for help about object.
help can be difficult to use with PyMEL (along with many other libraries). The documentation may be missing from the actual object, or may be defined somewhere else. Commonly, the documentation is too verbose to read comfortably in a terminal window. And in the case of a GUI Maya session, it is just more convenient to have documentation in your browser than in the Script Editor.
To better use the online documentation, we'll write an
minspect.pmhelp function that will link us to PyMEL's excellent online documentation.
As useful as something like
pmhelp can be, this exercise will be even more useful for understanding how PyMEL and Maya work. So even if you don't think you have a use for
pmhelp or if you get stuck with some of the more technical snippets in this chapter, keep going and at least read through how we build the function.
help(pmc.polySphere) is sort of like calling
print pmc.polySphere.__doc__. The
help function actually uses the interesting
pydoc module, which can do much more, but printing a docstring
is the most common use case. A docstring is the triple-quoted string that describes a function/method/class, as we had for our
minspect.info function. The triple-quoted string gets placed into the function or method's
pmhelp function will bring us to the PyMEL web help page for an object or name, searching for it if we do not get a direct match. We'll be able to use this function from within the mayapy interpreter, the Script Editor in Maya, and even a shelf button. We will use the introspection techniques we've learned to get enough information to search properly.
# Function converts a python object to a PyMEL help query url. # If the object is a string, # return a query string for a help search. # If the object is a PyMEL object, # return the appropriate url tail. # PyMEL functions, modules, types, instances, # and methods are all valid. # Non-PyMEL objects return None. # Function takes a python object and returns a full help url. # Calls the first function. # If first function returns None, # just use builtin 'help' function. # Otherwise, open a web browser to the help page.
First, open a web browser to the PyMEL help so we can understand the URL schema. For our examples, well use the Maya 2013 English PyMEL help at http://download.autodesk.com/global/docs/maya2013/en_us/PyMel/index.html.
C:\mayapybook\pylib\minspect.py and write the following code at the bottom of the file (the comments with numbers are for the explanation following the code sample and do not need to be copied):
def _py_to_helpstr(obj): #(1) return None def test_py_to_helpstr(): #(2) def dotest(obj, ideal): #(3) result = _py_to_helpstr(obj) assert result == ideal, '%s != %s' % (result, ideal) #(4) dotest('maya rocks', 'search.html?q=maya+rocks') #(5)
Let's pick apart the preceding code.
The leading underscore means this is a protected function. It indicates that we shouldn't call it from outside the module. It is for implementation only.
This function will contain all of our tests for the different possible objects we can pass in. We can write what we expect, and then assert that our function returns the correct result.
Defining a function within a function is normal in Python. Since we don't need
dotestoutside of our test function, just define it inside.
assertstatement means that if the expression following
False, raise an
AssertionErrorwith the message after the comma. So in this case, we're saying if the result is not equal to ideal, raise an error that tells the user the gotten and ideal values.
We call our inner function. All of the rest of our tests for this function will look very similar.
And in the mayapy interpreter, you can test the code by evaluating the following:
>>> import minspect #(1) >>> reload(minspect) <module 'minspect' from '...\minspect.py'> >>> minspect.test_py_to_helpstr() #(2) Traceback (most recent call last): #(3) AssertionError: ...
Import and reload
We invoke our test function, which evaluates our assertion.
Since we didn't actually implement anything, our assertion fails and an
The next step is to implement code to pass the test:
def _py_to_helpstr(obj): if isinstance(obj, basestring): return 'search.html?q=%s' % (obj.replace(' ', '+')) return None
Test Driven Development is a technique of developing code where you write your tests first. It is a really an excellent way to program, though not always possible in Maya. See Appendix, Python Best Practices, for more information about TDD.
Let's go ahead and write more test cases. We will add tests for the
pymel.core.nodetypes module, the
pymel.core.nodetypes.Joint type and instance, the
Joint.getTranslation method, and the
def test_py_to_helpstr(): def dotest(obj, ideal): result = _py_to_helpstr(obj) assert result == ideal, '%s != %s' % (result, ideal) dotest('maya rocks', 'search.html?q=maya+rocks') dotest(pmc.nodetypes, 'generated/pymel.core.nodetypes.html' '#module-pymel.core.nodetypes') dotest(pmc.nodetypes.Joint, 'generated/classes/pymel.core.nodetypes/' 'pymel.core.nodetypes.Joint.html' '#pymel.core.nodetypes.Joint') dotest(pmc.nodetypes.Joint(), 'generated/classes/pymel.core.nodetypes/' 'pymel.core.nodetypes.Joint.html' '#pymel.core.nodetypes.Joint') dotest(pmc.nodetypes.Joint().getTranslation, 'generated/classes/pymel.core.nodetypes/' 'pymel.core.nodetypes.Joint.html' '#pymel.core.nodetypes.Joint.getTranslation') dotest(pmc.joint, 'generated/functions/pymel.core.animation/' 'pymel.core.animation.joint.html' '#pymel.core.animation.joint')
I got the ideal values simply by going to the PyMEL online help and copying the appropriate part of the URL. Building code is simpler when your expectations are crystal clear!
The actual implementation of
_py_to_helplstr can be considered simple or complex depending on your experience with Python. It uses lots of double-underscore attributes and demands some understanding of Python's types and inner workings. Memorizing every detail of every line is less important than understanding the basic idea of the code.
We'll use various double underscore attributes such as
__name__. They are called either
dunder methods or magic methods, though there's nothing magical about them. Single leading underscore attributes, as we've seen, indicate implementation or protected attributes that callers outside of a module or class should not use. Double leading underscore indicate private attributes, though you can think of them the same as single leading underscore attributes (they cannot be called in a straightforward manner, however). Magic methods are generally not called directly but are for protocols such as the
__getitem__ method we saw earlier allowing an object to be indexed like a list or dictionary.
We'll add support for the different types of objects in the order they are listed in the test function until every test passes. We'll start with modules.
_py_to_helpstr function, we make heavy use of the
isinstance function. This sort of design may seem intuitive, but it is typically a bad practice to check if something is an instance of the class. It is generally better to check if something has functionality or behavior rather than checking what type it is. In this case, though, we do actually want to check if something is an instance. The type of the object is the behavior we are checking for.
pymel.core.nodetypes is a module, as are
pymel.core. The implementation of
_py_to_helpstr for modules is straightforward. We use the
__name__ attribute to identify the module (we will see other uses for
__name__ throughout this book).
>>> import pymel.core.nodetypes >>> pymel.core.nodetypes.__name__ 'pymel.core.nodetypes'
>>> import types >>> isinstance(pymel.core.nodetypes, types.ModuleType) True
We will continue to use the
types module throughout the rest of this chapter. The code should be mostly self-explanatory so I will only remark on the members we use when their usage is not obvious.
We also need to understand how documentation for modules is laid out in PyMEL's help. URLs to module documentation take the following form:
To support modules, the
_py_to_helpstr function should look like the following (don't forget to add
import types at the top of
def _py_to_helpstr(obj): if isinstance(obj, basestring): return 'search.html?q=%s' % (obj.replace(' ', '+')) if isinstance(obj, types.ModuleType): return ('generated/%(module)s.html#module-%(module)s' % dict(module=obj.__name__)) return None
>>> pmc.nodetypes.Joint <class 'pymel.core.nodetypes.Joint'> >>> pmc.nodetypes.Joint.__name__ 'Joint' >>> pmc.nodetypes.Joint.__module__ 'pymel.core.nodetypes'
The PyMEL help format for types is almost the same as the one for modules:
We'll treat a type as the default (last) case. If we pass in something that is not a type, we can just get the object's type and use that. Let's add support for types into our function:
def _py_to_helpstr(obj): if isinstance(obj, basestring): return 'search.html?q=%s' % (obj.replace(' ', '+')) if isinstance(obj, types.ModuleType): return ('generated/%(module)s.html#module-%(module)s' % dict(module=obj.__name__)) if not isinstance(obj, type): obj = type(obj) return ('generated/classes/%(module)s/' '%(module)s.%(typename)s.html' '#%(module)s.%(typename)s' % dict( module=obj.__module__, typename=obj.__name__))
When we use something like
mylist.append(1), we can say we are "invoking the
append method with an argument of
1 on the
list instance named
'mylist'." Methods are things we call on an instance of an object. Things we call on a module are called functions, and we'll cover them after methods.
It's important to define what a method is with precise vocabulary. While speaking with other people, it's not so important if you mix up "function" with "method", but when you're writing code, you must be very clear what you are doing.
The following code creates
a Joint and inspects the type of the instance's
getTranslation method. We can see the method's type is
instancemethod, and it is an instance of
>>> joint = pmc.nodetypes.Joint() >>> type(joint.getTranslation) <type 'instancemethod'> >>> isinstance(joint.getTranslation, types.MethodType) True
There are actually several other types of methods in Python. Fortunately, they are largely unimportant for our purposes here. We'll run through them quickly, in case you want to follow up on your own.
>>> class MyClass(object): ... def mymethod(self): ... pass ... @classmethod # (1) ... def myclassmethod(cls): ... pass ... @staticmethod # (2) ... def mystaticmethod(): ... pass >>> MyClass().mymethod # (3) <bound method MyClass.mymethod of <__main__.MyClass object athh... >>> MyClass.mymethod # (4) <unbound method MyClass.mymethod>
Class methods: These use the
@classmethoddecorator and are called with the type of the instance (
cls) instead of the instance itself. We cover decorators in Chapter 4, Leveraging Context Manager and Decorators in Maya.
Static methods: These use the
@staticmethoddecorator and are called with no
selfargument. I always advise against the use of static methods. Just use module functions instead. And in fact, static methods aren't really methods when you inspect their type; they are functions as described in the next section.
MyClass().mymethodrefers to a bound method. A method is said to be bound when it is associated with an instance. For example, we can say
bound = MyClass().mymethod. When we later invoke
bound(), it will always refer to the same instance of
MyClass.mymethodrefers to an unbound method. Note we're accessing
mymethodfrom the class itself, not an instance. You must call unbound methods with an instance. Calling
MyClass.mymethod(MyClass())is roughly equivalent. You rarely use unbound methods directly.
Methods also have three special attributes that link them back to the type and instance they are bound to. The
im_self attribute refers to the instance bound to the method. The
im_class attribute refers to the type the method is declared on. The
im_func refers to the underlying function.
>>> MyClass().mymethod.im_self <__main__.MyClass object at 0x0...> >>> MyClass().mymethod.im_class <class '__main__.MyClass'> >>> MyClass().mymethod.im_func <function mymethod at 0x0...>
The important one for us is the
im_class attribute so we can get the class for this method.
Let's go ahead and add support for methods. The pattern should be very familiar now. The help string format is just the same for types, but with an additional
"." and the name of the method. The new code is highlighted in the following listing:
def _py_to_helpstr(obj): if isinstance(obj, basestring): return 'search.html?q=%s' % (obj.replace(' ', '+')) if isinstance(obj, types.ModuleType): return ('generated/%(module)s.html#module-%(module)s' % dict(module=obj.__name__)) if isinstance(obj, types.MethodType): return ('generated/classes/%(module)s/' '%(module)s.%(typename)s.html' '#%(module)s.%(typename)s.%(methname)s' % dict( module=obj.__module__, typename=obj.im_class.__name__, methname=obj.__name__)) if not isinstance(obj, type): obj = type(obj) return ('generated/classes/%(module)s/' '%(module)s.%(typename)s.html' '#%(module)s.%(typename)s' % dict( module=obj.__module__, typename=obj.__name__))
>>> def spam(): ... def eggs(): ... pass ... pass
The preceding code contains two functions:
eggs. Basically, any
def that isn't part of a class definition (the first argument is not
cls in regular methods and class methods described previously) is considered a function. In addition to these earlier simple functions,
staticmethods are also functions, as we see in the following code:
>>> get10 = lambda: 10 >>> type(get10) <type 'function'> >>> class MyClass(object): ... @staticmethod ... def mystaticmethod(): pass >>> type(MyClass.mystaticmethod) <type 'function'> >>> type(MyClass().mystaticmethod) <type 'function'>
MyClass.mystaticmethod are considered functions by Python and will be handled by the logic in this section.
The PyMEL help URL for functions is straightforward and in fact very similar to types. Implementing functions has no surprises. The code added for handling functions is highlighted in the following listing:
def _py_to_helpstr(obj): if isinstance(obj, basestring): return 'search.html?q=%s' % (obj.replace(' ', '+')) if isinstance(obj, types.ModuleType): return ('generated/%(module)s.html#module-%(module)s' % dict(module=obj.__name__)) if isinstance(obj, types.MethodType): return ('generated/classes/%(module)s/' '%(module)s.%(typename)s.html' '#%(module)s.%(typename)s.%(methname)s' % dict( module=obj.__module__, typename=obj.im_class.__name__, methname=obj.__name__)) if isinstance(obj, types.FunctionType): return ('generated/functions/%(module)s/' '%(module)s.%(funcname)s.html' '#%(module)s.%(funcname)s' % dict( module=obj.__module__, funcname=obj.__name__)) if not isinstance(obj, type): obj = type(obj) return ('generated/classes/%(module)s/' '%(module)s.%(typename)s.html' '#%(module)s.%(typename)s' % dict( module=obj.__module__, typename=obj.__name__))
Right now we only have tests and support for PyMEL objects. We can add support for non-PyMEL objects by returning
None if our argument does not have a module under the
pymel namespace. The check can be a straightforward function using several of the things we've learned about the special attributes of objects. Add the following function somewhere in
def _is_pymel(obj): try: # (1) module = obj.__module__ # (2) except AttributeError: # (3) try: module = obj.__name__ # (4) except AttributeError: return None # (5) return module.startswith('pymel') # (6)
This logic is based on what we already know about Python objects. All user-defined types and instances have a
__module__ attribute, and all modules have a
__name__ attribute. Let's walk through the preceding code.
We use try/except in this function, rather than checking if attributes exist. This is discussed in the following section, Designing with EAFP versus LBYL. If you are not familiar with Python's error handling mechanisms including try/except, we discuss it more in Chapter 4, Leveraging Context Managers and Decorators in Maya.
__module__attribute (for example, if it is a type or function), we will use that as the namespace.
If it does not, an
AttributeErrorwill be raised, which we catch.
Try to get the
objis a module. There's a possibility
a __name__and no
__module__but is not a module. We will ignore this possibility (see the section Code is never complete later in this chapter).
objdoes not have a
__name__, give up, and return
If we do find what we think is a module name, return
Trueif it begins with the string
def test_py_to_helpstr(): def dotest(obj, ideal): result = _py_to_helpstr(obj) assert result == ideal, '%s != %s' % (result, ideal) dotest('maya rocks', 'search.html?q=maya+rocks') dotest(pmc.nodetypes, 'generated/pymel.core.nodetypes.html' '#module-pymel.core.nodetypes') dotest(pmc.nodetypes.Joint, 'generated/classes/pymel.core.nodetypes/' 'pymel.core.nodetypes.Joint.html' '#pymel.core.nodetypes.Joint') dotest(pmc.nodetypes.Joint(), 'generated/classes/pymel.core.nodetypes/' 'pymel.core.nodetypes.Joint.html' '#pymel.core.nodetypes.Joint') dotest(pmc.nodetypes.Joint().getTranslation, 'generated/classes/pymel.core.nodetypes/' 'pymel.core.nodetypes.Joint.html' '#pymel.core.nodetypes.Joint.getTranslation') dotest(pmc.joint, 'generated/functions/pymel.core.animation/' 'pymel.core.animation.joint.html' '#pymel.core.animation.joint') dotest(object(), None) dotest(10, None) dotest(, None) dotest(sys, None)
minspect and run the test function. The first new test should fail. Let's go in and edit our code in
minspect.py to add support for non-PyMEL objects. The changes are highlighted.
def _py_to_helpstr(obj): if isinstance(obj, basestring): return 'search.html?q=%s' % (obj.replace(' ', '+')) if not _is_pymel(obj): return None if isinstance(obj, types.ModuleType): return ('generated/%(module)s.html#module-%(module)s' % dict(module=obj.__name__)) if isinstance(obj, types.MethodType): return ('generated/classes/%(module)s/' '%(module)s.%(typename)s.html' '#%(module)s.%(typename)s.%(methname)s' % dict( module=obj.__module__, typename=obj.im_class.__name__, methname=obj.__name__)) if isinstance(obj, types.FunctionType): return ('generated/functions/%(module)s/' '%(module)s.%(funcname)s.html' '#%(module)s.%(funcname)s' % dict( module=obj.__module__, funcname=obj.__name__)) if not isinstance(obj, type): obj = type(obj) return ('generated/classes/%(module)s/' '%(module)s.%(typename)s.html' '#%(module)s.%(typename)s' % dict( module=obj.__module__, typename=obj.__name__))
It's important that the
_is_pymel check comes early so we don't try to generate PyMEL URLs for non-PyMEL objects. We now have a relatively complete function we can be proud of. Reload and run your tests to ensure everything now passes.
_is_pymel, we used a try/except statement rather than check if an object has an attribute. This pattern is called Easier to Ask for Forgiveness than Permission (EAFP). In contrast, checking things ahead of time is called Look Before You Leap (LBYL). The former is considered much more Pythonic and generally results in shorter and more robust code. Consider the differences between the following three ways of writing the second try/except inside the
# Version 1 >>> module = None >>> if isinstance(obj, types.ModuleType): ... module = obj.__name__ # Version 2 >>> module = None >>> if hasattr(obj, '__name__'): ... module = obj.__name__ # Version 3 >>> module = getattr(obj, '__name__', None) # Version 4 >>> try: ... module = obj.__name__ ... except AttributeError: ... module = None
Version 1 is thoroughly LBYL and should generally be avoided. We are interested in the
__name__ attribute, not whether
obj is a module or not, so we should look for or access the attribute instead of checking the type. Versions 2 and 3 are using LBYL by checking for the
__name__ attribute but would be an improvement over version 1 since they are not checking the type. These two versions are about the same, with version 3 being more concise. Version 4 is fully EAFP. Use the style of code that results in the most readable result, but err on the side of EAFP.
There is much more to the debate, and we'll be seeing more instances of EAFP versus LBYL throughout this book.
Note that the code in this chapter may not be complete. As Donald Knuth said:
Beware of bugs in the above code; I have only proved it correct, not tried it.
There are likely problems with this chapter's code and undoubtedly bugs in the rest of this book. While math may be provably correct, production code that depends on large frameworks (like PyMEL), which themselves rely on complex, stateful systems (like Maya) will never be provably correct. No practical amount of testing can ensure there are no bugs for edge cases.
But there are many types of bugs. In
_py_to_helpstr, a user can pass in a string that may be illegal for a URL. If this were externally released code, we'd want to handle that case, but for personal or in-house code (the vast majority of what you'll write) it is perfectly fine to have "bugs" like this. When the need arises to filter problematic characters, you can add the support.
In the same way, when we find a PyMEL object that isn't compatible with
_is_pymel, or some object that causes an unhandled error, we can edit the code to solve that problem.
The alternative is to try and write bug-free code all the time while predicting all the ways your code can be called. Good luck!
Understanding that we can't write perfect code is one reason why having automated tests is so important. When we find a use case we need to support, we can just go back to our test function, add the test, make sure our test fails, implement the change, make sure our test passes, and then refactor our implementation to make sure our code looks as good as it can. We cover refactoring in Chapter 2, Writing Composable Code.
Now that all of our tests pass, we can put together our actual
pmhelp function. We need to assemble our generated URL tail with a site, and open it in the web browser. This is actually very simple code because Python comes with so many batteries included. The following code should go into
import webbrowser # (1) HELP_ROOT_URL = ('http://download.autodesk.com/global/docs/' 'maya2013/en_us/PyMel/')# (2) def pmhelp(obj): # (3) """Gives help for a pymel or python object. If obj is not a PyMEL object, use Python's built-in 'help' function. If obj is a string, open a web browser to a search in the PyMEL help for the string. Otherwise, open a web browser to the page for the object. """ tail = _py_to_helpstr(obj) if tail is None: help(obj) # (4) else: webbrowser.open(HELP_ROOT_URL + tail) # (5)
Let's walk through the preceding code.
webbrowsermodule is part of the Python standard library and allows Python to open browser pages. It's awesome!
Define the documentation URL root. Point it to the correct source for your version of Maya and PyMEL.
pmhelpfunction, and give it a docstring because we are responsible programmers.
None, just use the built-in
_py_to_helpstrreturns a string, open the URL.
You can now reload your module once more and try it out, for real.
# Will open a browser and search for "joint". >>> minspect.pmhelp('joint') # Will open a browser to the pymel.core.nodetypes module page. >>> minspect.pmhelp(pmc.nodetypes) # Will print out help for integers. >>> minspect.pmhelp(1) Help on int object: class int(object) | int(x[, base]) -> integer ...
Note that doing
minspect.pmhelp(minspect.pmhelp) is totally valid and will show your docstring. This sort of robustness is the hallmark of well-designed code.
We can also hook up
pmhelp to be available through Maya's interface: select an object, hit the
pmhelp shelf button, and a browser page will open to its help page. Just put the following Python "code in a string" into a shelf button (all one line, broken into two here for print):
"import pymel.core as pmc; import minspect; minspect.pmhelp(pmc.selected())"
If you're not sure how to do this, don't worry. We will look more at using Python code in shelf buttons in Chapter 5, Building Graphical User Interfaces for Maya. Also be aware this code will error if nothing is selected, but you'll see how to better handle high-level calls from shelf buttons in Chapter 2, Writing Composable Code.
You may also notice we didn't write any automated tests for
pmhelp like we did for
_py_to_helpstr. Normally I would, especially if this function grows at all. But for now, it's so simple and would take more advanced techniques so we should be pretty confident to leave it alone.
In this chapter, we learned how Maya and Python work together to create PyMEL. First we learned how to use the mayapy interpreter, and how to create and use Python libraries and modules. Then we explored PyMEL via introspection: how it mirrors Maya concepts such as DAG nodes and attributes, how every Maya object is represented as a first-class PyMEL node, and PyMEL's special math data types. Finally, we built a function that can bring us to the PyMEL online help when we want more information about a PyMEL node. Along the way, we learned about concepts central to Python, such as types, the standard library, magic methods, a definition of the term Pythonic, and easier to ask for forgiveness than permission versus look before you leap.
In the next chapter, we will learn more about writing practical Maya Python with PyMEL by investigating the important concept of composability.