PyMEL for Maya¶
PyMEL makes python scripting in Maya work the way it should. Maya’s command module is a direct translation of MEL commands into python functions. The result is a very awkward and unpythonic syntax which does not take advantage of python’s strengths – particularly, a flexible, object-oriented design. PyMEL builds on the cmds module by organizing many of its commands into a class hierarchy, and by customizing them to operate in a more succinct and intuitive way.
Chapters:
What’s New¶
Version 1.0.7¶
Changes¶
- general: MultiAttrs now support __delitem__
- uitype: use PySide in toQtObject methods if no PyQt
- language: added mel.globals alias for melGlobals
- core: improve API undo callbacks
- system: saveFile resets name if filename is messed up
- system: make loadReference give more informative error if ref not loaded
- system: optimized FileReference.__init__
- system: switched file cmd used by iterReferences for more stable referenceQuery
- docs: improved the look of the docs
- util.arrays: Array objects now explicitly unhashable, added totuple
- utils.path: allow the pattern argument for various methods to take a compiled regular expression pattern.
Additions¶
- language: added Env.playbackTimes convenience property for getting/setting all timeline values at once
- allapi: added example usage to SafeApiPtr
- general: added Attribute.iterDescendants
- system: added mode and caseSensitive args to Translator.fromExtension
- nodetypes: added topLevel and descendants kwargs to DependNode.listAttr
- nodetypes: added Camera.isDisplayGateMask method
- nodetypes: added closestPolygon keyword arg to getUVAtPoint
- uitypes: added toPySide* functions for casting maya UI strings to PySide objects
- pymel.conf: added option to source initialPlugins.mel
- pymel.conf: added option to prefer PyQt4 or PySide
- util.arguments: added support for sets, useChangedKeys to compareCascadingDicts
- util.common: added inMaya() func
- stubs: make stub Mel.__getattr__ accept anything
- stubs: added PySide stubs
Bugfixes¶
- system: fixed bug with referenceQuery which occured when editStrings and liveEdits flags are True
- system: fixed bug with ReferenceEdit.remove() where self.rawEditData was referred to as a method instead of a property.
- ipymel: pressing ctrl-c no longer quits maya
- logging: fixed bug preventing logging menus from displaying
Version 1.0.6¶
Non-Backward Compatible Changes¶
- joint.limitSwitchX/Y/Z: now return [bool, bool] when queried, just like the mel command, to indicate whether the limit is on for the min/max
- joint.radius: now returns the float radius, instead of [radius]
Changes¶
- general: addAttr/setEnums now accept strings, lists, or dicts for setting enums
- other: cast NameParser arguments to unicode
- factories: issue deprecation warning for deprecated functions only when they’re actually used
Additions¶
- added (functional) namespace method to Attribute, Component
- general: added mute accessors to Attributes
- system: provide ReferenceEdit.rawEditData property for getting faster unparsed access to reference edits
- nodetypes: added stripUnderworld flag to DependNode.nodeName(), and default it to true. This removes the underworld prefix (the node prior to ->) from nodeName().
- ipymel: update for ipython 0.11
- util.arguments: compareCascadingDicts can show which keys have been added (as opposed to just changed)
- system: added workspace.expandName
Bugfixes¶
- general: for the keyframe command, the upper / lower limits substituted in for the “index” flag, when no upper/lower limits were given, are now correct; formerly, the behavior of index=”:” would vary depending on where (in time) the first and last keyframes were
- general: timerange flags (ie, keyframe(time=...), findKeyframe(time=...), etc) now provide correct results when no upper/lower limits given; formerly, the time=”:” would vary depending on if / which objects were selected
- animation: Joint.angleX/Y/Z and Joint.stiffnessX/Y/Z now work
- nodetypes: Container.getParentContainer, Container.getRootTransform, and Character.getClipScheduler now all return None instead of raising a runtime error if no object was found
- system: FileReference.fullNamespace and iterReferences/listReferences with namespaces=1 now handle situations where reference node itself is in a non-root namespace correctly
- general: when instantiating PyNode from an MPlug, to get the PyNode for the node, create from the underlying mobject, not the name (which may not be unique)
- animation: fix for Joint.getAngleX/Y/Z, .getStiffnessX/Y/Z; joint.radius no longer returns list
- mel2py: numerous fixes / tweaks
- system: handle situations where reference node itself is in a non-root namespace
- system: FileReference.nodes fix when reference contains no nodes
- general: duplicate - special-case workaround for duplicating a single underworld node with no children
- general: fix for duplicate + non-unique names
- general: duplicate - workaround for bug introduced in 2014
- nodetypes: fix for getAllParents with underworld nodes
- Upgrade path.py to version 5.0 from github (https://github.com/jaraco/path.py). This fixes an issue with Maya2014, python 2.7.3, and Windows where path().isdir() raised an error.
- general: cmds.group returns unique name in maya > 2014
- fix for virtual classes
- versions: parsing for ‘Preview Release’ format - from Dean Edmonds
Version 1.0.5¶
Non-Backward Compatible Changes¶
- DagNode.isVisible: has a new flag, checkOverride, which is on by default, and considers visibility override settings
- referenceQuery/FileReference.getReferenceEdits: if only one of successfulEdits/failedEdits is given, and it is false, we now assume that the desire is to return the other type (and set that flag to true); formerly, this would result in NO edits being returned
- parent no longer raises an error if setting an object’s parent to it’s current parent; this makes it behave similarly to the mel command, and to DagNode.setParent
- renameFile now automatically sets the ‘type’ if none is supplied (helps avoid renaming a file to ‘foo.ma’, then saving it as ‘mayaBinary’)
- general: 1D components have index() method: can no longer use string.index()
- uitypes: make PyUI.parent return None instead of PyUI(‘’)
Changes¶
- for maya versions >= 2012, creation of “ghost” plugin nodes no longer needed
- general: change to Component to speed up len(PyNode(‘pCube1Shape’).vtx)
- general: parent and DagNode.setParent now share common codebase
- general: when find unknown component type, default to just printing a warning and returning generic Component
- general: demoted raiseLog warning about unknown component type to DEBUG
- general: added uniqueObjExists function
- general: speedup for string representation of complete MeshVertexFace
- general: made listRelatives/listHistory/listConnections have same behavior for None and empty list
- system: clarified doc not about removeReferenceEdits not erroring
- system: FileReference.replaceWith - enable kwargs
- system: renameFile automatically sets type
- system: changed referenceQuery so when only one of successful/failed passed, other flag is opposite value
- language: make catch take args/kwargs
- nodetypes: attrDefaults - use MNodeClass in versions >= 2012, _GhostObjMaker otherwise
- nodetypes: Transform.setRotation now takes args as EulerRotation, Quaternion, or iterable of 3 or 4 elements
- nodetypes: isVisible checks overrideVisibility
- stubs: catch more dict-like-objects; special case exclude for maya.precomp.precompmodule
- stubs: create dummy data objects when safe; better handling of builtins
- stubs: use static code analysis to decide whether to include a child module in a parent module’s namespace
- stubs: better representations for builtin data types
- stubs: get all names in module, better ‘import *‘ detection
- plogging: added raiseLog func/method
- plogging: small tweaks to way default ERRORLEVEL is set, and raiseLog is added onto loggers
- ipymel: make sure stuff imported into global namespace in userSetup.py is available in IPython
Additions¶
- nodetypes: added stripNamespace option to DependNode.name
- general: disconnectAttr - support for disconnecting only certain directions
- general: MeshFace - added numVertices as alias for polygonVertexCount
- general: added DiscreteComponent.totalSize method
- general: added ParticleComponent class
- other: added DependNodeName.nodeName (for compatibility with DagNodeName)
- nodetypes: added DagNode.listComp
- datatypes: added equivalentSpace
- utilitytypes: proxyClass - added module kwarg to control __module__
- system: added FileReference.parent()
- system: listReferences - added loaded/unloaded kwargs
- system: added UndoChunk context manager
- system: Namespace.remove/.clean - added reparentOtherChildren kwarg
- system: added support for regexps to path.listdir/.files/.dirs
- system: added successful/failedEdits flags to FileReference.removeReferenceEdits
- windows: confirmBox - added returnButton kwarg to force return of button label
- plugins: added an example for creating plugin nodes
- util.enum: added Enum.__eq__/__ne__
- py2mel: added include/excludeFlagArgs
- system: added proper hash function for FileReference
Bugfixes¶
- general: fix for potential crashes due to using cached/invalid MFn
- general: fix pm.PyNode(‘pCube1.vtx[*]’)[2] to work like like pm.PyNode(‘pCube1’).vtx[2]
- general: fix for HashableSlice comparison (fixes bug with component indexing)
- general: Component - fixes for complete-component shortcut don’t use with empty meshes don’t use for subd components (including SubdUV) use ffd1LatticeShape.pt[*], not .pt[*][*][*]
- general: SubdEdge - hack to avoid a maya bug which causes crash
- language: MelGlobal.initVar now initializes in mel
- language: remove annoying callback error spam; instead make info available in a log from Callback.printRecentError()
- uitypes: fix for 2012 SP2 issue with objectTypeUI not working for windows with menu bars
- nodetypes: Transform.setRotation - fix for setting with EulerRotation object and non-standard rotation order or unit
- nodetypes: fix for ObjectSet.__len__
- nodetypes: AnimLayer.getAttribute - query dagSetMembers.inputs() to get full/unique path
- nodetypes: fix typo in name of NurbsCurve/Surface.controlVerts (not conrolVerts)
- core: _pluginLoaded - added fix for addPluginPyNodes triggered on reference load (fix for 2012+ only)
- core: fix erroneous ‘could not find callback id’ warnings
- utilitytypes: universalmethod now has doc pulled from original func
- util.conditions: bugfix for __ror__, added __str__
- allapi: toApiObject - low-level fix for Nucleus attributes
- startup: don’t use fixMayapy2011SegFault in >= 2013, seg fault was addressed by Autodesk
- stubs: fixes for objects with multiple aliases in a module
- py2mel: bugfixes, bugfix for excludeFlagArgs
Version 1.0.4¶
Changes¶
- core.uitypes: improved AETemplates to work when created from within a scripted plugin
- tools.mel2py: now output exact same filename as input on Windows
- core.nodetypes: Transform.getRotation - can get as euler or quaternion
- extras: improved reliability of stub files (for pydev, wing, etc)
- core: doing select([], replace=True) should clear selection
- api.allapi: replace toMObjectName with MObjectName
- core: namespace - root option is now False (for backward compatibility)
- core: MeshVertex.setColors - set colors for all verts in MeshVertex
- core: re-implement noIntermediate flag to listRelatives
- plogging: PYMEL_LOGLEVEL env var now sets minimum level for all pymel loggers
- core: use new 2012 pluginInfo flags for getting more command types
- core.windows: PopupError can now raise another exception type
- examples: update customClasses.py example
Additions¶
- util.path: added boolean normcase keyword arg to path.canonicalpath()
- api.plugins: added in classes for all MPxNode classes and methods for querying class / MPx to MPx enum mappings
- api.plugins: added new overridable methods which generate node callbacks: timeChagned, forcedUpdate, nodeAdded, nodeRemoved, preConnectionMade
- versions: added maya2012 hotfix 1,2,3,4
- core: Attribute.setDirty / evaluate
- core: DependNode.rename() now supports pyMel unique flag preserveNamespace
- core: added check to ensure name passed to DependNode.rename() is shortname
- core: implemented DependNode.rename() flags: i.e. ignoreShape can now be used
- core.uitypes: added Layout.findChild() which takes the shortname of a child as a string and returns the PyUI object
Bugfixes¶
- mayautils: fix so recurseMayaScriptPath, when given explicit roots, doesn’t wipe out old paths
- core: fixed bug where __pymelUndoNode was created in non root namespace
- tools.pymelScrollFieldReporter: use mel2py.melparse (issue 247)
- core: fixed FileReference.importContents(removeNamespace=True)
- core: _pluginLoaded callback now correctly triggered by importing
- core: fix promptForPath doesn’t work for mode 1/100 due to testing for the existance of the path.
- core.nodetypes: fix for DependNode.rename(preserveNamespace=True) when node in root namespace
- core.nodetypes: fixed bug with RenderLayer.added/removeAdjustments
- core.nodetypes: fix for DagNode.getAllParents (and test)
- core.nodetypes: fix for DependNode.hasAttr(checkShape=False)
- core.nodetypes: fix for AnimCurve.addKeys (issue 234)
- internal.startup: fix for error message when fail to import maya.cmds.about
- core: fixed addAttr(q=1, dataType=1) so it does not error if non-dynamic attr
- core: pythonToMelCmd - fixed bug when flagInfo[‘args’] was not a class
- core: pythonToMelCmd - fix for flags where numArgs > 1
- maya.utils: formatGuiException - fix for, ie, IOError / OSError
- updated 2012 caches to fix issue 243
Version 1.0.3¶
Changes¶
UI classes that have ‘with’ statement support now set parent back to previous ‘with’ object if there are nested with statements; if not in a nested with statement, resets parent back to UI element’s parent (or more precisely, the first element that is not a rowGroupLayout element)
with OptionMenuGrp()
will set parent menu properly‘Unit’ support for Quaternion objects is now removed (as it doesn’t make any sense)
can now pass in PyNode class objects to functions / methods that expect a mel node class name - ie:
listRelatives(allDescendents=True, type=nt.Joint)
is equivalent to:
listRelatives(allDescendents=True, type=’joint’)
other: NameParser(dagObj) now always gives a DagNodeName even if shortName has no |
Non-Backward Compatible Changes¶
- PyNode(‘*’) - or any other non-unique name - now returns an error use ls(‘*’) if you wish to return a list of possible nodes
- By default, the root pymel logger outputs to sys.__stdout__ now, instead of sys.stderr; can be overriden to another stream in sys (ie, stdout, stderr, __stderr__, __stdout__) by setting the MAYA_SHELL_LOGGER_STREAM environment variable
- skinCluster, tangentConstraint, poleVectorConstraint, and pointOnPolyConstraint commands now return a PyNode when creating, instead of a list with one item
- skinCluster command / node’s methods / flags for querying deformerTools, influence, weightedInfluence now return PyNodes, not strings
- Attribute.elements now returns an empty list instead of None
- general: Attribute.affects/affected return empty list when affects returns None
- setParent returns PyUI / None; menu(itemArray) returns [] for None
- general: make Attribute.elements() return empty list for None
- shape attribute lookup on all child shapes (like mel does)
Additions¶
- Shape.setParent automatically adds –shape flag
- nodetypes: added isVisible
- added MGlobal.display* methods to pymel.core.system namespace
- other: added NameParser.stripGivenNamespace()
- language: OptionVarList has more helpful error message when __setitem__ attempted
- nodetypes: getSiblings can now take kwargs
- Added MainProgressBar context manager
- Added isUsedAsColor method to Attribute class
- Added wrapper for listSets function
- Added method listSets to PyNode class
- Added a folderButtonGrp
- system: added Namespace.move
- system: added Namespace.listNodes
- mel2py: python mel command now translated to pymel.python (ie, maya.cmds.python)
- general: added Attribute.indexMatters
- language: added animStart/EndTime to Env
- system: added in a ‘breadth’-first recursive search mode to iterReferences
- general: added ability to set enum Attributes with string values (issue 35)
- plogging: set logging level with PYMEL_LOGLEVEL env var
- Added isRenderable() method to object set.
- deprecate PyNode.__getitem__
- mayautils: executeDeferred now takes args, like maya.utils.executeDeferred
Bugfixes¶
- py2mel failing with functions that take *args/**kwargs
- eliminated / fixed various ‘warning’ messages on pymel startup
- MayaNodeError / MayaAttributeError not being raised when a node / attribute not found
- some maya cmds were not handling ‘stubFunc’ correctly
- renderLayer.listAdjustments() was not functioning
- MainProgressBar fixed
- language: OptionVarList __init__ no longer raises deprecation warning
- listSets() throws away non-existant ‘defaultCreaseDataSet’ that maya.cmds.listSets() returns
- fix for dealing with maya bug where constraint angle offsets always returned in radians (but set in degrees)
- fixes for incorrect formatting of error strings in some cases
- fixes for unloading of commands/nodetypes when plugins unloaded (and pymel.all was imported first)
- miscellaneous documentation fixes
- fix for mayautils.executeDeferred when invoked with args
- fix for Attribute.getAllParents()
- fix for aliased multi/compound attributes
- fix for Attribute.isSettable with multi/compound attributes
- fix for Attribute.exists with multi/compound attributes
- fix for Attribute.type with multi/compound attributes dynamic attributes
- fix for published container node attributes / aliases
- fixes for plugin callback failing when plugin has uncreate-able nodes
- fixes for multiple iterators of a mutli-attribute not being independent
- fix for MeshVertex.setColor
- fix for MeshVertex.isConnectedTo
- fix for MeshVertex.getColor
- fix for MeshEdge.isConnectedTo
- fix for MeshFace.isConnectedTo
- fix for plogging handling case where various env. variables exist, but are empty
- Fix for Layout.children() Layout.children() now returns empty list if layout has no kids intead of raising error.
- listConnections: fix so rotatePivot always Attribute (not component)
- uitypes: bugfixes to AETemplates. corrected UITemplate to represent an existing uiTemplate if instantiated with the name of an existing template
- nodetypes: fixed a bug where Transform.setScalePivot was internally using MFnTransform.setScalePivotTranslation
- fixed a bug in pythonToMel where python booleans were not converted to integer. this caused the Mel class to not work properly with booleans.
- core.general: fix a bug with sets command where noWarnings was interpreted as a set flag, instead of a boolean flag
- Namespace: fix for getParent()
- general: various attr name fixes (stripping of [-1] indices, etc)
- nameparse: enable parsing of [-1] indices (for attributes)
- nodetypes: enable parsing of [-1] indices (for attributes)
- nodetypes: setParent to current parent no longer errors
- util.enum: fix for repr of EnumDict
- fixes for referenceQuery
- attr.exists() should return False if the node no longer exists
- datatypes: fixed bug to allow Point * FloatMatrix
- general: bugfix for Attribute.attrName
- utilitytypes: EquivalencePairs.get now correctly retrieves value=>key
- nodetypes: fixed setParent(world=1) bug
- uitypes: Fix issues with the popup and with support.
- pm.mel.command translation would fail with no-arg bool flags (like -q, -e)
- language: mel command translation makes no assumptions for unknown commands; None is translated to empty string, not ‘None’
- bugfix for uiTemplate(exists=1)
- general: Attribute.elements() now correctly works with array and element plugs
- fix get/set rotation by using eulerRotation
- startup: changes to fix issues with maya -prompt and plugins loading pymel
- fix for TransformationMatrix.get/setRotation, removed Quaternion units
- datatypes: fixes for EulerRotation
- fix for ui heights for pymelControlPanel
- uitypes: bugfix for with statement parent setting on exit
- mesh: fixes to allow creating component objects for empty meshes (ie, createNode(‘mesh’).vtx)
- mesh: made more num* functions work with empty meshes
- core.general: fix for move with no object
- datatypes: fix for EulerRotation comparison/len
- fix for menu(‘someOptionMenu’)
- FileReference: initialize correctly from a path
- windows: bugfix - informBox wasn’t using ‘ok’ kwarg
- plogging: bugfix for 182 - crash due to creating loggers as iterating over dict
- arrays: fix for dot/outer product error messages (issue 158)
- fix for ‘no useName’ and MfknSkinCluster.setBlendWeights warnings on startup
- Fixed language import in MainProgressBar
- fix for Issue 216: renderLayer.listAdjustments()
- docfix for issue 192
- fix for constraint angle offset query always being in radians
- nodetypes: fix for multi/compound alias attrs
- nodetypes: fixes for published container node attributes / aliases
- general: made attribute iterator independent
- general: fix for isSettable with multi/compound attributes
- general: fix so getAllParents doesn’t return orig object
- general: fix for Attribute.exists with multi/compound attrs
- Attribute.type() now works with multi/compound, dynamic attrs
- fixes for mesh components
Version 1.0.2¶
Changes¶
- rolled back
listConnections()
change from 1.0.1
commands wrapped to return PyNodes¶
container()
Additions¶
- added functions for converting strings to PyQt objects:
toQtObject()
,toQtLayout()
,toQtControl()
,toQtMenuItem()
,toQtWindow()
- added method for converting PyMEL UI objects to PyQt objects:
UI.asQtObject()
Bugfixes¶
- fixed a bug where
nt.Conditions()
created a script condition
Version 1.0.1¶
Changes¶
listConnections
: when destination is shape, always returns shape (not transform)select([])
only clears selection if mode is replace- deprecated
Attribute.firstParent()
Additions¶
mel2py
: now does packages/subpackages for recursed mel subdirectories- added various dict-like methods to OptionVarDict
- added new EnumDict support which
Attribute.getEnum
returns - added support to
getAttr()
/Attribute.get()
for getting message attributes, which are returned as DependNodes - added
core.system.saveFile()
- added
pymel.versions.is64bit()
- added new directory helpers to mayautils:
getMayaAppDir()
,getUserPrefsDir()
, andgetUserScriptsDir()
- added
DependNode.longName()
,DependNode.shortName()
, andDependNode.nodeName()
for easy looping through mixed lists of DependNodes and DagNodes - added
FileInfo.__delitem__()
- added
DependNode.deleteAttr()
Bugfixes¶
- unloading plugins no longer raises an error
- python AE templates were not being found. fixed.
- fixed a bug in api wrap, where
MScriptUtil
was not allocating space - fixed a bug with
Transform.setMatrix()
pymel.versions.installName()
is more reliable on 64-bit systems, which were sometimes detecting the installName incorrectlyAttribute('mytransform.scalePivot')
now returns an the scalePivot attributegetAttr()
/Attribute.get()
bugfix with multi-attrnodetypes
: fixed bug 172 where nested selection sets were raising an error when getting membersgetPanel
now always return panelsuitypes
: all panel classes now properly inherit from Panel- fixed some keywords that had been mistakenly refactored
core.general
: fixed a bug where dependNodes were not returned when duplicated
Version 1.0.0¶
Non-Backward Compatible Changes¶
- pymel no longer has ‘everything’ in namespace - use
pymel.all
for this pymel.core.nodetypes
now moved to it’s own namespacepymel.mayahook.Version
functionality moved topymel.versions
module. to compare versions, instead of Version class, use, for example,pymel.versions.current()
>=pymel.versions.v2008
pymel.mayahook.mayautils.getMayaVersion()
/getMayaVersion(extension=True)
replaced withpymel.versions.installName()
pymel.mayahook.mayautils.getMayaVersion(extension=True)
replaced withpymel.versions.shortName()
- removed 0_7_compatibility_mode
- removed deprecated and inapplicable string methods from , base of all PyNodes:
- removed Smart Layout Creator in favor of ‘with’ statement support
DagNode.getParent()
no longer accepts keyword arguments- Renamed
UI
base class toPyUI
sceneName()
now returns a Path class for an empty string when the scene is untitled. this makes it conform more tocmds.file(q=1, sceneName=1)
- replaced listNamespace with listNamespace_new from 0.9 line
removed deprecated methods¶
Attribute
:__setattr__
,size
Camera
:getFov
,setFov
,getFilmAspect
Mesh
:vertexCount
,edgeCount
,faceCount
,uvcoordCount
,triangleCount
SelectSet
:issubset
,issuperset
,update
- Mesh components:
toEdges
,toFaces
,toVertices
ProxiUnicode
:__contains__, __len__, __mod__, __rmod__, __mul__, __rmod__, __rmul__, expandtabs, translate, decode, encode, splitlines, capitalize, swapcase, title, isalnum, isalpha, isdigit, isspace, istitle, zfill
Features¶
- added support for creation of class-based python Attribute Editor templates, using
ui.AETemplate
- added ‘with statement’ compatibility to UI Layout and Menu classes
- added the ability to generate completion files for IDEs like Wing, Eclipse, and Komodo
Tools¶
ipymel
: added colorization to dag commandpy2mel
: now works with lambdas and methods. new option to provide a list or dictionary of mel types.- re-added missing scriptEditor files
- added upgradeScripts, a tool for converting 0.9 scripts to be 1.0 compatible
Changes¶
- moved functions for working with the shell into
util.shell
- split out ui classes from
core.windows
intocore.uitypes
for lazy loading - for versions >= 2009, use open/close undo chunks instead of mel hack to ensure that an entire callback can be undone in one go
- improved
lsUI()
- moved component types out of nodetypes and into general
__repr__
for nodetypes, uitypes, and datatypes reflect their location so as not to cause confusion. using short module names nt, ui, and dt.- caches are now compressed for speed
- allow setting
pymel.conf
location via environment variable PYMEL_CONF DagNode.getBoundingBox()
now allows you to specify space- ensured that the ‘name’ flag for surface and curve operates on shape as well
- changes to allow
myCube.vtx[1,3,5]
- commands wrapped by pmcmds that raise a standard TypeError for a non-existent object will now raise a MayaObjectError
- simplified getParent code on Attribute and DagNode to improve function signatures.
- fixed a bug with
ls(editable=1)
- fixed a bug with ObjectSets containing DagNodes
- callbacks: extra debug information is printed in tracebacks
commands wrapped to return PyNodes¶
skinCluster(q=1, geometry=1)
addAttr(q=1, geometry=1)
addDynamic()
addPP()
constraint()
animLayer()
annnotate()
arclen()
art3dPaintCtx()
artAttrCtx()
modelEditor(q=1,camera=1)
dimensionShape()
Additions¶
- added
TwoWayDict
/EquivalencePair
toutilitytypes
- added
preorder()
,postorder()
, and``breadth()`` functions inutil.arguments
, which have more intuitive arguments - added new
Layout
class that all layouts inherit from - added
UITemplate
class - added usable
__iter__
to workspace dict / file dict objects - added two tier setup scripts for maya (user/site) just like with python. This new
siteSetup.py
is intended for studio setup of maya and reserveduserSetup.py
for user level scripts. - added a partial replacement maya package with a logger with a shell and gui handler qne changed plogging to use the new default maya logger
- added
setAttr
/getAttr
support for all numeric datatypes, along with tests - added
Transform.getShapes()
for returning a list of shapes - added
FileReference
comparison operators - added
DependNode.longName(stripNamespace=False,level=0)
- added
SkinCluster.setWeights()
- added
AnimCurve.addKeys()
- added regex flag to ls command
- added
FileInfo.get()
- added
util.common.subpackages()
function for walking package modules - added
util.conditions.Condition
class for creating object-oriented condition testing pymel.conf
: added a fileLogger- added
Path.canonicalpath()
andPath.samepath()
- mel2py: added command-line flags, ability to recurse
added support for attribute aliases¶
DependNode.attr()
now casts aliases to Attributes properly (PyNode already does)- added
DependNode.listAliases()
- added ‘alias’ keyword to
DependNode.listAttr()
- added
Attribute.setAlias()
,Attribute.getAlias()
Bugfixes¶
- fixed instantiation of PyNode from MPlug instance
- fixed a bug where Maya version was incorrectly detected when Maya was installed to a custom location
- fixed bug where wrap of function which took multiple refs all pointed to same
MScriptUtil
- fixed wrapping of unsigned ptr api types
- fixed negative comp indices
mel2py
: bugfix withmel2pyStr()
Version 0.9.2¶
Changes and Additions¶
- added support for 2010 and python 2.6
- added basic support for all component types
- added a ‘removeNamespace’ flag to
FileReference.importContents()
- added support for open-ended time ranges for command like keyframes (Issue 82)
- enhanced
keyframe
function: if both valueChange and timeChange are queried, the result will be a list of (time,value) pairs - added ability to pass a list of types to
ls
‘type’ argument, as you can withlistRelatives
- added checkLocalArray and checkOtherArray arguments to
Attribute.isConnectedTo
which will cause the function to also test mulit/array elements - improved
core.language.pythonToMel()
reliability on lists - improved custom virtual class workflow
- added functionality to
pymel.tools.py2mel
for dynamically creating MEL commands based on python functions/classes - added a new module
pymel.api.plugins
for working with api plugins in a more reasonable and automated fashion - updated eclipse integration documentation
easy_install improvements¶
- setup now copies over a readline library for 2010 OSX using
readline.so
from toxik which is more compatible - changed ipymel to be part of the default install instead of an extra package
- fixed interpreter path of ipymel and other executable scripts on OSX
- setup now detects and fixes invalid python installations on Linux which previously caused
distutils
and thussetup.py
to fail
Bugfixes¶
importFile()
,createReference()
,loadReference()
, andopenFile()
now return PyNodes when passed returnNewNodes flag (Issue 85)- fixed rare bug with Vista where
platform.system
was failing during startup (Issue 87) - fixed a bug with plugin loading to intelligently handle when callback does not get a name
- fixed
optionMenu
andoptionMenuGrp
to return empty lists instead of None - restored
core.other.AttributeName.exists()
method - fixed a bug in 0.7_compatibility_mode
- fixed minor bug in
listRelatives()
- fixed a bug where curve command was returning a string instead of a PyNode (Issue 96)
Version 0.9.1¶
Changes and Additions¶
- new feature: virtual subclasses. allows the user to create their own subclasses which are returned by
PyNode
- added
v2009sp1
andv2009sp1a
toVersion
- changed
MelGlobals.__getitem__
to raise a KeyError on missing global, instead of a typeError util.path
now supports regular expression filtering in addition to globs.- moved
moduleDir()
fromutil
tomayahook
since it is explicitly for pymel. - ensured that all default plugins are loaded when creating the api cache so that we can avoid calculating those each time the plugins are loaded
- added a new
errors
flag to recurseMayaScriptPath for controlling how to handle directory walking errors: warn or ignore - moved
pwarnings
to ensure thatpymel.util
is completely separated from maya - adding new sphinx documentation. modifying source docstrings where necessary.
- setParent now allows
None
arg to specify world parent - adopted a standard setuptools-compliant package layout, with pymel as a subdirectory of the top level
- forced line numbers on for
Mel.eval
- changed ipymel to use $MAYA_LOCATION to find mayapy instead of /usr/bin/env
- changed datatypes examples to demonstrate the necessity to include a namespace
- added
groupname
,get_groupname
, andchgrp
toPath
class for dealing with unix groups as strings instead of as gid’s - added alias
path.Path
forpath.path
so as to follow PEP8 - added a new option to
pymel.conf
to allow disabling of mel initialization in standalone mode. - added ability to set logger verbosity using PYMEL_LOGLEVEL environment variable. great for quick testing.
Bugfixes¶
- fixed a bug in
undoInfo()
- fixed a bug that was breaking
mel2py
- fixed a bug with logging that was locking it to INFO level. INFO is now the default, but it can be properly changed in
pymel.conf
- fixed input casting of
datatypes.Time
- bug fixes in error handling within path class
- fixed issue 65:
DependencyNode.listAttr()
broken - made sure
NameParse
objects are stringified before fed toMFnDependencyNode.findPlug()
- added a few more reserved types so as to avoid creating them, which can lead to crashes on some setups
- fixed issue 66 where nodes could be created twice when using PEP8 style class instantiation:
pm.Locator
path.walk*
methods now properly prune all directories below those that do not match the supplied patterns- maya bug workaround: changed pluginLoaded callback to API-based for 2009 and later
- fixed bug in
hasAttr()
- removed bug in
arrays.dot
where incorrect duplicate definition was taking precedence - fixed bug in
PyNode.__ne__()
when comparing DagNodes to DependNodes - fixed Issue 72: cannot select lists of components
- fixed bug with startup on windows (backslashes not escaped)
- fix for
Component('pCube1.vtx[3]')
- fix for nurbsCurveCV(‘nurbsCircle1’) failing
- pythonToMel and Mel now properly convert
datatypes.Vectors
to mel vectors ( <<0,0,0>> ).MelGlobals
now returnsdatatypes.Vectors
- fixed bug with
duplicate(addShape=1)
- fixed a bug where selectionSets can’t be selected
- fixed a bug with
sets()
when it returns lists - fixed issue 76, where non-unique joint names were returned by
pymel.joint
and thus were unsuccessfully cast tonodetypes.Joint
- fixed issue 80, regading incorrect association of
nodetypes.File
withcmds.file.
- fixed a bug in
connectAttr()
that was preventing connection errors from being raised when the force flag was used
Installation¶
Supported Platforms¶
Our goal is to support the 3 latest versions of Maya.
PyMEL Package¶
Easy Install¶
As of version 0.9.1 PyMEL supports installation via setuptools, which makes it a lot easier to get started if you’re not well-versed in the intricacies of the PYTHONPATH
. If you don’t already have setuptools installed it will be downloaded for you when you run the install command below, but in order for this to all go smoothly you must have the following things:
- a connection to the internet
- write permission to your Maya installation directory
If you fall short on either of these you can always perform a Manual Install of PyMEL.
Installation on OSX¶
Open a shell: you’ll find the Terminal app in
/Applications/Utilities
cd
to the directory where you extracted the pymel zip file. A file called “setup.py” should exist directly below this directory. For example, I downloaded and extracted mine to my “Downloads” folder:cd ~/Downloads/pymel-1.0.0Note
On OSX and Linux you have to escape spaces in folder names ( or you can press the Tab key to auto-complete paths ). Here is an example of escaping a space in a folder name (notice the backslash after
Image
):cd /Applications/Image\ Capture.appNext, from the shell, run the installation for each version of Maya that you have installed:
sudo /Applications/Autodesk/maya2008/Maya.app/Contents/bin/mayapy setup.py install sudo /Applications/Autodesk/maya2009/Maya.app/Contents/bin/mayapy setup.py install sudo /Applications/Autodesk/maya2010/Maya.app/Contents/bin/mayapy setup.py installYou should be able to drag and drop the mayapy executable from the finder into the shell to get the path.
Installation on Windows¶
Open a shell: from the Start menu go to “Run...”, then type in
cmd
and press “OK”Note
On Windows Vista and above you will need to open
cmd
as administrator. To do this right-click theCommand Prompt
and selectRun as administrator
.
cd
to the directory where you extracted the pymel zip file. A file called “setup.py” should exist directly below this directory. For example, I downloaded and extracted mine to my desktop:cd "C:\Desktop\pymel-1.0.0"Next, from the shell, run the installation for each version of Maya that you have installed:
"C:\Program Files\Autodesk\Maya2008\bin\mayapy.exe" setup.py install "C:\Program Files\Autodesk\Maya2009\bin\mayapy.exe" setup.py install "C:\Program Files\Autodesk\Maya2010\bin\mayapy.exe" setup.py installYou should be able to drag and drop the mayapy.exe executable from windows explorer into the shell to get the path. Don’t forget to wrap it in quotes.
Note
Be sure to use the proper path to your mayapy.exe. For example, if you have 32-bit maya installed on 64-bit windows, it will be installed to
C:\Program Files (x86)
instead ofC:\Program Files
Manual Install¶
If the easy install did not turn out to be so easy, PyMEL can always be manually installed like any other python module or package. The process is simple in concept:
- put the extracted
pymel
folder somewhere- tell python where to find it
To find available modules, python searches directories set in an environment variable called PYTHONPATH
. This environment variable can be set for each Maya installation using the Maya.env file, or it can be set at the system level, which will set it for all instances of python, including those bundled with each Maya installation (aka “mayapy”).
Maya.env | System-Level | |
---|---|---|
allows per-maya configuration of environment variables | YES | NO |
allows easy execution of maya and mayapy in a shell |
NO | YES |
Note
If you set your PYTHONPATH
at the system level it will override any values for PYTHONPATH
set in Maya.env, except on OSX when launching Maya from it’s application bundle (an application bundle is the icon you click on to launch Maya).
Manual Method 1: Setting Up Your Environment Using Maya.env¶
The instructions below on setting up your python environment are essential to learning how to properly deploy any python module, not just PyMEL, and mastering them is also key to using the Standalone Maya Python Interpreter.
Warning
installation instructions have changed since version 0.9, so pay attention. PyMEL now includes a partial override of the maya package. This means that both the pymel
and maya
sub-directories must be on the python path, and they must come before the standard maya package in the search path. To keep things simple, we are now recommending that the top-level pymel-1.0.x
directory be added to the PYTHONPATH
instead of copying the pymel
sub-directory.
extract the pymel zip file that you downloaded. The directory structure should look something like this:
pymel-1.0.0 |-- docs |-- examples |-- extras |-- maya* | `-- app | `-- startup |-- pymel* | |-- api | |-- cache | |-- core | |-- internal | |-- tools | | |-- bin | | |-- mel2py | | `-- scriptEditor | `-- util | `-- external | `-- ply `-- testsThe folders marked with an asterisk are the required pymel packages, which must be on the PYTHONPATH. If you wish to relocate PyMEL, be sure to move both the pymel and maya folders.
- Locate the Maya.env for the desired version of Maya and open it in your favorite text editor. Maya.env can be found in your
MAYA_APP_DIR
under a sub-directory for each version of Maya.
OS MAYA_APP_DIR Linux ~/maya OSX ~/Library/Preferences/Autodesk/maya Windows drive:\My Documents\maya
- Once open, add a line to set
PYTHONPATH
to the top-level directory where you extracted pymel (the directory that contains both pymel and maya folders). ThePYTHONPATH
variable is a list of paths separated by semi-colons (on windows) or colons (on osx and linux). For example:On Windows:
PYTHONPATH = C:\path\to\pymel-1.0.0;C:\path\to\something_elseOn OSX and Linux:
PYTHONPATH = /path/to/pymel-1.0.0:/path/to/something_else
Manual Method 2: Setting Up Your System Environment¶
OSX and Linux¶
Setting up your python paths at the system level on OSX and Linux is a little bit involved. I will focus on OSX here, because Linux users tend to be more technical.
When you open a terminal on OSX ( /Applications/Utilites/Terminal.app
), your shell may be using one of several different scripting languages. (You can easily tell which is being used by looking at the label on the top bar of the terminal window, or the name of the tab, if you have more than one open. ) It will most likely say “bash”, which is the default, so that is what I will explain here.
To set up python at the system level using bash, first create a new file called .profile
in your home directory ( usually something like /Users/yourname
and denoted in a shell with the shortcut ~/
). Inside this file paste the following, being sure to set the desired Maya version:
export MAYA_LOCATION=/Applications/Autodesk/maya2009/Maya.app/Contents
export PATH=$MAYA_LOCATION/bin:$PATH
export PYTHONPATH=/path/to/pymel-1.0.0
Here’s a line-by-line breakdown of what you just did:
set
MAYA_LOCATION
, a special Maya environment variable that helps Maya determine which version to use when working via the command line ( be sure to point it to the correct Maya version).the
PATH
environment variable is a list of paths that will be searched for executables. Each path is separated by a colon:
.By adding$MAYA_LOCATION/bin
you can access all the executables in the Maya bin directory from a shell without using the full path. For example, you can launch Maya by typingmaya
, or open a Maya python interpreter by typingmayapy
.If you manually installed pymel and ipymel, include the path to the directory where the ipymel script resides. For example, if the path to the ipymel script is
/path/to/pymel-1.0.0/pymel/tools/bin/ipymel
, the line might look like the following:export PATH=$MAYA_LOCATION/bin:/path/to/pymel-1.0.0/pymel/tools/bin:$PATHfinally, set the
PYTHONPATH
to ensure that python will see thepymel
andmaya
packages. Like thePATH
environment variable,PYTHONPATH
is a list of paths separated by colons:
.
Windows XP¶
Open the Start Menu, right-click on “My Computer” and then click on “Properties”. This will open the “System Properties” window.
Changed to the “Advanced” tab, then click on the “Environment Variables” button at the bottom.
In the new window that pops up, search through your “User Varaibles” on top and your “System Variables” on the bottom, looking to see if the
PYTHONPATH
variable is set anywhere.If it is not set, make a new variable for either your user or the system (if you have permission). Use
PYTHONPATH
for the name and for the the value use the directory above thepymel
directory. So, for example, if the pymel directory isC:\My Documents\pymel-1.0.0\pymel
copy and paste in the valueC:\My Documents\pymel-1.0.0
from an explorer window.If
PYTHONPATH
is already set, select it and click “Edit”. This value is a list of paths separated by semi-colons. Scroll to the end of the value and add a semi-colon;
and after this add the directory above the pymel directory to the end of the existing path. For example, let’s say the starting value is:C:\Python25\libIf the top-level pymel directory is
C:\My Documents\pymel-1.0.0\pymel
, the edited value would be:C:\Python25\lib;C:\My Documents\pymel-1.0.0Add and set your
MAYA_LOCATION
. For example, for 2008 it would be:C:\Program Files\Autodesk\Maya2008Next, find and edit your
PATH
variable. Append the following to the end of the existing value:%MAYA_LOCATION%\binDon’t forget to put a semi-colon
;
between the existing paths and the new ones that you are adding.If installing ipymel include the path to your ipymel bin directory. For example, if you manually installed PyMEL, the line should look like the following:
%MAYA_LOCATION%\bin;C:\My Documents\pymel-1.0.0\pymel\tools\bin
Manual Method 3: sitecustomize¶
If you have don’t write permission to your Maya installation directory and you can’t change your PYTHONPATH
then you’ve come to the right place. This method relies on a special module in python called sitecustomize
to dynamically insert PyMEL into the path when python starts.
An advantage of this approach is that it allows for an arbitrary block of code to execute, which means you can use whatever logic you like to determine in what cases to add PyMEL, what version to use, etc.
A potential disadvantage of this approach is that it adds PyMEL to the python path system-wide, instead of just inside Maya. However, there are a number of utilities in pymel.util
that are useful outside of Maya as well, so this could be an advantage as well.
Here’s how to setup PyMEL using sitecustomize:
open your favorite text editor
paste in the text below:
import sys sys.path.insert(0,'/path/to/top-pymel-dir')Replace the
/path/to/top-pymel-dir
line with the path to the folder where you extracted PyMEL. The folder you want should contain both ‘pymel’ and ‘maya’ folders directly below itsave this file as
sitecustomize.py
somewhere in your system python path. If you are unsure what your python path is, you can run this from the python tab in the script editor to find out.import sys for i in sys.path: print i
Note
If your studio is already using sitecustomize.py
and you can’t edit it, you can use the same instructions with the filename usercustomize.py
instead. usercustomize is loaded immediately after sitecustomize and is intended for this situation.
ipymel¶
ipymel is an extension of the ultra-customizable IPython interpreter, which enables it to easily work with mayapy and PyMEL. It adds tab completion of maya depend nodes, dag nodes, and attributes, as well as automatic import of PyMEL at startup. Many more features to come.
ipymel Easy Install¶
As of version 0.9.2 ipymel is automatically installed when “easy” installing PyMEL, but you may have to do a few extra steps to get it working properly on Windows.
- Windows Only:
- Install python on your system. Install only the exact versions of python that come with Maya ( see `Versions of Maya and Python`_ )
- Install pyreadline for windows from the IPython website. By default it will install to your system copy of Python.
- Copy the pyreadline directory, and all the pyreadline.* files from your system site-packages directory
( ex.
C:\Python25\Lib\site-packages
) to your Maya site-packages directory ( ex.C:\Program Files\Autodesk\Maya2008\Python\lib\site-packages
).
To Run: In a new shell, run the following command:
ipymel
Note
The “easy” installation method produces an invalid ipymel.exe
on 64-bit windows systems. As of this writing I’m still looking into this.
Note
Though not a requirement for ipymel to work, it’s best to read up on Manual Method 2: Setting Up Your System Environment
ipymel Manual Install¶
OSX and Linux¶
Follow the installation instructions above for Manual Method 2: Setting Up Your System Environment
Install IPython. For a manual install, I recommend downloading the tarball, not the egg file. Unzip the tar.gz and put the sub-directory named IPython somewhere on your
PYTHONPATH
, or just put it directly into your python site-packages directoryOpen a terminal and run:
chmod 777 `which ipymel`then run:
ipymel
Windows¶
Follow the installation instructions above for Manual Method 2: Setting Up Your System Environment
Install python for windows, if you have not already.
Install IPython using their windows installer. The installer will most likely not find the maya python install, so install IPython to your system Python instead (from step 1).
Install pyreadline for windows, also from the IPython website
Copy the IPython directory, pyreadline directory, and all the pyreadline.* files from your system site-packages directory ( ex.
C:\Python25\Lib\site-packages
) to your Maya site-packages directory ( ex.C:\Program Files\Autodesk\Maya2008\Python\lib\site-packages
).open a command prompt ( go to Start menu, then click ‘Run...’, then enter
cmd
). Once it is open execute the following line to start ipymel:ipymel.bat
Troubleshooting¶
Linux¶
If you encounter an error installing on linux, you may have to fix a few symlinks. Here’s how you check. cd
to the directory where you unzipped pymel (you should be in the same directory where setup.py
is). start up maya’s standalone interpreter by typing mayapy
(or provide the full path to mayapy script if you do not have Maya’s bin directory on your PATH
) at the prompt. now import setup.py as a module and run one of it’s tests:
import setup
setup.test_dynload_modules()
This will print out any compiled modules that do not work on your platform. This occurs because the flavor and/or distribution of Linux that you are running has different versions of certain system libraries than the one that Maya was compiled on. The easiest way to fix the problem is to create symbolic links from your existing libraries to those that Maya expects to find.
For example, in my case hashlib won’t import because it can’t find libssl.so.4
. So, since I’m on a 64-bit version of linux, I check my /lib64/
( on a 32 bit OS, check /lib/
)
cd /lib64
ls -la libssl*
I see the following returned:
-rwxr-xr-x 1 root root 302552 Nov 30 2006 libssl.so.0.9.8b
lrwxrwxrwx 1 root root 16 Jul 16 2007 libssl.so.6 -> libssl.so.0.9.8b
In my case, Maya expects libssl.so.4
, but instead I have libssl.so.0.9.8b
and a symbolic link libssl.so.6
pointing to libssl.so.0.9.8b
. So, I have to create a symbolic link from the real library to the missing library:
sudo ln -s libssl.so.0.9.8b libssl.so.4
I’ve found that the same thing must sometimes be done for libcrypto
as well.
userSetup files¶
Next, to avoid having to import pymel every time you startup, you can create a userSetup.py file and add the line:
from pymel.core import *
Script Editor¶
PyMEL includes a replacement for the script editor window that provides the option to translate all mel history into python. Currently this feature is beta and works only in versions beginning with Maya 8.5 SP1.
Warning
this feature is still considered experimental
The script editor is comprised of two files located in the pymel/tools/scriptEditor directory: scriptEditorPanel.mel and pymelScrollFieldReporter.py.
- Place the mel file into your scripts directory, and the python file into your Maya plugins directory.
- Open Maya, go-to Window –> Settings/Preferences –> Plug-in Manager and load pymelScrollFieldReporter. Be sure to also check “Auto Load” for this plugin.
- Next, open the Script Editor and go to History –> History Output –> Convert Mel to Python. Now all output will be reported in python, regardless of whether the input is mel or python.
Why PyMEL?¶
The Nature of the Beast¶
Rather than reinvent the wheel, Autodesk opted to provide “wrappers” around their pre-existing toolsets: the C++ API and MEL. By wrapping them they provided an alternate python interface for each, but the core code that comprises the API and MEL remained largely the same. The wrappers serve as a (hopefully) thin layer to communicate between python and the Maya code being wrapped. The nature of the wraps is slightly different for each.
maya.OpenMaya
- In the case of the C++ API, Autodesk went with an open source wrapper called swig which generates python functions and classes from C++ counterparts. The python layer rests on top of C++, which remains the native execution language. During execution, swig marshals data back and forth between python and C++.
MEL itself is split into two components: commands, and everything else.
maya.cmds
- In the case of MEL commands, which are (effectively) written using the C++ API, Autodesk wrote the wrapping mechanism themselves such that MEL commands could also be registered and used in python as functions. The same command executed in MEL and python ultimately end up triggering the same underlying C++ API calls.
maya.mel.eval
- In order to allow execution of arbitrary MEL code such as procedures, Autodesk provided high level access to the MEL interpreter. This is as simple of a wrapper as you can get: evaluate a string representing a chunk of MEL code in the MEL interpreter and convert the result to a python object (string, float, int, and lists thereof).
So now that python is available in Maya all of our problems are solved, right? Well, not quite. Since these new modules are just wraps of the same underlying API and MEL code that we’ve had all along and neither were intended to eventually become “pythonified”, the syntax that results from this layering of python over MEL and C++ tends to be awkward, especially to those familiar with python’s idioms.
This syntactical awkwardness, particularly in maya.cmds
, was one of the initial inspirations behind PyMEL. Think of it this way: would you rather read a book that was translated from japanese into english by a software program like babelfish or by a human who is fluent in both languages? That is a key difference between an automatic wrap like maya.cmds
and a “restructured wrap” like PyMEL, which uses the maya python modules as building blocks to construct an intuitive, insightful, and pythonic API.
The Paradigm Shift¶
MEL is a procedural language, meaning it provides the ability to encapsulate code into reusable “procedures” ( aka “functions” ). (This is probably old news to you, but bear with me, there’s a mildly entertaining analogy coming up ). The term “procedural programming” is used primarily in the context of distinguishing a language from the newer, object-oriented paradigm.
Object-oriented programming adds organization by creating logical groupings of functions which are accessed from a common “object”. If you have used MEL extensively you know you can get pretty far with procedural programming alone, but there is often an exponential relationship between the complexity of a task and the amount of code it will take to complete in MEL. In other words, progress breaks down once the task at hand becomes too complicated. Object-oriented code tends to be easier to read and write, provides the ability to easily create new data types and reuse code, and ultimately scales better to complex tasks.
To prove my point, here’s a quick example that we’ll cover in more depth later.
procedural:
cmds.getAttr( cmds.listRelatives( cmds.ls(type='camera')[0], p=1 )[0] + '.translate' )[0][2]
object oriented:
pymel.ls(type='camera')[0].getParent().translate.get().z
See how nicely that reads from left to right? Object oriented programming isn’t necessarily about brevity, it’s about legibility derived from structure.
A quick perusal of the more than three thousand MEL commands in the Maya documentation will give you an idea why object-oriented groupings are a good idea. MEL is like a tool chest, a wardrobe, and a kitchen set all dumped into a bathtub – everything in there is useful in the proper context, but you’ve really got to know what you’re looking for to get anything done. The shame of it is that Maya’s great innovation is that it is itself inherently object-oriented – every scene represents networks of nodes, each with its own unique abilities and those that it inherits from its super-classes. Maya’s API is object-oriented, too. So how did MEL end up so crippled?
The original idea behind MEL and the API was to create a tiered relationship where complex tasks, or tasks where execution time is of great concern, were to be performed using the C++ API and bundled into easy-to-use MEL commands. These commands were in turn to become the building blocks of more generalized tasks performed in MEL. The API would be the toolset of the superuser – with access to complex types and external libraries – whereas the scope of MEL would be much more limited and thus more accessible to the average user.
The advent of python in Maya brought a common language to MEL and the API, and as a result, the clarity of this tiered structure has become muddied. API classes, MEL commands, and MEL procedures can be used within the same python script, completely outside the context of a plugin. Moreover, python manages to borrow the strengths of both languages: like C++, it is object-oriented and extensible through third party libraries and custom types, but like MEL, python is easy to learn, protects the user from certain daunting programming concepts, and compiles at runtime. Having managed to provide an object-oriented design without a steep learning curve, python makes the simplicity of MEL seem like an unfortunate over-simplification.
So, on the one hand we have the C++ API, now easily accessible from within Python, object-oriented, but too cryptic and verbose for everyday tasks, and on the other hand, we have a host of thousands of MEL-commands-turned-python-functions, too valuable to do without, but woefully unorganized and “unpythonic”. Which do we use? How can they work together? PyMEL bridges this gap by forging a new object-oriented API that’s as powerful and easy to use as python is.
Glossary¶
- MEL
- short for Maya Embedded Language, is a scripting language written for Maya, and with which much of Maya’s functionality and user interface is built. It was designed to be concise, simple, and Maya-specific.
- C++ API
- provides deeper access to Maya’s internals. With the API you can create new node types and new MEL commands. Prior to the introduction of python in Maya, the API could only be used with C++.
Getting Started¶
This tutorial assumes that you have some familiarity with python, but even if you only have MEL experience, you’ll probably be able to follow along. If you are a MEL scripter but have not used python in maya yet, you should start with the Maya docs on the subject, particularly the section Using Python. This will help you to understand the differences in syntax between the two languages and how to translate between them.
Formatting: Read Me First to Avoid Confusion¶
The code in these tutorials is formatted as you would see it in Maya’s Script Editor; however, the majority of the documentation outside of these tutorials is formatted as you would see it in a python interpreter, which is standard for python documentation.
In an external interpreter:
>>> ls(type='camera')[0]
nt.Camera(u'frontShape')
Maya’s script editor:
ls(type='camera')[0]
# Result: frontShape
You might notice that the differences go beyond formatting. Although it is not the case, it appears as though the same commands return two different results: nt.Camera(u'frontShape')
and frontShape
. Prior to version 2011, Autodesk chose to deviate from the standard python practice of using an object’s reproduction strings – as returned by repr()
– to display results, instead they use the object’s string representation – as returned by str()
. This inevitably leads to confusion.
Here’s how you can get more informative results in the Script Editor:
repr(ls(type='camera')[0])
# Result: nt.Camera(u'frontShape')
here’s a shorthand for the same thing:
`ls(type='camera')[0]`
# Result: nt.Camera(u'frontShape')
Example 1: The Basics¶
Importing PyMEL¶
To get started we need to import the pymel module:
from pymel.core import *
This brings everything in pymel into the main namespace, meaning that you won’t have to prefix the maya commands with the module name.
Warning
One problem with maya.cmds
is that importing it into the root namespace (e.g. from maya.cmds import *
) is dangerous because it will override several of python’s more important built-in methods. PyMEL is designed to be safe to import into the root namespace so that scripts can be written much more concisely. However, if you are a python novice, you might want to keep pymel in its own namespace, because, unlike in MEL, in python you can “overwrite” functions if you are not careful:
from pymel.core import *
s = sphere() # create a nurbsSphere
sphere = 'mySphere' # oops, we've overwritten the sphere command with a string
sphere()
# Error: name 'sphere' is not defined
# Traceback (most recent call last):
# File "<maya console>", line 1, in <module>
# NameError: name 'sphere' is not defined #
Note
All the functions in maya.cmds are in the pymel namespace, except the conflicting ones ( file
, filter
, eval
, help
, and quit
). The conflicting commands can be found in the pymel.cmds
namespace, along with all of the unaltered maya commands.
An Intro to PyMEL Objects in Maya¶
Before we proceed let’s make sure we have a clean scene so that you’ll get the same results as me:
f=newFile(f=1) #start clean
Let’s start by listing the cameras in the scene. We do this in the same way that we would with maya.cmds
:
ls(type='camera')
# Result: [nt.Camera(u'frontShape'), nt.Camera(u'perspShape'), nt.Camera(u'sideShape'), nt.Camera(u'topShape')]
Just for comparison, let’s do the same thing using maya.cmds
:
import maya.cmds as cmds
cmds.ls(type='camera')
# Result: [u'frontShape', u'perspShape', u'sideShape', u'topShape']
Notice the difference in the returned results. In the second example using maya.cmds
, the ls
function returns a list of strings. For those of us coming from a MEL background, a list of names as strings is what we would expect out of ls
. PyMEL returns something much better – instances of PyNode
classes – which are like strings on steroids. In addition to methods for operating on node names as strings, these classes have methods for operating on the type of node or UI element that the string represents.
Note
object oriented programming introduces the concept of classes or “types”, and each occurrence of a type is called an instance. For example, a string is a type of object that represents a series of characters. An instance of a string could be the word ‘hello’. You’ll probably find that the concepts behind object oriented programming are fairly familiar, because Maya itself is designed in a very object-oriented way. Maya has many types of nodes, each with its own attributes, properties, and capabilities. Each unique occurrence of one of these node types in your scene is like an instance of a class in python.
The great thing about python is, unlike MEL, we’re not stuck with the default data types. We can make new types! That’s a big part of what PyMEL adds: new Maya-specific data types to represent nodes, attributes, UI elements, vectors, matrices, etc.
Let’s use one of these camera objects to get some information. To do this using maya.cmds
we might write something like this:
cam = ls(type='camera')[0]
camera( cam, query=True, aspectRatio=True)
# Result: 1.5
camera( cam, query=True, focalLength=True)
# Result: 35.0
camera( cam, edit=True, focalLength=20)
Rewriting this in an object-oriented way is fairly straight-forward:
cam = ls(type='camera')[0]
cam.getAspectRatio()
# Result: 1.5
cam.getFocalLength()
# Result: 35.0
cam.setFocalLength(20)
- The two examples start out the same way: with the
ls
function. This is how we get our raw material to work with, in this case our node objects. In general, we can get nodes in 3 ways: - list them based on some criteria
- create them
- get them by their name if we know it
- Once we have our list of nodes, we can begin to work in an object-oriented fashion. There is not a hard-fast rule for converting from a procedural style to an object-oriented one, but here are some general guidelines:
- the argument to the command – in this case
cam
– becomes the operating object on the left - instead of flags –
focalLength
andaspectRatio
we use methods, which are attached to right of the object with a period.
- query methods are typically prefixed with ‘get’ and edit methods are prefixed with ‘set’. If a query does not have a corresponding edit, it may not have a ‘get’ prefix.
- the argument to the command – in this case
So, in our case query=True, aspectRatio=True
becomes .getAspectRatio
. And query=True, focalLength=True
becomes .getFocalLength
.
Getting Help¶
If you are ever unsure of what method to use, just use the builtin python help
command on the node class (the capitalized node type). First find out what type of node you’re working with. we’ll continue from the example above, with our cam
variable:
print repr(cam)
This prints nt.Camera(u'frontShape')
. The nt.Camera
part tells you what type of object it is. Now we can get help on that object type:
help(nt.Camera)
This prints all the documentation on the Camera
node type. If you want help on a particular method, you can do this:
help(nt.Camera.getFocalLength)
You can do the same thing for any function as well:
help(ls)
Example 2: Transitioning from Procedural Programming¶
Let’s look at another simple procedural example in which we find a camera, get it’s transform node, then get the z component of its translation:
cam = ls(type='camera')[0]
parent = listRelatives(cam, p=1)[0]
trans = xform(parent,q=1,t=1)
`trans`
# Result: [28.0, 21.0, 28.0]
trans[2]
# Result: 28.0
Now let’s convert this to an object-oriented style.
Instead of listRelatives
, we can use use methods available on cam
, which is a nodetypes.Camera
class ( which can also be referred to by the shorthand nt.Camera
). nodetypes.Camera
inherits from nodetypes.DagNode
, so it has methods such as getParent
, and getChildren
. In this case, we’ll use getParent
:
cam = ls(type='camera')[0]
parent = cam.getParent() # <---
trans = xform(parent,q=1,t=1)
`trans`
# Result: [28.0, 21.0, 28.0]
trans[2]
# Result: 28.0
Next, instead of xform
, we can use the getTranslation
method of the nodetypes.Transform
node:
cam = ls(type='camera')[0]
parent = cam.getParent()
trans = parent.getTranslation() # <---
`trans`
# Result: dt.Vector([28.0, 21.0, 28.0])
trans.z
# Result: 28.0
Now, let’s chain these commands together to compare procedural versus object-oriented.
procedural:
xform( listRelatives( ls(type='camera')[0], p=1)[0],q=1,t=1)[2]
# Result: 28.0
object-oriented:
ls(type='camera')[0].getParent().getTranslation().z
# Result: 28.0
In procedural programming, you take the result of one function and feed it into the arguments of another function, but in object oriented programming, functions are associated with – you might even say “attached to” – the returned objects themselves, so the chaining of functions is much easier to read. The object-oriented approach is shorter even though the procedural approach uses short flag names that obscure their purpose.
One thing that should be clear by now is that you can continue to code in a procedural way using PyMEL, because the pymel.core
module provides the same set of MEL-derived commands as maya.cmds
, but PyMEL wraps all of these commands to return powerful PyMEL node classes, so you can begin mixing in object-oriented code as you become more comfortable with it.
Example 3: Some More OO Basics¶
Now let’s create a node:
objs = polyPlane()
objs
# Result: [nt.Transform(u'pPlane1'), nt.PolyPlane(u'polyPlane1')]
You can see that, like the ls
command, the polyPlane
command also returns PyNode
classes. As in MEL, it returns a list: the first object is the tranform of the plane, and the second is the construction history. Now let’s get the shape of the transform:
# assign the transform from above to a variable
plane = objs[0]
shape = plane.getShape()
`shape`
# Result: nt.Mesh(u'pPlaneShape1')
So, we can clearly see that the shape is a Mesh. Let’s explore the nodetypes.Mesh
object a little. We can get the name as a string, formatted in different ways (the u
in front of the string denotes that it is a unicode string, meaning it can represent international characters).:
shape.longName()
# Result: |pPlane1|pPlaneShape1
shape.name()
# Result: pPlaneShape1
We can also get information specific to this mesh:
shape.numEdges()
# Result: 220
shape.numVertices()
121
`shape.vtx[0]`
# Result: MeshVertex(u'pPlaneShape1.vtx[0]')
On the last line you see that vertices have their own class as well, MeshVertex
. More on that later.
Attributes¶
I think it’s time we learned how to set some attributes. Let’s go back and take a look at our plane’s transform and access an Attribute
object. Just like nodes, attributes have their own class with methods that encompass the dozens of MEL commands for operating on them.:
print `plane.translateX`
# Result: Attribute(u'pPlane1.translateX')
To get and set attributes:
plane.translateX.get()
# Result: 0.0
plane.translateX.set(10.0)
Here’s a few examples of how to query and edit properties of attributes:
plane.translateX.isLocked()
# Result: False
plane.translateX.setLocked(True)
plane.translateX.isKeyable()
# Result: True
plane.translateX.setKeyable(False)
Connections¶
Now let’s look into getting other objects connected to our plane shape. The Attribute.connections
method accepts the
same flags as the procedural command listConnections
.
Below we get incoming and outgoing connections:
shape.connections()
# Result: [ShadingEngine(u'initialShadingGroup'), PolyPlane(u'polyPlane1')]
inputs
is a shorcut to connections(source=True)
:
shape.inputs()
# Result: [PolyPlane(u'polyPlane1')]
outputs
is a shorcut to connections(source=False)
:
shape.outputs()
# Result: [ShadingEngine(u'initialShadingGroup')]
Notice that when we enable the plugs
flag that the result becomes an Attribute
instead of a node type.:
shape.inputs(plugs=1)
# Result: [Attribute(u'polyPlane1.output')]
Here’s another handy feature of python: it supports 2D arrays, meaning you can put lists inside lists. PyMEL takes advantage of that in many situations, including when we enable the connections
flag, which causes listConnections
to list source-destination pairs.:
shape.connections(c=1, p=1)
# Result: [(Attribute(u'pPlaneShape1.instObjGroups[0]'), Attribute(u'initialShadingGroup.dagSetMembers[0]')), (Attribute(u'pPlaneShape1.inMesh'), Attribute(u'polyPlane1.output'))]
This is particularly useful for looping
for source, destination in shape.connections(c=1, p=1):
... print source, destination
pPlaneShape1.instObjGroups[0] initialShadingGroup.dagSetMembers[0]
pPlaneShape1.inMesh polyPlane1.output
Using Existing Objects by Name¶
In many cases, you won’t be creating objects directly in your code, but will want to gain access to an existing object by name. PyMEL provides two ways of doing this. Both of them will automatically choose the correct PyMEL class for your object.
The PyNode
class:
PyNode( 'defaultRenderGlobals').startFrame.get()
# Result: 1.0
The SCENE object ( an instance of the Scene
class )
SCENE.defaultRenderGlobals.startFrame.get()
# Result: 1.0
Mel Scripts¶
Calling MEL scripts through maya.mel.eval
is a nuisance because it requires so much string formatting on
the programmer’s part. pymel.mel
handles all of that for you so you can use your MEL scripts as if they
were python functions. This includes automatically formatting all iterable types into maya arrays.
See out pymel.core.language.Mel
for more information.
Transitioning Tips¶
What to Change¶
All of the MEL functions in maya.cmds
exist in pymel
, with a few exceptions ( see Module Documentation ). MEL functions that operate on nodes and/or attributes almost always fall into one or more of these categories: creating, listing, querying/editing. As you begin shifting toward a more object-oriented approach, you will still retain the need for procedural programming.
Use these guidelines for what aspects of PyMEL are best suited to object-oriented programming:
- creating nodes and UI elements : remains mostly procedural
- listing objects and UI elements: object-oriented, except for general listing commands like
ls
- querying and editing objects and UI elements: object-oriented, except for commands that operate on many nodes at once, like
select
anddelete
Partial Conversion is Bad, mmkay¶
Mixing maya.cmds
module with pymel.core
will result in problems, because maya.cmds
doesn’t know what to do with anything other than very basic data types. Passing a PyNode
, Matrix
, Vector
, or other custom type to a maya.cmds
function will result in errors. Your best bet is to completely replace maya.cmds
with pymel
throughout your code in one go. This might sound frightening, but it will ultimately lead to fewer errors than partial PyMEL integration. The purely procedural part of pymel.core
will behave exactly the same as maya.cmds
as long as it is replaced wholesale, but if it is not, results returned from PyMEL functions will cause errors when passed to maya.cmds
functions, and results from maya.cmds
will need to be cast to PyNodes.
There are only a handful of maya.cmds
wraps in PyMEL that are not backward compatible. They all fall into the category of functions which now return a list of tuple pairs instead of a flat list:
listConnections( connections=True )
keyframe( q=True, valueChange=True )
keyframe( q=True, timeChange=True )
ls( showType=True )
These changes are all noted in the docstrings of each function.
Glossary¶
- methods
- a method is a function bound to an object. ex. ‘bar’ is the method in
foo.bar()
PyNodes¶
The pymel module reorganizes many of the most commonly used mel commands and API methods into a hierarchy of classes. This design allows you to write much more concise and readable python code. It also helps keep all of the commands organized, so that functions are paired only with the types of objects that can use them.
The PyNode
class is the base object for all node-, component-, and attribute-related classes. We collectively refer
to all these classes as “PyNodes”.
In order to use the object-oriented design of pymel, you must ensure that the objects that you are working
with are instances of PyMEL classes. To make this easier, PyMEL contains wrapped version
of the more common commands for creating and getting lists of objects. These modified commands cast their results to the appropriate
PyNode
class type. See ls
, listRelatives
, listTransforms
, selected
, and listHistory
, for a few examples.
- Commands that list objects return PyMEL classes:
>>> s = ls(type='transform')[0] >>> print type(s) <class 'pymel.core.nodetypes.Transform'>
- Commands that create objects are wrapped as well:
>>> t = polySphere()[0] >>> print t, type(t) pSphere1 <class 'pymel.core.nodetypes.Transform'>
API Underpinnings¶
In MEL, the best representation we have have of a maya node or attribute is its name. But with the API we can do better! When creating an instance of a PyNode
class, PyMEL determines the underlying API object behind the scenes. With this in hand, it can operate on the object itself, not just the string representing the object.
So, what does this mean to you? Well, let’s take a common example: testing if two nodes or attributes are the same. In MEL, to accomplish this the typical solution is to perform a string comparison of two object names, but there are many ways that this seemingly simple operation can go wrong. For instance, forgetting to compare the full paths of dag node objects, or comparing the long name of an attribute to the short name of an attribute. And what if you want to test if the nodes are instances of each other? You’ll have some pretty nasty string processing ahead of you. And if someone renames the node or its name becomes non-unique after you’ve already gotten its name as a string then your script will fail. With PyMEL, the nightmares of string comparisons are over.
Since PyMEL uses the underlying API objects, these operations are simple and API-fast.
In this example, we’ll make a sphere, group it, then instance the group, so that we have a tricky situation with instances and non-unique names.
>>> from pymel.core import *
>>> # Make two instanced spheres in different groups
>>> sphere1, hist = polySphere(name='mySphere')
>>> grp = group(sphere1)
>>> grp2 = instance(grp)[0]
>>> sphere2 = grp2.getChildren()[0]
Now lets take a look at our objects and see how our various comparisons turn out.
>>> # check out our objects
>>> sphere1 # the original
Transform(u'group1|mySphere')
>>> sphere2 # the instance
Transform(u'group2|mySphere')
>>> # do some tests
>>> # they aren't the same dag objects
>>> sphere1 == sphere2
False
>>> # but they are instances of each other
>>> sphere1.isInstanceOf( sphere2 )
True
Attribute comparison is simple, too. Keep in mind, we are not comparing the values of the attributes – for that we would need to use the get
method – we are comparing the attributes themselves. This is more flexible and reliable than comparing names:
>>> # long and short names retrieve the same attribute
>>> sphere1.t == sphere1.translate
True
>>> sphere1.tx == sphere1.translate.translateX
True
>>> # the same attrs on different nodes/instances are still the same
>>> sphere1.t == sphere2.t
True
And here’s an incredibly useful feature that I get asked for all the time. Get all the instances of an object in a scene:
>>> sphere1.getInstances()
[Transform(u'group1|mySphere'), Transform(u'group2|mySphere')]
>>> sphere1.getOtherInstances()
[Transform(u'group2|mySphere')]
For more on the relationship between PyMEL and Maya’s API, see API Classes and their PyNode Counterparts.
PyNodes Are Not Strings¶
The PyNode
base class inherits from ProxyUnicode
class, which has the functionality of a string object, but removes the immutability restriction. It is important to keep in mind that although PyNodes behave like strings in most situations, they are not actual strings. Functions which explicitly require a string, might raise an error. For example:
>>> objs = ls( type='camera')
>>> print ', '.join( objs )
Traceback (most recent call last):
...
TypeError: sequence item 0: expected string, Camera found
The solution is simple: convert the PyNodes to strings. The following example uses a shorthand python expression called “list comprehension” to convert the list of PyNodes to a list of strings:
>>> objs = ls(type='camera')
>>> ', '.join([ str(x) for x in objs ])
'frontShape, perspShape, sideShape, topShape'
Similarly, if you are trying to concatenate your PyNode with another string, you will need to cast it to a string (same as you would have to do with an int):
>>> print "Camera 1 of " + str(len(objs)) + " is named " + str(objs[0])
Camera 1 of 4 is named frontShape
Alternately, you can use string formatting syntax:
>>> print "Camera 1 of %s is named %s" % ( len(objs), objs[0] )
Camera 1 of 4 is named frontShape
The %s
means to format as a string.
Note
You can get more control over how numbers are formatted by using %f
for floats and %d
for integers:
>>> "You can control precision %.02f and padding %04d" % ( 1.2345, 2 )
'You can control precision 1.23 and padding 0002'
By default, the shortest unique name of the node is used when converting to a string. If you want more control over how the name is printed, use the various methods for retrieving the name as a string:
>>> cam.shortName() # shortest unique
u'frontShape'
>>> cam.nodeName() # just the node, same as unique in this case
u'frontShape'
>>> cam.longName() # full dag path
u'|front|frontShape'
Finally, be aware that string operations with PyNodes return strings not new PyNodes:
>>> new = cam.replace('front', 'monkey')
>>> print new, type(new), type(cam)
monkeyShape <type 'unicode'> <class 'pymel.core.nodetypes.Camera'>
Node Renaming¶
Maya nodes can be renamed, which means that each time the name of the node is required – such as printing, slicing, splitting, or passing to any command derived from maya.cmds
– the object’s current name is queried from the underlying API object. This ensures renames performed via mel or the UI will always be reflected in the name returned by your PyNode class and your variables will remain valid despite these changes.:
>>> orig = polyCube(name='myCube')[0]
>>> print orig # print out the starting name
myCube
>>> orig.rename('crazyCube') # rename it (the new name is returned)
Transform(u'crazyCube')
>>> print orig # the variable 'orig' reflects the name change
crazyCube
As you can see, you do not need to assign the result of a rename to a variable, although, for backward compatibility’s sake, we’ve ensured that you still can.
Querying the name of the object is not infinitely fast, so try to avoid doing it repetitively, if possible.
See Using PyNodes as Keys in Dictionaries for more information on PyNode mutability.
Node Class Hierarchy¶
PyMEL provides a class for every node type in Maya’s type hierarchy. The name of the class is the node type capitalized. Wherever possible, PyMEL functions will return objects as instances of these classes. This allows you to use built-in python functions to inspect and compare your objects. For example:
>>> dl = directionalLight()
>>> type(dl)
<class 'pymel.core.nodetypes.DirectionalLight'>
>>> isinstance( dl, nodetypes.DirectionalLight)
True
>>> isinstance( dl, nodetypes.Light)
True
>>> isinstance( dl, nodetypes.Shape)
True
>>> isinstance( dl, nodetypes.DagNode)
True
>>> isinstance( dl, nodetypes.Mesh)
False
Many of these classes contain no methods of their own and exist only as place-holders in the hierarchy.
However, there are certain key classes which provide important methods to all their sub-classes. A few of the more important
include nt.DependNode
, nt.DagNode
, nt.Transform
, and nt.Constraint
.
Chained Function and Attribute Lookups¶
Mel provides the versatility of operating on a shape node via its transform node. For example:
camera -q -centerOfInterest persp
camera -q -centerOfInterest perspShape
PyMEL achieves this effect by chaining function lookups. If a called method does not exist on the Transform
class, the
request will be passed to appropriate class of the transform’s shape node, if it exists.
>>> # get the persp camera as a PyNode
>>> trans = PyNode('persp')
>>> # get the transform's shape, aka the camera node
>>> cam = trans.getShape()
>>> print cam
perspShape
>>> trans.getCenterOfInterest()
44.82186966202994
>>> cam.getCenterOfInterest()
44.82186966202994
Technically speaking, the Transform does not have a getCenterOfInterest
method:
>>> trans.getCenterOfInterest
<bound method Camera.getCenterOfInterest of Camera(u'perspShape')>
Notice the bound method belongs to the nt.Camera
class.
Using PyNodes as Keys in Dictionaries¶
A powerful feature was added in Maya 2009 that gives us access to a unique id per node. You can access this by
using the special method nt.DependNode.__hash__
, though typically you won’t need to use this directly. Its existence means that PyNodes can be used as a key in a dictionary in a name-independent way: if the name of the node changes, the PyNode object can still be used to retrieve data placed in the dictionary prior to the name change. It is important to note, however, that this id is only valid while the scene is open. Once it is closed and reopened, the id for each node will change.
Below is an example demonstrating how this feature allows us to create a dictionary of node-to-name mappings, which could be used to track changes to a file.
AllObjects = {} # node-to-name dictionary
def store():
for obj in ls():
AllObjects[obj] = obj.name()
def diff():
AllObjsCopy = AllObjects.copy()
for obj in ls():
try:
oldName = AllObjsCopy.pop(obj)
newName = obj.name()
if newName != oldName:
print "renamed: %s ---> %s" % (oldName, newName)
except KeyError:
print "new: %s" % obj.name()
for obj, name in AllObjsCopy.iteritems():
print "deleted:", name
create some objects and store them to start:
s = sphere()[0]
c = polyCube(ch=0)[0]
store() # save the state of the current scene
now make some changes:
s.rename('monkey')
delete(c.getShape())
polyTorus()
print out what’s changed since we ran store()
:
diff()
this prints out:
renamed: nurbsSphere1 ---> monkey
renamed: nurbsSphereShape1 ---> monkeyShape
new: polyTorus1
new: pTorus1
new: pTorusShape1
deleted: pCubeShape1
Mommy, Where Do PyNodes Come From?¶
In order to understand PyNode classes, it’s best to understand their relationship to the underlying objects that they wrap. The methods on each node class are derived from three sources:
- automatically, from maya.cmds
- automatically, from maya.OpenMaya*
- manually, written by PyMEL team
MEL Node Commands and their PyNode Counterparts¶
As you are probably aware, MEL contains a number of commands which are used to create, edit, and query object types in maya. Typically, the names of these commands correspond with the node type on which they operate. However, it should be noted that there are a handful of exceptions to this rule.
Some examples of command-class pairs. Notice that the last two nodes do not match their corresponding command:
Mel Command | Maya Node Type | PyMEL Node Class |
---|---|---|
aimConstraint | aimConstraint | AimConstraint |
camera | camera | Camera |
directionalLight | directionalLight | DirectionalLight |
spaceLocator | locator | Locator |
vortex | vortexField | VortexField |
This example demonstrates some basic principles. Note the relationship between the name of the object
created, its node type, and its class type. Also notice that instead of creating new objects using
maya.cmds functions ( ex. spotlight
), the class ( ex. nt.SpotLight
) can also be used :
>>> from pymel.core import *
>>> l = nodetypes.SpotLight()
>>> print "The name is", l
The name is spotLightShape1
>>> print "The maya type is", l.type()
The maya type is spotLight
>>> print "The python type is", type(l)
The python type is <class 'pymel.core.nodetypes.SpotLight'>
Once you have an instance of a PyMEL class, you can use it to query and edit the maya node it represents in an object-oriented way.
Make the light red and get shadow samples, the old, procedural way:
>>> spotLight(l, edit=1, rgb=[1, 0, 0])
>>> print spotLight(l, query=1, shadowSamples=1)
1
Now, the object-oriented, PyMEL way:
>>> l.setRgb([1, 0, 0])
>>> print l.getShadowSamples()
1
For those familiar with MEL, you can probably already tell that the DirectionalLight class can be understood as an object-oriented reorganization of the directionalLight command, where you ‘get’ queries and you ‘set’ edits.
Some classes have functionality that goes beyond their command counterpart. The nt.Camera
class,
for instance, also contains the abilities of the track
, orbit
, dolly
, and cameraView
commands:
>>> cam = nodetypes.Camera(name='newCam')
>>> cam.setFocalLength(100)
>>> cam.getHorizontalFieldOfView()
20.407947443463367
>>> cam.dolly(distance=-3)
>>> cam.track(left=10)
>>> cam.addBookmark('new')
API Classes and their PyNode Counterparts¶
PyNode classes derive their methods from both MEL and the API ( aka. maya.cmds and maya.OpenMaya, respectively ). If you’re familiar with Maya’s API, you know that there is a distinct separation between objects and their abilities. There are fundamental object types such as maya.OpenMaya.MObject
and maya.OpenMaya.MDagPath
that represent the object itself, and there are “function sets”, which are classes that,
once instantiated with a given fundamental object, provide it with special abilities. ( Because I am a huge nerd, I like to the think of the function sets as robotic “mechs” and the fundamental objects as “spirits” or “ghosts” that inhabit them, like in Ghost in the Shell ).
For simplicity, PyMEL does away with this distinction: a PyNode instance is the equivalent of an activated API function set; the necessary fundamental API objects are determined behind the scenes at instantiation. You can access these by using the special methods nt.DependNode.__apimobject__
, Attribute.__apimobject__
, nt.DependNode.__apihandle__
, nt.DagNode.__apimdagpath__
, Attribute.__apimdagpath__
, Attribute.__apimplug__
, and PyNode.__apimfn__
. (Be aware that this is still considered internal magic, and the names of these methods are subject to change ):
>>> p = PyNode('perspShape')
>>> p.__apimfn__()
<maya.OpenMaya.MFnCamera; proxy of <Swig Object of type 'MFnCamera *' at ...> >
>>> p.__apimdagpath__()
<maya.OpenMaya.MDagPath; proxy of <Swig Object of type 'MDagPath *' at ...> >
>>> a = p.focalLength
>>> a
Attribute(u'perspShape.focalLength')
>>> a.__apimplug__()
<maya.OpenMaya.MPlug; proxy of <Swig Object of type 'MPlug *' at ...> >
As you can probably see, these methods are enormously useful when prototyping API plugins. Also of great use is the PyNode
class, which can be instantiated using API objects.
Glossary¶
- mutable
Mutability describes a data type whos value can be changed without reassigning. An example of a mutable data type is the builtin list. >>> numbers = [1,2,3] >>> numbers.append(4) >>> numbers [1, 2, 3, 4]
- As you can see we have changed the value of
numbers
without reassigning a new valuenumbers
(in plain english, we didn’t use an equal sign).
You might have noticed when working with strings in python that they cannot be changed “in place”. All string operations that modify the string, return a brand new string as a result, leaving the original intact. This is is known as immutability:: >>> s1 = 'hampster dance' >>> s2 = s1.replace('hampster', 'chicken') >>> s1 'hampster dance' >>> s2 'chicken dance'
- The value of
s1
remained the same, but the result of thereplace
operation was stored intos2
. Because strings are immutable and the value ofs1
cannot change without assigning a brand new value tos1
:: >>> s1 = 'brand new dance!'
Attributes¶
The Attribute
class is your one-stop shop for all attribute related functions. Those of us who have spent time using MEL
have become familiar with all the myriad commands for operating on attributes. This class gathers them all into one
place. If you forget or are unsure of the right method name, just ask for help by typing help(Attribute)
.
For the most part, the names of the methods follow a fairly simple pattern: setAttr
becomes Attribute.set
, getAttr
becomes Attribute.get
, connectAttr
becomes Attribute.connect
and so on.
Here’s a simple example showing how the Attribute class is used in context.
>>> from pymel.core import *
>>> cam = general.PyNode('persp')
>>> if cam.visibility.isKeyable() and not cam.visibility.isLocked():
... cam.visibility.set( True )
... cam.visibility.lock()
...
>>> print cam.v.type() # shortnames also work
bool
Accessing Attributes¶
You can access an attribute class in three ways. The first two require that you already have a PyNode
object.
attr
Method¶
The attr method is the safest way to access an attribute, and can be used to access attributes that conflict with python methods and would therefore fail using shorthand syntax. This method is passed a string which is the name of the attribute to be accessed.
>>> cam.attr('visibility')
Attribute(u'persp.visibility')
Unlike the shorthand syntax below, this method is capable of being passed attributes as variables:
>>> for axis in ['scaleX', 'scaleY', 'scaleZ']:
... cam.attr( axis ).lock()
Shorthand¶
The shorthand method is the most visually appealing and readable – you simply access the maya attribute as a normal python attribute – but it has one major drawback: if the attribute that you wish to acess has the same name as one of the attributes or methods of the python class then it will fail.
>>> cam # continue from where we left off above
Transform(u'persp')
>>> cam.visibility # long name access
Attribute(u'persp.visibility')
>>> cam.v # short name access
Attribute(u'persp.visibility')
Keep in mind, that regardless of whether you use the long or short name of the attribute, you are accessing the same underlying API object.
If you need the attribute formatted as a string in a particular way, use Attribute.name
, Attribute.longName
, Attribute.shortName
,
Attribute.plugAttr
, or Attribute.lastPlugAttr
.
Direct Instantiation¶
The last way of getting an attribute is by directly instantiating the class with the full name of the attribute, including the node. You can pass the attribute name as a string, or if you have one handy, pass in an api MPlug object. If you have a name as a string, but you don’t know whether it represents a node or an attribute, you can always instantiate via the PyNode
class, which will determine the appropriate class automatically.
explicitly request an Attribute:
>>> Attribute('persp.visibility')
Attribute(u'persp.visibility')
let PyNode
figure it out for you:
>>> PyNode('persp.translate')
Attribute(u'persp.translate')
Setting Attributes Values¶
To set the value of an attribute, you use the Attribute.set
method.
>>> cam.translateX.set(0)
to set an attribute that expects a double3, you can use any iterable with 3 elements:
>>> cam.translate.set([4,5,6])
>>> cam.translate.set(datatypes.Vector([4,5,6]))
Getting Attribute Values¶
To get the value of an attribute, you use the Attribute.get
method. Keep in mind that, where applicable, the values returned will
be cast to pymel classes. This example shows that rotation (along with translation and scale) will be returned as a Vector
.
>>> t = cam.translate.get()
>>> print t
[4.0, 5.0, 6.0]
>>> # translation is returned as a vector class
>>> print type(t)
<class 'pymel.core.datatypes.Vector'>
Attribute.set
is flexible in the types that it will accept, but Attribute.get
will always return the same type
for a given attribute. This can be a potential source of confusion:
>>> value = [4,5,6]
>>> cam.translate.set(value)
>>> result = cam.translate.get()
>>> value == result
False
>>> # why is this? because result is a Vector and value is a list
>>> result
dt.Vector([4,5,6])
>>> # use `Vector.isEquivalent` or cast the list to a `list`
>>> list(result) == value
True
>>> result.isEquivalent(value)
True
Connecting Attributes¶
As you might expect, connecting and disconnecting attributes is pretty straightforward.
>>> cam.rotateX.connect(cam.rotateY)
>>> cam.rotateX.disconnect(cam.rotateY)
there are also handy operators for connection
and disconnection
. Note that to keep your code clear, it is recommended to use these shorthand operators only when scripting interactively.
>>> c = polyCube(name='testCube')[0]
>>> cam.tx >> c.tx # connect
>>> cam.tx.outputs()
[nt.Transform(u'testCube')]
>>> cam.tx // c.tx # disconnect
>>> cam.tx.outputs()
[]
Non-Existent Objects¶
You cannot instantiate a PyNode
for an object which does not exist. An exception will be raised if the passed name does not represent an object in the scene. This has several advantages:
- you will never unwittingly attempt to use a node or attribute that does not exist, either due to a typo or unexpected context
- it brings PyMEL’s attribute handling more in line with pythonic rules, where attributes must exist before accessing them
- it prevents the awkward situation of having a python object for which only a handful of methods will actually work
PyMEL Exceptions¶
PyMEL has three exceptions which can be used to test for existence errors when creating new PyNodes: MayaObjectError
,
MayaNodeError
, and MayaAttributeError
.
>>> for x in [ 'fooBar.spangle', 'superMonk' ] :
... try:
... PyNode( x )
... print "It Exists"
... except MayaNodeError:
... print "The Node Doesn't Exist:", x
... except MayaAttributeError:
... print "The Attribute Doesn't Exist:", x
...
The Attribute Doesn't Exist: fooBar.spangle
The Node Doesn't Exist: superMonk
Both exceptions can be caught by using the parent exception MayaObjectError
. In addition MayaAttributeError
can also be caught
with the builtin exception AttributeError
.
Note that you will get different exceptions depending on how you access the attribute. This is because the shorthand notation can also
be used to access functions, in which case the MayaAttributeError
does not make sense to raise. As mentioned above, you can always
use AttributeError
to catch both.
Explicit notation:
>>> x = polySphere(name='earth')[0]
>>> x.attr('myAttr')
Traceback (most recent call last):
...
MayaAttributeError: Maya Attribute does not exist: u'earth.myAttr'
Shorthand notation:
>>> x = polySphere(name='moon')[0]
>>> x.myAttr
Traceback (most recent call last):
...
AttributeError: Transform(u'moon') has no attribute or method named 'myAttr'
Testing Node Existence¶
Still supported:
>>> if objExists('fooBar'):
... print "It Exists"
... else:
... print "It Doesn't Exist"
It Doesn't Exist
PyMEL construct:
>>> try:
... PyNode('fooBar')
... print "It Exists"
... except MayaObjectError:
... print "It Doesn't Exist"
It Doesn't Exist
Testing Attribute Existence¶
Still supported:
>>> if objExists('persp.spangle'):
... print "Attribute Exists"
... else:
... print "Attribute Doesn't Exist"
Attribute Doesn't Exist
PyMEL construct:
>>> x = PyNode('persp')
>>> if x.hasAttr('spangle'):
... print "Attribute Exists"
... else:
... print "Attribute Doesn't Exist"
Attribute Doesn't Exist
PyMEL construct:
>>> try:
... PyNode( 'persp.spangle' )
... print "Attribute Exists"
... except MayaAttributeError:
... print "Attribute Doesn't Exist"
Attribute Doesn't Exist
PyMEL construct:
>>> x = PyNode('persp')
>>> try:
... x.spangle
... print "Attribute Exists"
... except AttributeError:
... print "Attribute Doesn't Exist"
Attribute Doesn't Exist
Manipulating Names of Non-Existent Objects¶
One advantage of the old way of dealing with non-existent objects was that you could use the name parsing methods of the PyNode
classes to manipulate the object’s name until you found what you were looking for. To allow for this, we’ve added
several classes which operate on non-existent nodes and contain only methods for string parsing and existence testing.
These nodes can be found in the other
module and are named other.NameParser
, other.AttributeName
, other.DependNodeName
,
and other.DagNodeName
.
Asserting Proper Type¶
While PyNode
serves to easily cast any string to its proper class in the node hierarchy, other nodes in the hierarchy can achieve the
same effect:
>>> PyNode('lambert1')
nt.Lambert(u'lambert1')
>>> DependNode('lambert1')
nt.Lambert(u'lambert1')
If the determined type does not match the requested type, an error will be raised. For example, a lambert node is not a DAG node:
>>> nt.DagNode('lambert1')
Traceback (most recent call last):
...
TypeError: Determined type is Lambert, which is not a subclass of desired type DagNode
This is useful because it can be used as a quick way to assert that a given node is of the desire type.
>>> select('lambert1') # this line represents user action
>>> try:
... nt.DagNode( selected()[0] )
... except TypeError:
... print "Please select a DAG node"
Please select a DAG node
Building User Interfaces¶
As with Maya nodes, pymel adds the ability to use object-oriented code for building MEL GUIs. Like nodes and
PyNodes, every UI command in maya.cmds has a class counterpart in pymel derived from the base class PyUI
.
There is one class for every UI element type, each with necessary methods to get and set properties. And as with nodes,
the procedural commands you already know and love are retrofitted to return PyUI
classes, so you don’t have to completely change the way you code
from pymel.core import *
win = window(title="My Window")
layout = columnLayout()
chkBox = checkBox(label = "My Checkbox", value=True, parent=layout)
btn = button(label="My Button", parent=layout)
def buttonPressed(*args):
if chkBox.getValue():
print "Check box is CHECKED!"
btn.setLabel("Uncheck")
else:
print "Check box is UNCHECKED!"
btn.setLabel("Check")
btn.setCommand(buttonPressed)
win.show()
Command Callbacks¶
One common point of confusion when building UIs with python is command callbacks. There are several different ways to handle command callbacks on user interface widgets.
Function Name as String¶
The simplest method of setting up a callback is to pass the name of the callback function as a string. Maya will try to execute this as python code. Here’s a simple example:
from pymel.core import *
def buttonPressed():
print "pressed!"
win = window(title="My Window")
layout = columnLayout()
btn = button( command='buttonPressed()' )
showWindow()
This example works fine if you run it from the script editor, but if you save it into a module, say myModule
, and then import that module as normal ( e.g. import myModule
), it will cease to work (assuming you haven’t already run it from the script edtior). This is because the namespace of the buttonPressed
function has changed. It can no longer be found as buttonPressed
, because from Maya’s perspective, its new location is myModule.buttonPressed
.
There are several solutions to this.
you can import the contents of
myModule
directly into the main namespace ( e.g.from myModule import *
). This will allowbuttonPressed
to be accessed without the namespace. This is not generally recommended as its clutters up your main namespace which could ultimately lead to name clashes.you can change your script and prefix the function with the module it will be imported from:
button( command="myModule.buttonPressed()" )
The problem with both of these solutions is that you must ensure that the module is always imported the same way, and, if you plan to share your module with someone, it’s pretty impossible to do this.
A more robust solution is to include an import command in the string to execute:
button( command="import myModule; myModule.buttonPressed()" )
That gives us a fairly reliable solution, but it still has one major drawback: any parameters that we wish to pass to our function must also be converted to strings. This becomes impractical when the parameters are complex objects, such as dictionaries, lists, or other custom objects.
So, while string callbacks may seem simple at first, they have limitations, and are generally not recommended.
Function Object¶
When using this technique, you pass an actual function object instead of a string.
from pymel.core import *
def buttonPressed():
print "pressed!"
win = window(title="My Window")
layout = columnLayout()
btn = button(command=buttonPressed)
showWindow()
Note
The callback function has to be defined before it is passed to the command flag.
The difference from the previous example is subtle: command="buttonPressed()"
is now command=buttonPressed
. The most important thing to realize here is that buttonPressed
is not a string, it is a python function object. As such, if we had included ()
( e.g. command=buttonPressed()
) we would have executed the buttonPressed
function immediately, but we don’t want that to happen yet. By leaving the parentheses off, we hand the function over to the UI element to execute later. For most commands that support callbacks, Maya recognizes when you are passing a string and when you are passing a function, and it treats each differently when the callback is triggered.
This method is very robust, but there are a few caveats to be aware of. To see what I mean, try executing the code above and pressing the button...
...when we press it, we get this error:
# TypeError: buttonPressed() takes no arguments (1 given) #
Why?! The button
UI widget, like many others, automatically passes arguments to your function, whether you want them or not. Sometimes these arguments contain the state of the UI element, such as whether a checkbox is on or off, but in our case they are pretty useless.
Note
The automatic passing of arguments to python callbacks is an attempt to recreate a feature of MEL. When creating callbacks in MEL you can request that your callback procedure be passed an argument when the callback is triggered. Take the example below:
radioButton -changeCommand "myRadButtCB #1";
When the callback is executed, the #1
gets replaced with the current state of the radioButton: 0
or 1
. Unfortunately, when using python callbacks, you can’t request which arguments you want, you get them all.
So, to make our callback work, we need to modify the function to accept the argument that the button command
callback is passing us:
def buttonPressed(arg):
print "pressed!"
The tricky part is that different UI elements pass differing numbers of arguments to their callbacks, and some pass none at all. This is why it is best for your command to use the *args
syntax, like so:
def buttonPressed(*args):
print "pressed! here are my arguments %s" % ( args, )
The asterisk in front of args
allows the function to accept any quantity of passed arguments. All of the positional arguments to the function are stored in the variable args
(without the *
) as a read-only list, known as a tuple. Making it a habit to use this syntax for your callbacks can save you a lot of headache.
Putting it all together:
from pymel.core import *
def buttonPressed(*args):
print "pressed!"
win = window(title="My Window")
layout = columnLayout()
btn = button( command=buttonPressed )
showWindow()
Lambda Functions¶
The next technique builds on the last by simplifying the following situations:
- You want to pass arguments to your callback function other than those automatically sent by the UI element
- You’re using a function that someone else wrote and can’t add the
*args
to it
For example, I want to pass our buttonPressed
function a name argument. Here’s how we do this using a lambda function:
from pymel.core import *
def buttonPressed(name):
print "pressed %s!" % name
win = window(title="My Window")
layout = columnLayout()
btn = button( command = lambda *args: buttonPressed('chad') )
showWindow()
So, what exactly is a lambda? It’s a shorthand way of creating a simple function on one line. It’s usually used when you need a function but you don’t need to refer to it later by name, which makes it well suited for callbacks. Combining lambda functions with the lessons we learned above adds more versatility to command callbacks. You can choose exactly which args you want to pass along.
Let’s clarify what a lambda is. In the above example, this portion of the code...
lambda *args: buttonPressed('chad')
...could have been written as:
def tempFunc(*args):
return buttonPressed('chad')
We would have then passed tempFunc
as the callback:
btn = button( command = tempFunc )
Instead, we use the lambda to create the function on one line and pass it directly to the command
flag.
btn = button( command = lambda *args: buttonPressed('chad') )
The lambda function serves as a mediator between the UI element and our real callback, buttonPressed
, giving us control over what arguments will be passed to buttonPressed
. In our example, we ignore all the arguments passed by the button and instead opt to pass the string 'chad'
, however, if the circumstances require it, you could use the arguments in args
as well:
btn = button( command = lambda *args: buttonPressed('chad', args[0]) )
In the example above we’re using the first of the arguments passed by the button (remember, args
is a tuple, which is like a list) and passing it on to our callback in addition to the name string. Keep in mind that for this to work our buttonPressed
callback would need to be modified to accept two arguments.
Whew! That was a lot to learn, but unfortunately, this method has a drawback, too. It does not work properly when used in a ‘for’ loop.
In the following example, we’re going to make several buttons. Our intention is that each one will print a different name, but as you will soon see, we won’t succeed.
from pymel.core import *
def buttonPressed(name):
print "pressed %s!" % name
win = window(title="My Window")
layout = columnLayout()
names = ['chad', 'robert', 'james']
for name in names:
button( label=name, command = lambda *args: buttonPressed(name) )
showWindow()
When pressed, all the buttons will print ‘james’. Why is this? Think of a lambda as a “live” object. It lives there waiting to execute the code it has been given, but the variables in that code are live too, so the value of the variable named character
changes with each iteration through the loop, thereby changing the code that lambda is waiting to execute. What is its value at the end of the loop? It’s ‘james’. So all the lambda’s execute the equivalent of:
buttonPressed('james')
To solve this we need to “pin” down the value of our variable to keep it from changing. To do this, pymel provides a Callback
object...
Callback Objects¶
In my experience this method handles all cases reliably and predictably, and solves the ‘lambda’ issue described above.
A Callback
object is an object that behaves like a function, meaning it can be ‘called’ like a regular function.
The Callback object ‘wraps’ another function, and also stores the parameters to pass to that function. And, lucky for you, the Callback class is included with pymel.
Here’s an example:
from pymel.core import *
def buttonPressed(name):
print "pressed %s!" % name
win = window(title="My Window")
layout = columnLayout()
names = ['chad', 'robert', 'james']
for name in names:
button( label=name, command = Callback( buttonPressed, name ) )
showWindow()
Our example now works as intended. The Callback
class provides the magic that makes it work.
Pay close attention to how the Callback is created: the first argument, buttonPressed
, is the function to wrap, and the rest are arguments to that function. The Callback stores the function and its arguments separately and then combines them when it is called by the UI element.
So, on assignment, something that looks like this...
Callback( buttonPressed, name )
...on execution becomes:
buttonPressed(name)
In this case, the variable name
is the only additional argument, but the Callback class can accept any number of arguments or even keyword arguments, which it will dutifully pass along to your function when the callback is triggered.
The Callback
class ignores any arguments passed in from the UI element, so you don’t have to design your function to take these into account. However, if you do want these, use the alternate callback object CallbackWithArgs
which will pass the UI arguments after yours.
Layouts¶
One major pain in designing GUIs is the placing of controls in layouts. Maya provides the formLayout command which lets controls resize and keep their relationship with other controls, however the use of this command is somewhat cumbersome and unintuitive. Pymel provides an extended FormLayout class, which handles the details of attaching controls to one another automatically:
win = window(title="My Window")
layout = horizontalLayout()
for i in range(5):
button(label="button %s" % i)
layout.redistribute() # now will redistribute horizontally
win.show()
The ‘redistribute’ method redistributes the children (buttons in this case) evenly in their layout. A formLayout will align its controls vertically by default. By using the ‘verticalLayout’ or ‘horizontalLayout’ commands you can explicitly override this (note that both commands still return a FormLayout object):
By default, the control are redistributed evenly but this can be overridden:
layout.redistribute(1,3,2) # (For 5 elements, the ratios will then be 1:3:2:1:1)
You can also specify the ratios at creation time, as well as the spacing between the controls. A ratio of 0 means that the control will not be resized, and will keep a fixed size:
win = window(title="My Window")
layout = horizontalLayout(ratios=[1,0,2], spacing=10)
for i in range(5):
button(label="button %s" % i)
layout.redistribute() # now will redistribute horizontally
win.show()
Finally, just for fun, you can also reset, flip and reverse the layout:
layout.flip() # flip the orientation
layout.reverse() # reverse the order of the controls
layout.reset() # reset the ratios
Anyone who has coded GUIs in Maya using both MEL and python will tell you that if there is one thing they miss about MEL (and only one thing), it is the use of indentation to organize layout hierarchy. this is not possible in python because tabs are a syntactical element, indicating code blocks. In this release, PyMEL harnesses python’s with
statement to use indentation to streamlines the process of GUI creation.
Here is a comparison of the uiTemplate
example from the Maya docs.
First, using maya.cmds
:
import maya.cmds as cmds
if cmds.uiTemplate('ExampleTemplate', exists=True):
cmds.deleteUI('ExampleTemplate', uiTemplate=True)
cmds.uiTemplate('ExampleTemplate')
cmds.button(defineTemplate='ExampleTemplate',
width=100, height=40, align='left')
cmds.frameLayout(defineTemplate='ExampleTemplate', borderVisible=True,
labelVisible=False)
window = cmds.window(menuBar=True,menuBarVisible=True)
cmds.setUITemplate('ExampleTemplate', pushTemplate=True)
cmds.columnLayout(rowSpacing=5)
cmds.frameLayout()
cmds.columnLayout()
cmds.button(label='One')
cmds.button(label='Two')
cmds.button(label='Three')
cmds.setParent('..')
cmds.setParent('..')
cmds.frameLayout()
cmds.optionMenu()
menuItem(label='Red')
menuItem(label='Green')
menuItem(label='Blue')
cmds.setParent('..')
cmds.setParent('..')
cmds.setUITemplate(popTemplate=True)
cmds.showWindow( window )
menu()
menuItem(label='One')
menuItem(label='Two')
menuItem(label='Sub', subMenu=True)
menuItem(label='A')
menuItem(label='B')
setParent('..', menu=1)
menuItem(label='Three')
Now, with PyMEL:
from __future__ import with_statement # this line is only needed for 2008 and 2009
from pymel.core import *
template = uiTemplate('ExampleTemplate', force=True)
template.define(button, width=100, height=40, align='left')
template.define(frameLayout, borderVisible=True, labelVisible=False)
with window(menuBar=True,menuBarVisible=True) as win:
# start the template block
with template:
with columnLayout( rowSpacing=5 ):
with frameLayout():
with columnLayout():
button(label='One')
button(label='Two')
button(label='Three')
with frameLayout():
with optionMenu():
menuItem(label='Red')
menuItem(label='Green')
menuItem(label='Blue')
# add a menu to an existing window
with win:
with menu():
menuItem(label='One')
menuItem(label='Two')
with subMenuItem(label='Sub'):
menuItem(label='A')
menuItem(label='B')
menuItem(label='Three')
Python’s with
statement was added in version 2.5 (Maya 2008 and 2009). Its purpose is to provide automatic “enter” and “exit” functions for class instances that are designed to support it. This is perfect for MEL GUI creation: for example, when we enter the with
block using a PyMEL ui.Layout
class or a command that creates one, the layout object sets itself to the active default parent, and when the code block ends, it restores the default parent to it’s own parent. There is now little need to ever call setParent
. As you can see in the example, the with
statement also works with windows, menus, and templates: windows call setParent
and showWindow
, and templates are automatically “pushed” and “popped”.
Standalone Maya Python Interpreter¶
Maya’s standalone interpreter can be useful for both development and batch processing, as an alternative to maya -batch
.
To use maya functions in an external python interpreter, maya provides a handy executable called mayapy
, which you can find in the maya bin directory. Before using maya.cmds
in this interpreter, you must first initialize Maya:
import maya.standalone
maya.standalone.initialize(name='python')
import maya.cmds as cmds
The problem with this is that it does not fully initialize Maya the way that it would be when using maya -batch
or the Maya UI, and as a result, certain scripts and plugins will not be available. This can lead to errors since many developers test their code in a Maya GUI session, assuming that mayapy
will behave the same.
PyMEL ensures that using python within mayapy
is as close as possible to using maya in batch mode. When PyMEL detects that it is being imported in a standalone interpreter it performs these operations:
- initializes maya.standalone
- sources Autodesk’s initialization MEL scripts
- sources user preferences
- sources userSetup.mel
Because of these improvements, working in this standalone environment is nearly identical to working in interactive mode, except of course you can’t create windows.
Warning
There is one caveat that you must be aware of: scriptJobs do not work: use callbacks derived from api.MMessage
instead.
In order to use mayapy
you must first properly setup your system environment.
Appendices:
Seting Up PyMEL Autocompletion in Eclipse¶
The methodology for setting up auto-completion has been dramatically simplified with this release. We now include “stub” files which can be used with almost any IDE to provide code completion. These stub files comprise “dummy” versions of the pymel and maya packages: they contain all the function and class definitions along with their documentation strings, but do not contain any functional code. This method (first brought to my attention by Ron Bublitz) has many advantages over using the “real” pymel package for completion:
- easier to setup
- faster and more reliable
- completes ui commands
- completes maya.cmds and maya.OpenMaya
The only disadvantage that has come to my attention is that IDE tools that reveal the source of a function will direct you to the stub packages, which contain no real code and therefore won’t be particularly helpful. However, this is really only a concern for those who are brave enough to delve into pymel code.
Before You Begin¶
These instructions are updated for Pydev 1.4.6, and should work on either Eclipse 3.4.x or 3.5.x
Note
If you checked out PyMEL from our git repo then you will need to generate the stubs first.
To generate stubs:
be sure that pymel is on the path by following the Manual Install method.
open a Maya GUI and run the following in a python tab of the script editor:
import maintenance.stubs maintenance.stubs.pymelstubs()
Adding The Maya Python Interpreter¶
Note
with the introduction of the new completion stubs, it is no longer necessary to use mayapy as your interpreter – any python interpreter will do. However, it’s still a good idea to use Maya’s interpreter to ensure that the site-packages within it are the same as when you are using Maya.
Open the Eclipse preferences window.
Windows OSX under the Window menu: under the Eclipse menu: In the left pane, drop down to Pydev > Interpreter-Python
Click the New.. button at the top right of the Python Interpreters preferences window
In the window that comes up give your interpreter a name (such as maya2009-osx).
Next, you can either copy and paste the path to your maya interpreter (aka
mayapy
) or you can click ‘Browse’ and navigate to it.Note
On OSX, browsing to
mayapy
is not as easy as it should be. The problem is that it’s buried within Maya.app, which you cannot access in a file browser (thanks Apple!). To get to it, hold down Command+Shift+G to bring up a box to enter a path (that’s the Apple “Command” button, plus Shift, plus the letter G). You can’t use Command-V to paste a path in this browser, but you can right click in the path entry box and choose Paste.Windows OSX C:\Program Files\Autodesk\Maya2009\bin\mayapy.exe
/Applications/Autodesk/maya2009/Maya.app/Contents/bin/mayapy
Once you choose the “mayapy” binary, you’ll get this window:
Windows OSX On windows: add a check beside
python25.zip
then press “OK”
- From the list, select the one path that ends with
site-packages
and click the “remove” button. Remember this path because we are going to re-add it later. - If you installed PyMEL using the Easy Install method: you’ll see the pymel “egg” in the list of automatically detected site packages. Remove the pymel egg
- Click on the “New Folder” button. In the browser that pops up, navigate to the directory where you extracted the pymel zip file. Under it, there is a folder called
extras
, under that a folder calledcompletion
, and then finally one calledpy
. Choose thepy
folder and press “OK”. - Click the “New Folder” button again, and add the
site-packages
directory you removed earlier. We did this in order to ensure that the stub maya package is found before the real maya package. When you’re done, the mainsite-packages
directory should be somewhere below theextras/completion/py
folder you just added.
Testing That It Worked¶
Restart Eclipse
Create a new file from within eclipse ( File / New / File ) named foo.py or whatever you want ( just make sure to include the .py )
Add the following line:
import pymel.core as pm
Save the file. Sometimes this helps force pydev to begin performing completion
Now type:
pm.bin
you should get
bindSkin()
as a completion.
Note
If you like to import everything from pymel, aka from pymel.core import *
, then you should open the Eclipse preferences, go to Pydev > Editor > Code Completion, and enable Autocomplete on all letter chars and ‘_’
Troubleshooting¶
If you’re still not getting completion:
- Go to Eclipse preferences under Pydev > Editor > Code Completion and increase Timeout to connect to shell to 30 seconds or more.
- Restart Eclipse and retry steps 3-5 above
- Open a log view (Window / Show View / Error Log) and if you see any suspicious errors, post for help at the Pydev suport forum
For Developers¶
An Overview of PyMEL’s Wrapping Mechanisms¶
Parsed Caches and Maintained Constants¶
In order to fuse the many disparate parts of maya’s python package into a cohesive whole PyMEL requires a great deal of data describing how each MEL command and API class works: the arguments they require, the results they return, and their relationships to each other. Some of this data is parsed and cached, while other bits are manually maintained.
Below is a description of how each major cache is created.
MEL Commands¶
- gather MEL function data
- parse data from MEL documentation
- merge this with info from MEL
help
command
create a dictionary of MEL-commands to the maya nodes they create, query, and edit
create a dictionary of MEL-commands to the ui elements they create, query, and edit
run tests on node commands to gather additional information required to ensure values returned by queries are compatible with values required for edits
pickle this info into two separate caches: one used for auto-wraps and the other for doc strings. the latter data can be lazily loaded on request since it is not required for the wrap itself.
Nodes and API Classes¶
- gather API class data
- parse API docs
- correct erroneous docs based on documentation
- gather node hierarchy data
- parse maya node hierarchy docs
- determine MFn-to-maya-node mappings
- determine apiType-to-maya-node mappings
pickle this info into two separate caches: one for auto-wraps and the other for doc strings
Bridge¶
A special control-panel GUI is used to manage a dictionary controlling how MEL and API interact to produce PyNode classes. It is known as the bridge. The bridge allows us to:
- create manual overrides to correct input and output types
- create mappings between MEL commands and API commands that perform the same task
- specify whether MEL or API should be used to generate each PyMEL method
- specify the name of the method that will be produced
Run Time Function and Class Factories¶
When PyMEL is imported it uses the cached data to generate the wrapped functions and classes you’ve come to know and love. Here’s how in a nutshell.
Functions¶
Every command is wrapped in up to 3 stages.
1. Data Compatibility Wrap¶
pymel.internal.pmcmds
wraps every function in maya.cmds
such that any arguments that are passed to it are first converted into datatypes that maya.cmds
will accept (string, int, float, or list/tuple thereof). The way we do this is simple yet powerful: if the argument has a __melobject__
method, we evaluate it and use the result in place of the original value. It is the responsibility of this method to return a “MEL-friendly” representation of the object’s data. For example, core.PyNode.__melobject__
returns its object’s name as a string, and datatypes.Matrix.__melobject__
returns itself converted into a flat 16-item list.
2. Manual Wrap¶
Optional manual wraps are created for cases that cannot be handled automatically or semi-automatically (below). They can use the auto-wrapped function in pmcmds
as a starting point
3. Automatic Wrap¶
Certain wraps are applied automatically based on information attained during parsing. If a manual wrap of the function is found, it is used as the starting point, otherwise the lower-level pmcmds
wrap is used.
For each function:
- add open-ended time ranges to appropriate flags: (1,None), (1,), slice(1,None), “1:”, etc
- cast results to
PyNode
orPyUI
if it is a node or UI command, correcting where determined necessary in tests - fix UI callbacks to return proper python objects instead of ‘true’, ‘false’, ‘1’, ‘0’, etc
- perform simple wraps: these are manually maintained, semi-automatic wraps of commands that are altered in standard and straightforward ways. For example, a command that should return a
PyNode
on a particular query is a wrap that can be handled easily through this mechanism. - add docstrings based on cached data
Classes¶
- manually create classes that need custom methods
- add the appropriate
__metaclass__
attribute:internal.factories.MetaMayaNodeWrapper
orinternal.factories.MetaMayaUIWrapper
for each node and UI type:
choose the appropriate metaclass
- add methods:
- use bridge to determine whether to use MEL or API
- skip if it has already been manually added
Contributing¶
Getting Started with Git¶
The PyMEL project hosts its code on github.
- Sign up for a Github account
- Check out the Github Guides for instructions on how to setup git for your OS
- Download a GUI front-end: SmartGit and TortoiseGit are by far my favorites
- Make a fork of the main PyMEL repository, and start hacking
To get maya to use the version of pymel you have just checked out, either manually add it to your PYTHONPATH environment variable, or cd into the directory, then run:
PATH/TO/MAYA/INSTALL/bin/mayapy setup.py develop
When you “push” your changes up to Github, we’ll be able to track them, give you feedback, and cherry-pick what we like. You, in turn, will be able to easily pull new changes from our repo into yours.
Running the Tests¶
PyMEL has a suite of unit tests. You should write a test for every major addition or change that you make. To run the tests, you need ‘nose’, which is a discovery-based test runner, that builds on python’s unittest
module and makes writing running a lot of tests a lot easier. Here’s the easiest way to get nose.
On linux/osx:
sudo mayapy setup.py easy_install nose
On windows:
mayapy.exe setup.py easy_install nose
This assumes that you’ve properly setup your environment, which you should definitely know how to do before contributing to PyMEL.
Design Philosophy¶
Background¶
MEL is a procedural language, meaning it provides the ability to encapsulate code into reusable “procedures” ( aka “functions” ). (This is probably old news to you, but bear with me, there’s a midly entertaining analogy coming up ). The term “procedural programming” is mentioned primarily in the context of disguishing something from the newer, object-oriented paradigm. If you have used MEL extensively you know you can get pretty far with procedural programming alone, but once you’ve gotten comfortable with python’s object-oriented design, you will never go back.
Object-oriented programming adds another level of organization by creating logical groupings of functions which are accessed from a common “object”. A quick perusal of the hundreds of MEL commands in the Maya documentation will give you an idea why these groupings are a good idea. MEL is like a toolchest, a wardrobe, and a kitchen set all dumped into a bathtub – everything in there is useful, but you’ve really got to know what you’re looking for to get anything done. Through the use of classes and modules, python makes sure that everything is in its right place.
So now that python is availabe in Maya all of our problems are solved, right? Not quite. The root of the problem is that maya.cmds is just a python wrap of the same underlying MEL codebase we’ve had all along. And since it was never intended to be python in the first place, the syntax that results from this layering of Python over MEL tends to be awkward, especially to those used to python idioms.
The C++ API also has a python wrap but it too suffers from awkward and unpythonic idioms, stemming from its C++ heritage. Unlike MEL, Maya’s C++ API benefits from the fact that it was object-oriented to begin with, but from a scripters’ standpoint, it’s tortuously verbose and cryptic. Certainly nothing you would want to write an entire pipeline with.
Enter PyMEL. The primary reasons for pymel’s existence are threefold:
- to fix bugs in
maya.cmds
- to modify the behavior of maya.cmds to improve workflow and make it more pythonic ( like returning an empty list instead of None )
- to provide a complete object-oriented design for working with nodes, attributes, and other maya structures
If you’re still not sure you’re ready to make the jump to object-oriented programming, the first two points alone are reason enough to use pymel, but the object-oriented design is where PyMEL really shines. PyMEL strikes a balance between the complicated yet powerful API, and straightforward but unruly MEL.
Procedural (maya.cmds
)¶
When approaching the reorganization of the existing commands provided by maya.cmds
, PyMEL follows these practical guidelines:
- a value returned by a query flag should be accepted as a valid argument by the corresponding edit flag
- example:
camera( 'persp', e=1, focalLength = camera( 'persp', q=1, focalLength=1) )
- a function which returns a list should return an empty list (not None) if it finds no matches
- example:
ls
,listRelatives
- a function which always returns a single item should not return that item in a list or tuple
- example:
spaceLocator
- wherever possible, pymel/python objects should be returned
- example: pretty much every command
- a function which provides a mapping mechanism should have a dictionary-like PyMEL counterpart
- example:
fileInfo
andFileInfo
,optionVar
andOptionVar
- a function which returns a list of pairs should be a 2D array, or possibly a dictionary
- example:
ls( showType=1 )
,listConnections(connections=1)
- the arguments provided by a ui callback should be of the appropriate type
- as a test, it should be capable of being used to set the value of the control
- if a function’s purpose is to query and edit maya nodes, that node should be passed as an argument, not a keyword
- example:
sets
Object-Oriented¶
In constructing the PyNode classes, PyMEL follows these design rules:
- node classes should never use properties – all behavior should be placed in methods to differentiate them from shorthand attribute syntax
- ( ex.
foo.bar
retrieves anAttribute
class,foo.bar()
executes a function )
- node classes are named after the nodes they control, not the mel commands that they proxy
- ( ex.
nt.Locator
vs.spaceLactor
)a value returned by a get* function should be accepted as a valid argument by the corresponding set* function
MEL to Python Cheat Sheet¶
Procedures vs. Functions¶
MEL
global proc myproc( string $arg1, int $arg2 )
{
print ($arg1 + " " + arg2 + "\n" );
}
Python
def myfunc( arg1, arg2 ):
print arg1, arg2
Scripts vs. Modules¶
MEL
source "myScript.mel";
Python
import myModule
In MEL, procedures are often readily available to use, even if you have not explicitly sourced the MEL script that contains it. This automatic availability is only achieved if a script is on the MAYA_SCRIPT_PATH
and it contains a global procedure with the same name as the script. Many people are not aware of this caveat and it is a source of great confusion and inconsistent results.
Python has a more explicit set of rules. Any python script with a .py extension found on the PYTHONPATH
is considered a module and must be imported inside every script that utilizes it. This usually occurs at the very top of a file. Think of it as a way of telling python and anyone else who looks at your module exactly what other modules it depends on.
Module Namespaces¶
The primary feature that conceptually distinguishes a module from a MEL script is that a module by default serves as its own namespace. In other words, once the module is imported, any functions contained inside it must be referenced with the module’s name as a prefix (without the .py extension):
import myModule
myModule.myfunc()
The concept of namespaces should be familiar to anyone who has used referencing in Maya. In Maya, namespaces provide organization and help avoid conflicts between the contents of each referenced file. And like Maya, python provides the ability to modify the default namespace when importing.
Here’s how you can change the namespace:
import myModule as mod
mod.myfunc()
or you can get rid of the namespace all together:
from myModule import *
myfunc()
Environment Variables¶
MEL | Python |
---|---|
MAYA_SCRIPT_PATH |
PYTHONPATH |
Vectors¶
MEL | Python |
---|---|
<<0,1,2>> |
Vector(0, 1, 2) |
tokenize¶
MEL
string $buf1[];
tokenize( "chad dombrova", $buf1 );
string $buf2[];
tokenize( "joint_01_left_leg", $buf2, "_" );
Python
- ::
- buf1 = ‘chad dombrova’.split() buf2 = ‘joint_01_left_leg’.split(‘_’)
Module Documentation¶
pymel.core
¶
The primary sub-package.
The following submodules are brought directly into the pymel.core
namespace. They contain all of the wrapped maya.cmds
functions.
pymel.core.animation |
|
pymel.core.effects |
|
pymel.core.general |
|
pymel.core.language |
|
pymel.core.modeling |
|
pymel.core.other |
|
pymel.core.rendering |
|
pymel.core.system |
|
pymel.core.windows |
|
pymel.core.context |
pymel.core.datatypes
¶
Data classes that are returned by functions within pymel.core
. Automatially imported into pymel.core
as pymel.core.dt
.
pymel.core.datatypes |
pymel.core.nodetypes
¶
Classes corresponding to each node type in Maya. Returned by functions within pymel.core
. Automatially imported into pymel.core
as pymel.core.nt
.
pymel.core.nodetypes |
pymel.core.uitypes
¶
Classes corresponding to each UI type in Maya. Returned by functions within pymel.core
. Automatially imported into pymel.core
as pymel.core.ui
.
pymel.core.uitypes |
pymel.core.runtime
¶
Runtime commands are kept in their own namespace to avoid conflicts with other functions and classes.
pymel.util
¶
Utilities that are independent of Maya.
pymel.util.arguments |
|
pymel.util.arrays |
|
pymel.util.common |
|
pymel.util.decoration |
|
pymel.util.enum |
|
pymel.util.mathutils |
|
pymel.util.namedtuple |
|
pymel.util.path |
|
pymel.util.utilitytypes |
pymel.versions
¶
Functions for getting and comparing versions of Maya. Can be safely imported without initializing maya.cmds
.
pymel.versions |
pymel.mayautils
¶
Utilities for getting Maya resource directories, sourcing scripts, and executing deferred. Can be safely imported without initializing maya.cmds
.
pymel.mayautils |
pymel.tools
¶
pymel.tools.envparse |
|
pymel.tools.mel2py |
|
pymel.tools.py2mel |
|
pymel.tools.loggingControl |
pymel.api
¶
pymel.api.plugins |