A blog about technical art, particularly Maya, Python, and Unity. With lots of obscurantist references
We've Moved
The blog has been retired - it's up for legacy reasons, but these days I'm blogging atblog.theodox.com. All of the content from this site has been replicated there, and that's where all of the new content will be posted. The new feed is here . I'm experimenting with crossposting from the live site, but if you want to keep up to date use blog.theodox.com or just theodox.com
Blogging has been super light, thanks to the triple whammy of a milestone, a cold, and my grand plan to migrate this blog over to a static site generator. However I did want to mention that I've been trying to update the minq wiki page so it is more accessible. I'd appreciate any feedback, comments or suggestions to make it clearer. When you write something yourself you just accept as natural the quirks and habits of thought that go with it, so it's handy to have outside eyes when you're writing documentation.
I've added a couple of minor features as well. There's a bunch of operators for counting things -- for example you can get the vertex count of meshes with something like Meshes().get(VertCounts)
. On a somewhat related note all streams have a count()
function which will return the length of the stream, and a first()
method which will pull the head item from a stream -- which is handy if you expect to narrow down to a single item and don't want a single-item iterable.
Last but not least I'd love to hear from the community about good minq hacks -- I'll be happy to add cool ones to the examples file. I'd also appreciate any bugs you find going into the issues page!
mGui updates in the offing...Changes on the way for mGui, the maya Gui framework
For those of you who’ve been using mGui to speed up and simplify your Maya gui coding, there are some interesting changes on the horizon. Although I’m not entirely ready to release the changes I have in mind they are mostly sitting in their own branch in the Github repo.
The upcoming version introduces some new idioms - in particular, it gets rid of the need for explicitly setting keys on new controls to get access to nested properties. In the first version of mGui you’d write something like this:
withgui.Window('window',title='fred')asexample_window:withVerticalForm('main')asmain:Text(None,label="Items without vertex colors")lists.VerticalList('lister').Collection<bind()<boundwithHorizontalStretchForm('buttons'):Button('refresh',l='Refresh')Button('close',l='Close')
With the new refactor that looks like this:
withgui.Window('window',title='fred')asexample_window:withVerticalForm()asmain:Text(label="Items without vertex colors")lister=lists.VerticalList()lister.collection<bind()<boundwithHorizontalStretchForm()asbutton_row:refresh=Button(label='Refresh')close=Button(label='Close')
The big advantage here is that those local variables are not scoped exclusively to the layout context managers where they live, which makes it easy to control when and where you hook up your event handlers: In the above example you could defer all the bindings and event handlers to the end of the script like this:
withgui.Window('window',title='fred')asexample_window:withVerticalForm()asmain:Text(label="Items without vertex colors")lister=lists.VerticalList()withHorizontalStretchForm()asbutton_row:refresh=Button(label='Refresh')close=Button(label='Close')lister.collection<bind()<boundrefresh.command+=refresh_defclose.command+=close_def
So far I’m really liking the new idiom, particularly eliminating the extra quotes and redundant None keys. However this is a minorly breaking change: in some cases, old code which relied on the key value to name and also label a control at the same time will when the keys become redundant. Moreover I bit the bullet and started to refactor the entire mGui module to use correct pep-8 naming conventions – in particular, member variables are no longer capitalized. So if you have code outside of mGui this will introduce some issues. When I converted my own code, most of the changes could be done with a regular expression but there were a few danglers.
I think the changes are worth the effort, but I’d be really interested in hearing from users before trying to bring the new mGui branch back into the main line. It should actually be possible to write a script that fixes most existing code automatically, that’s something we could refine collaboratively.Please let me know in the comments or by opening an issue on the GitHub site if you have comments or plans. As always, bug fixes and pull requests always entertained!
A touch of minq
If you’re a long-time reader, you may recall that i’m very ambivalent about wrapper code. I’m just as prone to adding my own little spoonful of syntax sugar on top of my daily tasks, but I’ve also been around long enough to be a bit cynical about my own various faddisms and dubious style choices over the years. Sure, extra typing is annoying – but nowadays I tend to set a pretty high bar for actually writing wrapper code instead of just, ya know, doing my actual job.
So, it’s with a little bit of trepidation that I’m sharing my latest library. Minq bills itself as ‘a query language for Maya scenes.’ The goal is to simplify a very common task for Maya coders: finding things in a scene.
Now, that isn’t a particularly interesting job most of the time, but it’s one we do a lot: a quick grep of my own codebase shows over 600 calls to cmds.ls(), cmds.listRelatives(), cmds.listHistory and cmds.nodeType() in various combinations: as far as I can tell, ls() is actually the single most common call I make.
Moreover, I’m reasonably certain (though I didn’t do the grepping to bear this out) that those hundreds of ls() calls are accompanied by hundreds of little snippets of code to deal with Maya’s quirks. How often have you run into little gems like this?
foritemincmds.ls(my_meshes,type='mesh'):printitem# Error: 'NoneType' object is not iterable# Traceback (most recent call last):# File "<maya console>", line 1, in <module># TypeError: 'NoneType' object is not iterable #
There are of course ways around these little gotchas - but given the number of times you have to interact with them it’s hard to be sure you’ve really nailed them all. In my case a 99% correct handlong of my ls() calls alone will produce at least 5 bugs.
More importantly – and, frankly, the whole reason for this project – dealing with these little gotchas is not an interesting job. Finding, filter and sorting stuff in your Maya scene is not am opportunity for you to display your brilliant algorithms or clever strategies for bending Maya to your will: it’s just a bunch of stuff you have to on your way to fixing the problems your users really want fixed.
Minq in action
Hence, minq.
The goal of minq is to provide a more concise and more readable way to find things in your maya scenes. Here’s an example to give you the idea of how the project is supposed to work.
Suppose you need to find all of your character skeletons and distinguish them from other things lying around in the scene. The easy way to do that is usually to look for assemblies (top level nodes) which have children who drive skinClusters. Here’s an example of how you could find all the root nodes in the scene which drive skins using conventional means:
You’ll notice the littering of or [] to make sure we don’t get errors for failed queries. We have to create two temporary variables (childen and history) in order to store the intermediate results. And, obviously, we’re 3 layers deep when we get to the actual work item.
Above all, though, you need to remember two little bits of Maya trivia to make sense of this code: that cmds.ls(asm=True) means ‘give me the assemblies’ and that listRelatives(ad=True) gives you the children of an object. These are, of course, very clear to Maya vets – but there are over 50 flags in ls() and more than a dozen in listRelatives() . I’ve been working in Maya for 20 years and I still need to look up most of them. You pass those flags to Maya as strings which won’t get evaluated until runtime – and it’s possible to mistype them and not even know because ls(), in particular, makes wierd tweaky decisions about how to interpret conflicting flags.
Here’s the minq equivalent to the previous function:
It’s shorter, but the real goal is to make it more readable. Here’s what happens, which should be pretty clear from the names:
1. drives_skin() takes a maya object
2. It gets all of that object’s children
3. It gets all of the future history of those children
4. It it filters down to only the skin clusters in that future history
5. it returns true if any skin clusters are present
The rest of it pretty self evident: unskinned_assemblies just collects all of the assemblies which pass drives_skin(). The algorithm is exactly the same as the first version – but, at least to me, that algorithm is actually expressed much more clearly in the minq version. As for concision, I deliberately broke the query into two lines to make it easier to read -- otherwise it could all be done in a single expression.
A purist will probably point out that there are important under-the-hood details in the first one that are hidden in the second, and s/he’d be right. However after doing a lot of this kind of code down the years I’m fairly certain that those important details have almost always been important because screwing them up causes problems – not because they provide an opportunity for a wizardly optimization or better approach to the problem. I’m interested in finding unskinned meshes, not in remembering to pass the correct flags to ls and listRelatives.
Here’s a couple of other examples to give you the flavor of what a minq query looks like:
# get all mesh transforms in a scenemesh_transforms=Meshes().get(Parents)# find stub jointsdefis_stub(obj):returnnotany(using(obj).get(Children).only(Transforms))stubs=Joints().where(is_stub)# filtering by type, by name, and with functionscube_creator_nodes=PolyCreators().only('polyCube')used_to_be_cubes=cube_creator_nodes.get(Future).only(Meshes)has_8_verts=lambdap:cmds.polyEvaluate(p,v=True)==8still_are_cubes=used_to_be_cubes.where(has_8_verts)# adding, subtracting or intersecting queriestoo_high=Transforms().where(item.ty>100)too_low=Transforms().where(item.ty<-100)middle_xforms=Transforms()-(too_high+too_low)
So, that’s the basic idea: to replace a lot of tedious boilerplate with something a little cleaner, more predictable and easier to maintain. The code for the whole thing is up on up on Github under the usual MIT, ‘use it as you like but keep the copyright header’ license. It’s still very much a work-in-progress and I’d love feedback, particularly on issues of syntax and style.
It’s the little things that really matter in life.
If you’ve ever spent any time wrestling with Maya distribution, you’ve probably noticed that userSetup.py executes in an odd fashion: it’s not a module that gets imported, it’s basically a series of statements that get executed when Maya fires up. Unfortunately that also means that most of the usual strategies you’d use in python to find out where, exactly, you are running from is problematic. The usual python tricks like __file__ don’t work; and most of the time asking for os.getcwd() will point at your Maya program directory. Usually you end up running around looking at all the directories where Maya might be stashing a userSetup and trying to figure out which one is the one you are in`. It’s ugly.
However today, I actually found one which works. At least, I haven’t figured out how to break it yet.
Since I’ve tried to figure this one out on at least a hundred previous occasions, I am feeling unduly smug about this one.
PS, if you’re wondering why I care: this makes it really easy to do a simple install/uninstall of a userSetup.py / userSetup.zip combo with no environment variables or special rules.
PPS: Take that, Maya!
We tend to be the kind of people who throw themselves into things – we live for the joy of problem solving. So when we’re really grappling with the intricacies of todays disaster, we immerse ourselves in it. We tear it apart and inspect all the little moving pieces till we understand it well enough to duct-tape it back together again.
Along the way, that attention to detail and mastery of nuance tends to make us think we know it all. But – a shock, I know – we don’t. More to the point, we might now it all for the moment. But we’ll dump that knowledge to make sure we have room for our encyclopedic knowledge of tomorrow’s problem. And next week’s. And next months.
In short, we’re constantly flushing our caches. Unless you’re stuck in a rut, doing the same thing every day, you’re constantly learning new little things for your current problem and silently shelving the knowledge you aquired for your last.
This is one reason why good code comments are so important. Sure, comments rot just like code. But a couple of well-placed notes about how and why the code looks the way it does can save future you a lot of time when many layers of memory recycling have left you completeley oblivious about what the hell past you was up to. I can easily think of a couple of embarrassing occasions where I’ve literally chased my own tail – done something non-obvious because of a wierd maya bug, then come back six month later to ‘clean up’ my ‘ugly code’ and of course hit the exact same bug again.
Of course, good comments don’t have to have high literary quality, they don’t need to cover every variable and for loop, and they certainly don’t need to be overwhelming: what they should be is notes to future-self that will help him or her revive all the fading memories which seem so obvious today but which will be utterly erased before the next season of Silicon Valley is released.
Which brings me, by a very roundabout route, to what I actually set out to talk about: a perfect case in point. I was noodling around with a system that needed to fire events for maya attribute changes: basically, a way to make attributeChanged scriptJobs that were easy to start, stop and restart. So I did a little googling and…
Yep. I’d already written it. I’d even put it up on Github.
In my defense, I realized in retrospect that I had cancelled the project at work that made it necessary the first time: I did the work on the system, got it ready to go, and then decided that there was a simpler way to solve the problem without all those attribute-change scripts anyway. Nonetheless it’s a perfect illustration of how thoroughly one’s short-term memory cache gets flushed – and of the importance of leaving good comments. At least when I found the damn thing the readme that Github makes you put up reminded me how it was supposed to work (as an aside, it’s a great reason for putting your stuff up on GitHub or similar forums: knowing that other people will be looking at it forces you to clean up and document more than you would if you just decided to shelve a project).
So there you have it: an object lesson in the importance of clarity in tools development and a free module for messing around with AttributeChange scriptJobs!
I've posted a couple of fixes to the code for the shaderfx module I posted a little while ago. +Sophie Brennan spotted a problem with the way that module handled some kinds of nodes -- which I had assumed were just plain old objects but which were in fact buttoned-up group nodes. Since they didn't use the same method to report their outputs as the rest of shaderfx they could not easily be created or connected using the module.
Luckily +Kees Rijnen, the main author of shaderfx, noticed the blog post and helpfully pointed me at the source of the problem which I've included in a fix.
If you are using the original version of the code this may be a breaking change. To unify the way that individual nodes and groups are connected, I changed the connect() and disconnect() methods to take only two arguments where they previously took 4. In the first pass you would write
It's been a busy few months at work, and the blogging has been pretty light. But I promised some folks on the Tech-Artists.org slack that I'd share some code for dealing with Mays's ShaderFX system: a very useful toolkit but not the best documented or automatable part of Maya. Since it's New Years' Resolution time, I thought I'd kill two birds with one stone and put up some notes to go with the code
All of shaderfx in maya is organized by a single, undocumented command. Which is pretty lame.
However, it’s not as bad as it seems once you figure out the standard command form, which is always some variant of this form:
The sfxnode argument tells maya which sfx shader to work on. The command flag indiciates an action and the node id specifies an node in the network. Nodes are assigned an id in order of creation, with the firstnode after the root ordinarily being number 2 and so on – however the ids are not recycled so a network which has been edited extensively can have what look like random ids and there is no guarantee that the nodes will form a neat, continuous order.
Many commands take additional arguments as well. Those extra always follow the main command; thus
sets the value of the uiorder field on node 19 to a value of 1.
The shaderfx command can also return a value: to query the uiorder field in the example above you’d issue
So, the good news is that the shaderfx command is actually pretty capable: so far, at least, I have not found anything I really needed to do that the command did not support. For some reason the help documentation on the mel command is pretty sparse but the python version of the help text is actually quite verbose and useful.
Still, it’s kind of a wonky API: a single command for everything, and no way to really reason over a network as a whole. Worse, the different types of nodes are identified only by cryptic (and undocumented) numeric codes: for example a Cosine node is 20205 – but the only way to find that out is to use the getNodeTypeByClassName command (and, by the way, the node type names are case and space sensitive).
Cleanup crew
With all that baggage I was pretty discouraged about actually getting any work done using shaderfx programmatically. However a little poking around produced what I hope is a somewhat more logical API, which I’m sharing on github.
The sfx module is a plain python module - you can drop it into whatever location you use to story your Maya python scripts. It exposes two main classes: SFXNetwork represents a single shader network – it is a wrapper around the Maya shader ball. The SFXNetwork contains an indexed list of all the nodes in the network and also exposes methods for adding, deleting, finding and connecting the nodes in the network. SFXNode represets a single node inside the network. It exposes the properties of the node so they can be accessed and edited using python dot-style syntax.
The module also includes to submodules, sfxnodes and pbsnodes. These make it easier to work with the zillions of custom node ids: Instead of remembering that a Cosine node is type 20205, you reference sfxnodes.Cosine. I’ll be using the StingrayPBSNetwork class and the pbsnodes submodule in my examples, since most of my actual use-case involves the Stingray PBS shader. The syntax and usage, however, are the same for the vanilla SFXNetwork and sfxnodes – only the array of node types and their properties.
Here’s a bit of the basic network functionality.
Create a network
To create a new shaderfx network, use the create classmethod:
That creates a new shaderball (note that it won’t be connected to a shadingEngine by default – that’s up to you).
Listing nodes
An SFXNetwork contains a dictionary of id and nodes in the field nodes. This represents all of the graph nodes in the network. Note I’ve used a different shader than the default one in this example to make things easier to read.
The keys of the dictionary are the node ids. As already noted, these are not guaranteed to be in a continuous order depending on what you do to the network - however they are stable and they will always match the id numbers shown in the shaderfx ui when you activate the show node IDs toggle in the ShaderFX window.
The values of the node dictionary are SFXNode objects.
Adding new nodes
To add a node to the network use its add() method and pass a class from either the sfxnodes or pbsnodes submodule to indicate the type.
if_node=network.add(pbsnodes.If)# creates an If node and adds it to the networkvar_node=network.add(pbsnodes.MaterialVariable)# creates a MaterialVariable node and adds it to the network
Connecting nodes
Connecting nodes in shaderfx requires specifying the source node the source plug, the target node and the target plug. Unforunately the plugs are indentifited by zero-based index numbers: the only way to know them by default is to count the slots in the actual shaderfx UI. Output plugs are usually (not always) going to be index zero but the target plugs can be all over the map.
To make this cleaner, each SFXNode object exposes two fields called inputs and outputs, which have named members for the available plugs. So to connect the ‘result’ output of the var_node object to the input named ‘B’ on the if_node:
If the connection can’t be made for some reason, a MayaCommandError will be raised.
It’s common to have to ‘swizzle’ the connections: to connect the x and z channels of a 3-pronged output to channels of an input, for example. Mismatched swizzles are a common cause of those MayaCommandErrores. You can set the swizzle along with the connection by passing the swizzle you need as a string
network.connect(var_node.outputs.result,if_node.inputs.b,'z')# connects the 'x' output of var_node to the b channel of the input
Setting node properties
Nodes often have editable properties. There are a lot of different ones so it is often necessary to inspect a node and find out what properties it has and what type of values those properties accept. Every SFXNode object has a read-only member properties, which is a dictionary of names and property types. Using the same example objects as above:
printif_node.properties### BLah blah example here
If you know that a property exists on an object you can query it or set it using typical python dot syntax:
node=network.properties[5]# get the node at index 5 in this networkprintnode.properties:# { 'min': 'float', 'max': 'float', 'method': 'stringlist' }printnode.min# 1.0# getting a named property returns its value.node.min=2.0# sets the node valueprintnode.min# 2.0
If you try to access a property that doesnt exist, an error will be raised:
printnode.i_dont_exist# AttributeError: no attribute named i_dont_existnode.i_dont_exist=99# MayaCommandError
Help wanted!
So, there’s the basics. This module is pretty simople but I’ve found it extremely helpful in workign with SFX nodes. It will be much easier to work with, of course, if you already know your way around ShaderFX. Please let me know how it works for you – and as always bug reports and pull requests are very welcome!
Some kinds of pain are just occasional: you stub your toe or bump your head, ouch, and then its over.
Other kinds of pain aren't as sharp or as sudden... but they're chronic. That persistent twinge in your lower back may not hurt as much as a twisted ankle - but it's going to be there forever (at least unless you get in to power Yoga, or so my wife claims).
Maya is old enough to have a few of those chronic pains, and I just ran in to one which -- once we debugged it and figured it out -- I realized has been a constant irritant for at least the last decade and if my creaky old memory does not lie was a distinct pain in the butt as long ago as 2002. In another context I might even have been able to diagnose it but instead we spent a ton of time and energy working around an unexpected behavior which is, in fact, purely standard Maya. It's stupid Maya, but it's standard too. Maya, alas, is double plus ungood about mixing per-face and per-object material assignments. So, I figured I'd document this here for future sufferers: it might not ease the pain much, but at least you'll know you're not crazy.
The basic problem is that assigning materials to faces and to objects use slightly different mechanisms. If you check your hypergraph you'll see that per-face assignments connect to their shadingGroup nodes through the compInstObjectGroups[] attribute while object-level assigmemts go through the similar-but-not-identical instObjectGroups attribute (if you're looking for these in the docs, the component cone is inherited from the geometryShape class and the object version comes from dagNode).
As long as you're working with one object at a time this isn't a problem. However, if you're duplicating or copy-pasting nodes, there's a gotcha: If you ever try to merge meshes which have a mix of per-face and per-object assignments, Maya will magically "remember" old per-face assigments in the combined mesh. If you're a masochist, here's the repro:
create a object, give it a couple of different materials on different faces
duplicate it a couple of times
assign a per-object material to the duplicates, overriding the original per-face assignments
combine all the meshes.
Et voîla! The cloned meshes revert to their original assignments
What appears to happen is that those compInstObjectGroups connections are driven by hidden groupID nodes which don't get deleted when the per-face assignments are overridden by the per-object ones in step (3) . They stick around even though they aren't being used, and when the mesh is combined they step right back into their original roles.
If you're doing this interactively it's an annoyance. If you're got tools that do things like auto-combine meshes to cut down on transform load in your game.... well, it's a source of some surprising bugs and equally surprising bursts of profanity. But at least it'ss predictable.
The workaround: Before doing any mesh combination, delete the history and something harmless to this history of the meshes you're about to combine. (I use a triangulate step, since this happens only at export time) . That kills the rogue <code>groupID</code> nodes and keeps the combined mesh looking the way you intended.
When you’re shopping around for something new – whether it’s a cool new piece of software or just a kitchen gadget – it’s not uncommon to tell yourself, “man, I wish I had thought of that.” But what’s really impressive is when you see a polished product and you say to yourself Dammnit, I absolutely thought of that!, or I’ve been wanting this exact thing for years!. It’s a rare thrill when you stumble across something that seems as if it were a gift from some future self, come back to give you exactly what you wanted in a way that only you, yourself could.
One of my coworkers found one of those little somethings the other day - a product that will make pretty much any TA go feel like Christmas came a little early.
The Charcoal Editor from Chris Zurbrigg is a slick, polished replacement for Maya’s script editor. It’s a plugin (available for Maya on Mac, Windows and Linux) that offers many of the features of a slick Python IDE right inside of Maya. Some of the key highlights include
Syntax highlighing
Autcomplete (including your own code and also the entire Maya cmds api)
Smart indent and dedent
Bracket matching
but the feature that will sell most Maya veterans instantly is the fact you can execute lines or scripts without the familiar Select > Enter that has deleted countless lines of your test code down the ages.
That one feature alone would probably be worth the price for most people who do a lot of scripting. But the whole package is thoughtfully put together in a way that clearly says the author wrote a tool for himself – and that he shares a lot of the frustrations that have driven you and I bonkers for the last 18 years of Maya history. A great example is the addition of quick help for Maya commands: if you (like me) can never remember the difference between the flags for listConnections and those for listRelatives, Charcaol allows you to pop up a quick in-window help view or to open the relevant documentation in a browser: a welcome alternative to the maddening ritual of entering “cmds.whatever” into Chrome and being directed to the Maya 2011 Japanese docs by the mysterious imps of the internet.
In general, Charcoal shows a lot of attention to the nuances of scripting work. For example, it allows you to quickly toggle layouts: Charcoal allows you to quickly flip back and forth between the usual split view and a full panel of either script or history, so you don’t have to give up coding space to see your printouts or vice-versa. Likewise, you can set font sizes and color schemes for the scripting panel and the history panel separately – a big help if you want to save space on your printouts or if (like me) your eyes are going and you need to bump up the font size for coding. The history panel even supports highlighting – separating errors and warnings clearly from regular printouts, for example. All in all it’s a collection of small touches that offers a much-appreciated sense that the program has your back and that the author has wrestled with many of the same irritations you’ve had.
The product also ventures into territory that’s useally associated with full-fledged IDEs. It particular it offers an “outline view” which displays the classes and functions in the current scope - a big help for navigating around in a longish file, as well as a handy way to remember what you’re working with. There’s also a “project view” which displays all of the scripts in a project folder tree – more or less the same as the project views in Sublime Text or Atom (two other scripter-friendly editors you should check out if you’ve never seen them.)
These IDE features will be very helpful for folks who’ve been soldiering on with nothing but the Maya script editor and Notepad. If you’re already using an IDE like PyCharm, Wing, or PTVS they may not be quite enough to wean you out of your fancy environment – particularly if you’r gotten used to using a real debugger instead of littering your code with print statements. Charcoal’s project features are functional but – given the nature of the task and the audience – are not as fancy as the equivalent features in big budget development environments. If you really prize the ablity to inifitely noodle on color themes, or a built-in style guide, you may find yourself wandering back to one of the bigger packages. That’s not a knock on Charcoal, though – it’s just a reminder that it’s a specialist tool for Maya users and not a general-purpose project management powerhouse.
For myself, I plan on sticking with PyCharm for long coding sessions (btw, PyCharm fans, you’ll be incredibly pleased to hear that Charcoal allows cut and paste directly from PyCharm, unlike Maya’s wonky script editor. Whoop-de-doo!) However Charcoal more than justifies itself as a replacement for the vanilla script editor with a lot of juicy productivity features. I’ve already gotten a lot of productivity bounce by using MayaCharm to bypass the Maya script editor whenever possible – but I still spend quite a lot of time in the clunky old Maya pane nonetheless. I’ve got high hopes that Charcoal will save precious brain power for real problems and allow me to focus more on doing my job and less on frantically hitting Undo after my last attempt to execute a line accidentally erased an hour’s work.
Charcoal offers a free, non-saving demo; an individual license is $49 US (site licenses are available but you’ll have to negotiate them with the author).
Here some_function must be using one of Python’s handiest features, the ability to return lists or tuples of different types in a single function. Python’s ability to return ‘whatever’ - a list, a tuple, or a single object – makes it easy to assemble a stream of data in one place and consume it in others wihout worrying about type declarations or creating a custom class to hold the results. Trying to create a similarly flexible system in, say, C# involves a lot of type-mongering. So it’s nice.
At least, it’s nice at first. Unfortunately it’s got some serious drawbacks that will become apparent after a while – outside the context of a single script or function, relying entirely on indices to keep things straight is dangerous. As so often in Pythonia, freedom and flexibility can come at the cost of chaos downstream if you’re not careful.
I have a bad feeling about this…
Everything will be hunky-dory as long as some_function continues to pack its output the same way. In this example some_function is probably doing something like:
# imagine some actual code here ...results=[]fornodeinobject_listforattribinattrib_list:settable=is_attrib_settable(node,attrib)ifsettable:new_value=dict_of_defaults[attrib]results.append([node,attrib,new_value])returnresults
Inevitably, though, something will come along that causes the order of the results to change. In a Maya example like this, for example, the likely cause would be some other user of this function finding out that the code needs to set defaults on an unusual value type. setAttr needs to be told what type of data to expect if things are unusual.
That being the case, your teammate extends some_function to output the data type needed. If you’re lucky, the results look like [node, attribute, value, type] and your existing code works fine. But if it changes to [node, attribute, type, value] your existing code will break in wierd ways. Moreover if you haven’t written a lot of comments, the person fixing the bugs will have to sit down and deduce what item[0], item[1] and item[2] were supposed to be.
This example is a perfect illustration unit tests are such a nice thing to have in Python-land: a unit test would probably catch the change in signature right away, alerting your helpful co-worker to the can of worms they have opened up by changing the output of the function. But the real moral of the story is how dangerous it is to rely on implicit knowledge of structures – like the ordering of a list – instead of on explicit instructions. When somebody fails to understand the implications of that ordering, bad things will happen. When the knowledge you need to debug the problem is hidden, things will be
even worse.
Sometimes things get complicated
Return classes strike back
In most languages the way around this is to create a class that holds the results of something like some_function. A result class provides clear, named access to what’s going on:
classSomeFuncResult(object):def__init__(self,node,attr,val):self.node=nodeself.attribute=attrself.value=val# and inside of some_function()...results.append(SomeFuncResult(object,attrib,val))...
This means the receiving code is much neater and easier to understand:
This is a better record of what you were trying to achieve in the first place, and it’s also much more survivable: as long as HelpfulCoworker01 does not actually rename the fields in the result object it can be tweaked and updated without causing problems.
For many cases this is the right way to go. However it comes with some drawbacks of its own.
First off – let’s be honest – there’s a lot of typing for something so dull. I always try to leave that out of the equation when I can - the time spent typing the code is such a tiny fraction of the time you’ll spend reading it that trying to save a few keystrokes is usually a Bad Idea (tm). However, typing 5 lines when you could just type a pair of bracket does feel like an imposition – particularly when the 5 lines are 100% boring boilerplate.
The second issue is that, being a class, SomeFuncResult is comparatively expensive: it costs a smidge more in both memory and processor time than just a list or a tuple of values. I’m ranking this behind the typing costs because most of the time that increment of cost doesn’t matter at all: if you’re dealing with a few hundred or even a few thousand of them, at a time the costs for spinning up new instances of SomeFuncResult just to hold data are going to be invisible to users. However if you are doing something more performance-intensive the costs of creating a full mutable object can be significant in large numbers. As always, it’s wiser not to try to optimize until things are working but this is still a consideration worth recalling.
The last issue is that SomeFuncResult can be changed in flight. Since it is a class, the data in a SomeFuncResult can be updated (for you CS types, it is mutable). This means some other piece of code that looks at the result object in between some_function and you might can decide to mess with the results. That can be a feature or a bug depending on how you want to code it – but since Python does not have a built-in mechanism for locking fields in an object, you’d have to put in extra work to make sure the results didn’t get changed by accident if keeping the data pristine was mission-critical. You can use the a property decorator to make a fake read only field:
Alas, our 5 lines of boilerplate have now blossomed into 16. Our quest for clarity is getting expensive.
One common way to get around the hassles – or at least, they typing costs –of custom return objects is simply to use dictionaries instead. If you use the perforce Python API you’ll be quite familiar with this strategy; instead of creating a class, you just return dictionaries with nice descriptive names
Like a custom class this increases readability and clarity; it’s also future proof since you can add more fields to the dictionary without messing with existing data.
Even better, dictionaries – unlike classes – are self-describing: in order to understand the contents of a custom result class like SomeFuncResult you’ll have to look at the source code, whereas you can see the contents of a result dictionary with a simple print statement. Dictionaries are slightly cheaper than classes (there is a good workaround to speed up classes, but it’s something you have to write and maintain). And, of course, dictionaries have minimal setup costs: they are boiler-plate free.
This doesn’t mean they are ideal for all circumstances, howerver.
The Achilles’ heel of using dictionaries is keys, which are likely to be strings. Unless you are very disciplined about using named constants for all your result dictionaries you’ll inevitably find that somebody somewhere has type attribite with an extra i instead of a u and suddenly perfectly valid, impeccably logical code is failing because nobody thought to look at the key names. Instead of typing lots of setup code once, you’ll be dribbling out square brackets and quotes till the end of time, with lots of little missteps and typos along the way. While that’s not an insurmoutable problem it’s another annoyance.
Not so scary when you know the secret!
Return of the namedtuples
Luckily there is yet another – and for most purposes better – way to return complex results — one that is both flexible and self-describing. namedtuples are part of the python standard library and they offer a clean, simple way to create lightweight objects that have named properties – like classes – but require almost no setup: you can create a new type of named tuple with a single line of code, and then use it like a lightweight (and immutable) class.
A namedtuple is just a python tuple that can also use names to access it’s own fields. For example:
fromcollectionsimportnamedtuple# create a namedtuple called 'SomeFuncRes' to hold nodes, attributes and valuesSomeFuncRes=namedtuple("SomeFuncRes","node attribute value")# make an instanceexample=SomeFuncRes('pCube1','tx',33.0)# Result: SomeFuncRes(node='pCube1', attribute='tx', value=33.0)
As you can see, namedtuples are as even easier to ‘read’ than dictionaries when printed out. However, namedtuples give you dot-access to their contents.
printexample.node# pCube1
This saves a few characters: result.node beats result['node'] – but mopre important offers with far fewer opportunities for mistyped keys or open quotes.
However, namedtuples can also use old-fashioned indexed access too:
printexample[0]# pCube1
And you can even iterate over them if you need to, since a namedtuple is in the end just a slightly fancier tuple:
foriteminexample:printitem# pCube1# tx# 30
Namedtuples are easy to instantiate: You can create them using index ordering, names, or **keyword arguments. Names tend to be better for clarity, but if you’re expanding the results of other functions like zip() indices and double-starred dictionaries can be very handy. Having all three options allows you to create them in the most appropriate way.
Unlike classes or dictionaries, namedtuples are immutable; that is, they are read-only by default. This is usually a Good Thing(tm) for a result object, since data changing in mid-flight can lead to subtle bugs that may be very hard to reproduce. Immutability also makes them cheaper: they don’t require Python to do as much setup behind then scenes when a they are created, which can be significant in large quantities. They usually take up less memory as well.
This combination of features is tough to beat in a cheapo data-only class. If for some reason you need to upgrade to a real class instead, you probably won’t even need to change the code which reads your namedtuples: Python doesn’t care if result.node is a namedtuple field or a regular object field. For all these reasons, namedtuples are a great little tool for a lot of common data-bundling jobs. No strategy fits every battle, but namedtuples are an excellent - and often overlooked! – way to manage this very common (albeit not very interesting) problem and to keep your overall toolkit cleaner, more robust and easier to maintain.
It was inevitable, after I started noodling around with terminal colors in ConEmu, that I’d waste an afternoon cooking up a way to color my Maya terminal sessions automatically.
The actual code is up on GitHub (under the usual MIT Open License - enjoy!).
As implemented, its a module you can activate simply by importing conemu. Ordinarily I don't like modules that 'do things' on import, but this one is such a special case that it seems justifiable. Importing the module will replace sys.stdout, sys.stdin, and sys.display_hook with ConEmu-specific classes that do a little color formatting to make it easier to work in mayapy. If for some reason you want to disable it, calling conemu.unset_terminal() will restore the default terminal.
Here are the main features:
Colored prompts and printouts
This helps de-emphasize the prompt, which is the least interesting but item on screen, and to emphasize command results or printouts
Unicode objects highlighted
Since all Maya objects returned by commands are printed as unicode string (like u'pCube1', the terminal highlights unicode strings in a different color to make it easy to pick out Maya objects in return values. The annoying little u is also suppressed.
Code objects highlighted
Code objects (classes, functions and so on) are highlighted separately
Comment colors
Lines beginning with a # or a / will be highlighted differently, allowing you separate out ordinary command results from warnings and infos. In this version I have not isolated the path used by cmds.warning, which makes this less useful. Does anybody out there know which pipe that uses? It appears to bypass sys.stdout.write() and sys.stderr.write()
Automatic prettyprint
If the result of a command is anything other than a string, it will be run through prettyprintso that it will be formatted in a slightly more legible manner. This is particularly handy for commands like ls or listAttr which produce a lot of results: pprint will arrange these vertically if they result would otherwise be wider than 80 characters.
Utilities
The module contains some helper classes if you want to make your own display more elaborate, or to mess with it interactively during a console session.
Terminal: screen formatting
The Terminal class makes it less cumbersome to control the display. The main use is to color or highlight text. The 16 terminal colors are available as Terminal.color[0] through Terminal.color[15], and you can highlight a piece of text like so:
print"this is "+Terminal.color[10]("colored text")
The background colors are Terminal.bg[0] through terminal.bg[5] and work the same way:
printTerminal.bg[2]("backgound text")
Terminal also has a helper for setting, coloring, and unsetting prompt strings.
Conemu: console control
The Conemu class includes some limited access to the more elaborate functions offered by ConEmu (The methods in Terminal might work in other ANSI terminals – I haven’t tried ! – but the ConEmu ones specific to ConEmu). The key methods are:
ConEmu.alert(message)
Pops up a GUI confirm dialog with ‘message’ in it.
ConEmu.set_tab(message)
Sets the name of the current ConEmu tab to ‘message’.
ConEmu.set_title(message)
Sets the name of the current ConEmu window to ‘message’.
ConEmu.progress(active, progress)
if active is True, draw a progress indicator in the window task bar at progress percent. For example ConEmu.progress(True, 50) overlays a 50% progress bar on the ConEmu task bar icon. If active is false, the progress bar is hidden. This can be handy for long running batch items
If you do a lot of tools work in maya – particularly if you’re working one something that integrates with a whole studio toolset, instead of being a one-off script – you spend a lot of time restarting. I think I know every pixel of the last five Maya splash screens by heart at this point. A good knowledge of the python reload() command can ease the pain a bit, but there are still a lot of times when you want to get in and out quickly and waiting for the GUI to spin up can be a real drag.
If this drives you nuts, mayapy - the python interpreter that comes with Maya - can be a huge time saver. There are a lot of cases where you can fire off a mayapy and run a few lines of code just to validate that things are working and you don’t need to watch as all the GUI widgets draw in. This is particularly handy if you do a lot of tools work or script development, but’s also a great environment for doing quickie batch work – opening a bunch of files to troll for out of date rigs, missing textures, and similar annoyances.
All that said, the default mayapy experience is a bit too old-school if you’re running on Windows, where the python shell runs inside the horrendous CMD prompt, the same one that makes using DOS so unpleasant. If you’re used to a nice IDE like PyCharm or a swanky text editor like Sublime, the ugly fonts, the monochrome dullness, and above all the antediluvian lack of cut and paste are pretty offputting.
However, it’s not too hard to put a much more pleasant face on mayapy and make it a really useful tool.