My favorites | Sign in
Project Logo
                
Search
for
Updated Sep 27, 2008 by chadrik
Labels: Featured
Autodesk  
Brain storming changes autodesk could make to imporove pymel integration

What Autodesk Can Do To Improve maya.cmds

Pymel patches many bugs and design problems in maya.cmds, but there are major limitations to maya.cmds which pymel cannot circumvent. These issues must be addressed by Autodesk. Once resolved, the door will be open for a host of new features and speed improvements, not just for pymel but for any 3rd party app.


Exceptions when Calling Mel from Python

The Problem

If maya.mel.eval fails to execute its string as mel code, it does not return any information on how or where the mel script failed. The result is that when a mel script called from a python script fails, there is no traceback information, and it is very difficult to trouble-shoot. The python scripter must setup the same scenario in mel in order to produce a useful traceback to help determine why and where the error is occuring.

The Details

If a mel script fails when executed in the mel environment the user gets a meaningful error message, telling in what script and on what line the error occurred. For example:

global proc example(){
string $f[]= `ls -type foo`;
};
example();
// Error: line 3: Unknown object type: foo //  

But if we now execute this same code via python, the exception message tells us only that the mel script failed and nothing else.

>>> import maya.mel as mm
>>> mm.eval( "string $res[] = `ls -type foo`;")
// Error: line 1: Unknown object type: foo //  
# Error: Error occurred during execution of MEL script
# Traceback (most recent call last):
#   File "<maya console>", line 1, in <module>
# RuntimeError: Error occurred during execution of MEL script # 

The mel error that appears first ( the one that starts with // ) is only available from an interactive session within the script editor. It does not appear when executing maya.mel.eval from within an imported python module.

To further illustrate the problem, imagine I call a mel script that is thousands of lines long.

>>> import maya.mel as mm
>>> try:
>>> 	mm.eval( "myHugeMelCommand()")
>>> except RuntimeError, msg:
>>> 	print "The mel script failed and all I got was this message: %s" % msg
The mel script failed and all I got was this message: Error occurred during execution of MEL script

as a TD responsible for troubleshooting this, I have no clues as to what caused myHugeMelCommand to fail. I now have to attempt to recreate the exact scenario that caused the failure, but this time purely IN MEL, just so i can get a line number and useful error message.

The Solution

The solution is fairly straightforward. Fix maya.mel.eval to return an exception with the mel error message.

Here is how it would work in practice:

>>> import maya.mel as mm
>>> mm.eval( "string $res[] = `ls -type foo`;")
# Error: Error occurred during execution of MEL script
# Traceback (most recent call last):
#   File "<maya console>", line 1, in <module>
# RuntimeError: line 3: Unknown object type: foo  # 

Alternately, a MelError class could be defined, for an even more explicit error:

>>> import maya.mel as mm
>>> mm.eval( "string $res[] = `ls -type foo`;")
# Error: Error occurred during execution of MEL script
# Traceback (most recent call last):
#   File "<maya console>", line 1, in <module>
# MelError: line 3: Unknown object type: foo  # 

Stringifying Arguments

The Problem

Certain commands, when passed instances of classes which are not derived from str or unicode, but which implement str and unicode methods, raise a TypeError. Similarly, iterable classes are rejected if they are not lists or tuples.

The Details

Some commands fail with user classes, and some succeed. Here is a test that shows the move command works, but the rename command fails.

import maya.cmds as cmds

class MyClass(object):
	def __init__( self, object ):
		self.object = str(object)
	def __str__(self):
		return self.object
	def __unicode__(self):
		return self.object

sphereString, hist = cmds.sphere()
sphereObj = MyClass( sphereString )
print
try:
	cmds.rename( sphereObj, "foo" )
except Exception, msg:
	print "rename with custom class failed: '%s'" % msg
else:
	print "rename  with custom class succeeded"

try:
	cmds.rename( sphereString, "foo" )
except Exception, msg:
	print "rename with string failed: '%s'" % msg
else:
	print "rename  with string class succeeded"

The Results:

rename with custom class failed: 'Object nurbsSphere4 is invalid'
rename  with string class succeeded

The Solution

Autodesk must check that they are dealing with string arguments in a consistent manner across all commands. For strings, their code should not be doing any of the following:

isinstance(arg, str)
isinstance(arg, unicode)
isinstance(arg, basestring)

but should instead do:

hasattr( arg, '__str__')

or simply:

try:
  arg = str(arg)
except:
  ...

The problem also exists for iterable arguments, and the solution here is similar:

hasattr( arg, '__iter__')

or

try:
  arg = list(arg)
except:
  ...

Passing an MObject Instead of a String

The Problem

Functions in maya.cmds automatically reject passed arguments of type MObject. Allowing these arguments to be passed in place of a string could increase performance on large loops by an order of magnitude -- providing API level speed within maya.cmds. It would also facilitate a new object-oriented paradigm in maya.cmds.

The Details

maya.cmds is created by autodesk's wrapper around the c++ code responsible for the mel commands. as a result of this heritage, maya.cmds is not capable of receiving object types which cannot nicely resolve to one of mel's data types: string, int, float, etc.

When a command is passed a string argument and needs it converted to an MObject, it must use MSelectionList. The MSelectionList can be populated with a list of strings or MObjects, or it can use currently selected objects.

It is notworthy that the MSyntax object already has some notion of allowing or disallowing MString vs. MObject

From the docs:

MSyntax:: setObjectType( MObjectFormat objectFormat, ...) "Specifies the representation of the objects passed to the command as either MObjects or MStrings. Also specifies the minimum number of objects required by the command."

the first argument for this method is an MObjectFormat enum:

MObjectFormat

on the back-end, its most likely that the mel commands must convert string =>> MObject via an MSelectionList. MSelectionList can generate its list from selected objects or from those passed in to it. The 'add' method is overloaded to support inputs of multiple types: strings, MObjects, MDagPaths, and MPlugs. since python supports lists of mixed types, it would not matter if the input list was a mixture of these accepted types ( e.g. 'mySphere', MObject() ). the end result would always be the same: cycle through the selection list as maya objects.

Speed Test

Below is a demonstration of the order of magnitude speed improvements that could be achieved by opening up this features. The idea behind the test:

The move command can be called with and without a passed object. If the object argument is omitted then the currently selected object is used. In API terms, what this means is that when a string naming an object to be moved is passed to the command, it must first convert this string into an MObject using MSelectionList. Omitting the argument avoids the string-to-MObject lookup, and the MSelectionList retrieves the MObject directly.

This test demonstrates that the move command is dramatically faster when using a selection than when passing a node to operate on. The cause is the string-to-MObject lookup. If autodesk allows MObjects to be passed directly, we remove this bottleneck.

import maya.cmds as cmds
cmds.file( new=1, force=True)
cmds.sphere()
cmds.group()
cmds.group()
cmds.group()
cmds.group()
cmds.group()
cmds.group()
cmds.group()
cmds.group()
cmds.group()
print

startTimer = cmds.timerX()
for t in xrange(2000):
       cmds.duplicate(rr=True)
print 'dupe spheres 2000 times:'.ljust(40), cmds.timerX(startTime = startTimer)

print "-"*60

print "By Selection"
startTimer = cmds.timerX()
cmds.select("nurbsSphere*")
time1 = cmds.timerX(startTime = startTimer)
print 'select Spheres'.ljust(40), time1

startTimer = cmds.timerX()
cmds.move(10,10,10)
time2 = cmds.timerX(startTime = startTimer)
print 'move the selected objects'.ljust(40), time2
print 'total'.ljust(40), time1 + time2
print "-"*60

print "By Passed Argument"
startTimer = cmds.timerX()
obList = cmds.ls(sl=True)
time3 = cmds.timerX(startTime = startTimer)
print 'create list of selected objects'.ljust(40), time3

startTimer = cmds.timerX()
cmds.move(0,0,0, obList)
time4 = cmds.timerX(startTime = startTimer)
print 'move a list of objects'.ljust(40),  time4
print 'total'.ljust(40), time3 + time4

print "-"*60
print 'By Selection is %.02fx faster' % ( (time3+time4)/(time1+time2)  )


The Results:

dupe spheres 2000 times:                 12.46
------------------------------------------------------------
By Selection
select Spheres                           0.0199999999968
move the selected objects                0.23
total                                    0.249999999996
------------------------------------------------------------
By Passed Argument
create list of selected objects          0.119999999999
move a list of objects                   23.18
total                                    23.3
------------------------------------------------------------
By Selection is 93.204x faster

The Solution

Autodesk must change its wrapper to allow MObjects to be passed in place of strings.

In addition to accepting MObjects, the python commands could look for a special method __mobject__ where any user class could place the MObject instance.

for example:

def handleStringArg( arg ):
   if isinstance(arg,MObject):
      doItMObject( arg )
   elif hasattr( arg,'__mobject__' ):
      if not isinstance(arg.__mobject__,MObject):
         raise TypeError, "special method __mobject__ must contain an MObject instance"
      doItMObject( arg.__mobject__ )
   elif hasattr( arg,'__str__' ) or hasattr( arg,'__unicode__' ):
      doItString( arg )
   else:
      raise TypeError, "A string or MObject is required"

Command Syntax Introspection and Specificity

The Problem

Information on the syntax of functions within maya.cmds is not accessible from within python. As a result pymel is unable to generate documentation strings and node classes for custom commands and nodes.

Pymel's object-oriented design rests on pairing a maya node with a mel command, and then wrapping that command as a class. Unfortunately, the MSyntax class which defines a command's syntax is very loose, so most of the flag handling logic -- such as setting defaults and pairing primary flags with secondary flags -- is coded by hand by the programmer of the command. The result is that this additional level of syntax specificity is unstandardized and inaccessible.

The Details

Currently, pymel relies heavily on parsing maya's html documentation to find out information about commands and their flags, but obviously this is not possible for custom commands and nodes.

In order to provide the same pymel features to custom commands on plugin load, we need to know certain information.

Required Command Information (currently parsed from docs, or maintained manually)

Optional Command Information

The MSyntax class has no concept of secondary flags, per flag modes, flag defaults, or command-node pairs, so even if the C++ to Python command generator could add documentation to the doc string of each command, it could not provide any more information than is retrieved from the mel help command, which is insufficient.

The Solution

Provide a higher level MSyntax and MArgParser pair with support for per-flag mode info (queryable, editable), modifier flags, and command-node pairs.

class MSuperSyntax( OpenMaya.MSyntax ):
   def makeFlagQueryable(self, flag): ...
   def makeFlagEditable(self, flag): ...
   def addSecondaryFlag(self, primaryFlag, shortname, longname, arg1, arg2,... ): ...
   def setFlagDefault(self, val): ...
   def setNodeCounterpart(self, id): ...
   

Once these classes are provided, there are two ways to make the information available:

  1. Add human readable and parsable documentation to the doc string of each command.
  1. Provide an API method for retrieving an MSuperSyntax info object for a particular command. The object could then be used in a programmatic way to query the desired information.

In either case, adding greater specificity to the MSyntax of a command has the added bonus of protecting the programmer from the often tedious task of coding exceptions for all the flag scenarios that are not supported. For instance, with the current MSyntax one can designate that a command as a whole is queryable, but not which specific flags -- certain flags might only be editable. Rejecting these exceptions is up to the programmer to code manually and the user to find out through trial and error (since the help command in python and mel is of no help in this regard). Similarly, the onus is on the programmer to sort out primary and secondary flag compatibility. A higher level MSyntax and MArgParser pair with greater syntax control would provide a standardized interface for programmers to specify compatible flag combinations while simulataneously providing greater syntax information to the user of the command through more detailed help.


Sign in to add a comment
Hosted by Google Code