Showing posts with label Python. Show all posts
Showing posts with label Python. Show all posts

Friday, November 11, 2011

Auto-placing Toolbars

I fiddled around with Maya 2012's toolbar options a bit more today, and learned a few interesting things. The most interesting of all was the discovery of a way to auto-load a custom toolbar when Maya opens, and have it appear in the same position that it was in when you last closed the program. (This is all in Python, by the way; I'm not sure if it would work the exact same way in MEL.)

Saving Toolbar Positions
The first thing I had to figure out was how Maya saved toolbar placement. When I wrote the other day about fixing a co-worker's missing tool settings panel, I mentioned the discovery of Maya's startupMainWindowState settings file. I also found some flags on the windowPrefs command for saving and restoring these settings. Today I learned that when using those flags, the full absolute path to the file should not be passed. Maya only wants the name of the file; it already knows where it lives. So to save, it's as simple as this:


import maya.cmds as mc
mc.windowPref(saveMainWindowState="startupMainWindowState")

Maya also saves the toolbar settings automatically when it closes, and we'll take advantage of this as we move forward.

Restoring the Toolbar
Immediately after you have built your interface and wrapped it up in a toolbar, restoring Maya's window state settings will move the toolbar back into its original position (assuming it had an original position when the settings were last saved).  Here's a quick example:



import maya.cmds as mc
window = mc.window("myWindow")
column = mc.columnLayout(p=window)
button = mc.button(p=column, l="Click me!",
                   c="print('clicked!')")
toolbar = mc.toolbar("myToolbar", content=window, label="Clicker",
                     area="top")
mc.windowPref(restoreMainWindowState="startupMainWindowState")



Naming the toolbar is important, as this is what will allow you to re-position that toolbar automatically between sessions.  If it doesn't have a consistent name, it won't work.

Obviously the last line won't do much the first time that you run it, because there is no saved position for this toolbar.  However, move the toolbar to some other position, then close Maya, which will save the toolbar's position.  Open Maya, run the same code, and the toolbar should re-spawn in the same place where you moved it.

The next step is to make Maya auto-load your toolbar each time that it starts.

Auto-Loading the Toolbar
Maya looks for  -- and executes the contents of -- two files each time that it starts: userSetup.mel and userSetup.py.  If used, these files should be placed in Maya's "scripts" folder.  In this case, we're going to use the Python startup file to kick off our toolbar.

First, we'll need to wrap our code inside a function, and save it in a file.  Copy the following into a new file in your favorite text editor:



import maya.cmds as mc

def main():
    window = mc.window("myWindow")
    column = mc.columnLayout(p=window)
    button = mc.button(p=column, l="Click me!",
                       c="print('clicked!')")
    toolbar = mc.toolbar("myToolbar", content=window,
                         label="Clicker", area="top")
    mc.windowPref(restoreMainWindowState="startupMainWindowState")


Save this file as "myToolbar.py" into Maya's prefs/scripts folder, which is one of the places in the sys.path setting of Maya's embedded Python.

Now start a new text file, and add this to it:

import maya.cmds as mc
import myToolbar
mc.evalDeferred(myToolbar.main)

Save this file as "userSetup.py" into Maya's scripts folder.

The reason we're using "evalDeferred" is because Maya needs to wait to execute the UI code until it has a UI to work with.  If you don't defer the execution, Maya will crash before it even opens.

Close Maya and re-open it.  Your custom toolbar should automatically appear where you last placed it.


Wednesday, August 18, 2010

Maya to Qt: Preface

Before digging into the meat of the Maya-to-Qt discussion, I should probably cover some basics first.

Software Versions

Because all of my Maya/Qt work is currently happening at Reel FX (as I still don't have a current version of Maya at home for various reasons), everything I do will be based on the software versions installed at Reel FX. As of this writing, we're using PyQt 4 with Qt 4.4.2. Most of my work at this point is with Maya 2009. While we do have later versions available, and I may make occasional reference to idiosyncrasies in those versions, assume for the most part that this is all happening in Maya 2009.

Installation

One thing that I definitely won't talk about in these posts is how to install either Qt or PyQt, or how to get PyQt conversing with Maya in versions prior to 2011. I'm avoiding that hurdle largely because it was cleared for me by the folks at work, so I don't have any hands-on experience to share in that regard. If you want, head on over to Nathan Horne's blog, where he has kindly shared some packages that he put together to make the PyQt installation process a little easier. I haven't personally used those, though, so I can't offer any comments on their effectiveness.

Code Content

While the code samples that I list will be (ideally) fairly complete, I may only present partial code pieces here and there, partly because I'll be pulling some of these examples from actual stuff I'm doing at work. Those omissions are not only to address code-proprietary-ness issues, but because of some customizations that were done to unify all of our Maya/PyQt development at Reel FX. Just be aware of that in case you copy-paste any code samples that you find here. I'll leave in the essential stuff like imports, naturally, but some other parts won't be there.

I think that about covers it. In the first post (coming soon), we'll take a look at Qt's signals and slots, and then delve into meatier UI topics from there.

Monday, August 16, 2010

The latest goings-(Pyth)on

Now that animation has wrapped at Reel FX on both Open Season 3 and the three Wile E / Roadrunner shorts, I'm back to updating the animation tools. While I was able to do a little development during production, it's tough making big changes for various reasons. With things flowing a bit slower now, it's nice to finally have the time to tackle some long-overdue to-do items.

One particular programming puzzle piece that I've been half dreading, half eagerly-anticipating is Qt. For those not in the know, Qt is (in the words of its current developer, Nokia) "a cross-platform application and UI framework." With the popular PyQt module that provides an interface to this framework for Python users, its use in the development of tools and interfaces for CG software has grown immensely, just as Python's use has grown in the same field.

The TD team at Reel FX has been steadily pushing farther into Qt development over the past several months, but production demands pretty much kept me from taking a serious look at it until last week. After just a few days of play, though, I'm already a fan/convert/addict. Just as Python is so much more versatile than MEL, Qt's UI tools are WAY more versatile than the UI tools provided by Maya. My mind is reeling with so many ideas of how we can significantly upgrade our animation tools to take advantage of the versatility provided by the Qt toolkit. While I'm far from an expert at it after only a few days, I've managed to conduct several successful tests of greatly enhanced functionality for our character GUI system, and can't wait to overhaul the whole thing and put the new pieces in place.

The only hitch is that getting Maya set up to use the Qt framework takes a bit of work, at least if you're using Maya 2009 (which we still are for some recent projects). While the folks at work have taken care of that already, and we are already using Qt for a greater number of internal Maya tools, we have several tools that are also used by remote animators who are hired to help us with various projects. By relying only upon the toolset within Maya up until now, we've been able to minimize the setup hassle for remote artists. If we suddenly ask folks to also go through the necessary steps to get Maya and Qt talking, we'll likely run into some problems, and that kind of remote support is something that we're just not prepared to handle.

This means that until production switches to using the later versions of Maya that have built-in Qt support, it's going to be difficult to justify spending much time on the tools that are used by both internal and external animators, which is just a tad frustrating. However, there are plenty of internal-only things that can be upgraded, which will give me time to become more familiar with Qt's idiosyncrasies.

On a somewhat-related note, I've taken on yet another side job. This one's more informal, though, but it also involves Python. One of my friends owns a dent-repair business, and he approached me a few weeks ago asking for help with something that will perform a bunch of calculations and track certain information related to the jobs done by him and his team members. Originally he wanted to do everything in a spreadsheet, and in our initial meeting we came up with something that worked fairly well after a couple hours. However, as my friend came up with additional features that he wanted for this system, it quickly became apparent that while it's certainly possible to do the whole thing via spreadsheets, in the long run it would be too much of a headache. I've been wanting to try my hand at standalone development for a while, and this looked like the perfect opportunity. To keep deployment simple, I plan on using Tkinter for the program interface, and will pack the whole thing up using py2exe. While I've only put a few hours into it so far, my experience with Python development at work is helping immensely. I could not imagine tackling something like this in my early Python programming days.

I hope to put out some more posts in the coming days/weeks with some further Python programming thoughts, but only time will tell if it actually happens.

Thursday, March 26, 2009

Booleans, ternary operators, and max

I ran into a situation today that initially stumped me. I needed to take a list of unknown size, check each item in the list to see if it could be found in a line of text, and return a single True or False if any one item in the list matched that check. Oh, and if the list was empty, that had to be addressed appropriately as well. And I wanted to do it in the smallest way possible.

Originally I'd hacked together a short four or five line function that did the job. However, after coming back to the code for some other updates, I noticed that said function was only being called once, so I began looking for ways to nix that function and do the job more directly. Part of the challenge was that this comparison I needed to do was part of an existing if statement, and it had to remain so due to the way the rest of the code worked. In short, the comparison went like this (in pseudo-Python):

thisList = ["list", "of", "unknown", "length"]
if not ":" in line and not anythingInThisList in line:
do stuff
else:
do other stuff

The first thing I tried to figure out was a way to get a single True or False out of a list of various Boolean values. On a whim, I tried to use the built-in max function. I'd used max before in standard numeric comparisons, but never with Booleans. Not surprisingly, it worked quite well.

>>> max(True, False)
True
>>> max([False, False, True, False])
True

Getting the necessary Boolean list to pass to max was a no-brainer thanks to Python's list comprehensions. Here's an example that quickly shows if a given word contains any vowels:

>>> vowels = ["a", "e", "i", "o", "u"]
>>> max([v in "sadness" for v in vowels])
True
>>> max([v in "shhhh" for v in vowels])
False

This process alone solved most of my problem. However, I couldn't always be sure that my list of things to look for would contain anything. The variable containing the list was set by an argument in a function definition, and that argument defaulted to None if no other data came through. That meant that I had to somehow force the output of the max comparison to False if the variable was None, or let it do its thing if not.

That got my thoughts going toward ternary operators. For those who don't know, many programming languages offer a condensed one-line shortcut for the standard if-else comparison, called a ternary operator. In short, it turns this:

if statement:
doThis
else:
doThat

into this:

statement ? doThis : doThat

Not long ago I dug around to see if Python supported anything like this, and sure enough it does (as of one of the more recent versions, but I forget which one), although it isn't in the standard documentation from what I could see. At any rate, the above example would look like this using Python's approach to the ternary operator:

doThis if statement else doThat

With that applied to my max comparison, the end result looked something like this (and I hope this all ends up on a single line in the blog post):

# "line" is the line of text
# "y" is either None or a list of items to find in "line"

max([x in line for x in y] if y is not None else [False])

The reason that False needs to be encapsulated inside a list is because max needs a list of items through which to iterate, even if that list only contains a single item.

It's also possible to generate the Boolean list using the map function and a lambda, and the code isn't that much longer:

max(map(lambda x:x in line, y) if y is not None else [False])

Wednesday, February 25, 2009

Maya and Python lambdas, part 3

A thought occurred to me this morning before coming into work. I was still thinking that there had to be some way of further improving my use of lambdas in Maya GUI creation. Then it hit me: assign the data from the loop as a default value for one of the lambda's arguments!

import maya.cmds as mc

def showStuff (stuff):
print "You've given me %s!" % stuff

stuffList = ["an apple", "a pear", "a pickle"]
mc.window("Stuff to Give", w=300, h=200)
cl = columnLayout()
for item in stuffList:
mc.button(l=item, c=lambda x, i=item:showStuff(i))

Look, ma...no lambda factory! :) Here's my guess as to why this works when it doesn't work to put the variable "inside" the lambda...

By stuffing a variable into the function/method call inside the lambda function, it appears that the value of the variable isn't retrieved until the lambda function is executed. For a variable defined by a loop, this means that the variable value is the same as it was during the last iteration through the loop.

However, by taking a variable and assigning it as the default for one of the lambda's arguments, the value assigned to that variable is retrieved and stored in the lambda function definition. When called by Maya when the appropriate GUI element is used, the lambda function already has that value, and knows to pass it as the default for the appropriate argument. Because no other data is passed to replace it, it can be used reliably inside the function as part of the real function call we want to make.

The only thing to keep in mind when using this technique is the extra data that Maya passes on its own, which is caught (and promptly ignored) in the example above by the x argument.

Clean and simple. Me likey.

Tuesday, February 24, 2009

More Lambda and GUI fun

Just tripped over an interesting problem with the lambda stuff I shared in the last post. It's just peachy if you're passing a literal value:

# other code omitted for brevity
mc.button(l="click me", c=lambda x:colorMe("Purple"))

However, if you're creating a collection of controls using a loop, it no worky correctly:

names = ["me", "you", "him"]
for name in names:
mc.button(l=name, c=lambda x:nameMe(name)

In this example, no matter which button you click, it will pass "him".

I tried a number of ways to get around this, and wasn't successful until I revisited the page that flipped the lambda light switch for me. The second example on that page shows how to create a "lambda factory" of sorts. In the context of the loop situation, the factory serves to isolate the creation of the lambda function from the loop. While the factory function was a standalone item in the example on that page, it would be convenient to nest said factory inside the same function/method that contains the loop. Here's a more fleshed-out example:

import maya.cmds as mc

def addButtons (names):
# here's the factory function
def factory (nm):
return lambda x:showName(nm)

# and here's our loop
for name in names:
mc.button(l=name, c=factory(name))

It's a bit more extra code than I'd hoped for, but it's only needed when using the lambda technique inside a loop, and still allows me to keep the target functions/methods clean by avoiding nesting.

Monday, February 23, 2009

Python Lambdas and Maya GUIs

I love epiphanies. :)

For some reason, I haven't been able to figure out "lambda" functions in Python. Granted, I've not put a great deal of time into them. It's just that whenever I would see them in someone else's code, I couldn't immediately figure out what they were doing, so I'd just add another mark next to the "Need to research lambdas" entry in my mental to-do list and move along. To make a long story short, I ran across a page today that flipped the light switch on lambdas. But that's not the epiphany of which I speak. The epiphany hit when I began trying to figure out what (if anything) I could do with those lovely little lambdas in my Python programming at work.

Most of my development work involves the creation of tools with some kind of graphical user interface (GUI). When assigning a command to a Maya GUI element in Python, it expects either a string that contains some Python code to execute, or a pointer to a function or method that will be called. In the vast majority of situations, I'll use the latter option. If I don't need to pass any data to the target function, there's no problem, and Maya gets the function pointer as expected:

import maya.cmds as mc

def blah(*args):
# "*args" is required because even though the
# command doesn't pass any data, Maya passes
# some anyway. Go figure...
print "You touched me!"

mc.window(w=500, h=500)
mc.columnLayout()
mc.button(l="Touch!", c=blah)
mc.showWindow()

However, in most of the GUIs that I create, some control will need to pass specific data to the function that it calls. The problem is that once you include parentheses to pass data to the function, Maya is no longer getting a function pointer. It's getting the value (if any) returned by the called function, or None if the function doesn't return anything.

Up until now, I've been using nested functions to get around this problem. By defining and returning a "dummy" function inside the main function that is called by the GUI element, Maya will get the function pointer it wants, and I can pass in any data that I please:

import maya.cmds as mc

def blah(value):
def b(*args):
# "*args" is still required because this is the
# function that Maya will ultimately call when
# the button is pushed
print "I was given:", value
return b

mc.window(w=500, h=500)
mc.columnLayout()
mc.button(l="Touch!", c=blah(10))
mc.showWindow()

This process has been working fine, but in the back of my mind, I kept hoping to find a more elegant solution.

Enter my new friend: the lambda!

After some experimentation, I learned two very helpful things about lambdas. The first is that the expression evaluated by a lambda doesn't necessarily have to have any connection to the data it is passed. For example, a "normal" lambda definition might look something like this:

x = lambda y: y * 2

In this case, calling x(5) will return 10 (the 5 gets passed to y, when is then evaluated through the expression y*2 to yield 10, which is then returned). However, the expression can be changed to return something that has nothing to do with the value passed in through y, like so:

x = lambda y: 15

In this case, no matter what value you pass, 15 will always be returned.

Based on this example alone, I can already begin to use lambdas to simplify my example code above. (It's simpler on the function definition side of things because we get rid of the nesting issue, but some might see the syntax of assigning the desired function call to the GUI element a tad more confusing.)

import maya.cmds as mc

def blah(value):
print "I was given:", value

mc.window(w=500, h=500)
mc.columnLayout()
mc.button(l="Touch!", c=lambda x:blah(10))
mc.showWindow()

What happens is that the mystery-data passed by Maya gets assigned to x in the lambda definition, but we don't need to use it. All we need is to call our function with the desired value.

For some GUI elements, though, the data passed by Maya is actually useful. Take an intSlider, for example:
import maya.cmds as mc

def blah(value):
print "The slider value is", value

mc.window(w=500, h=500)
mc.columnLayout()
mc.intSlider(dragCommand=lambda x:blah(int(x)))
mc.showWindow()

In this situation, the data passed by Maya when dragging the slider is the slider's value. However, it's passed as a Unicode string, so I just converted it to an integer before passing it to the function. This means I don't have to query the slider, as the data I need has already been passed.

Some GUI operations, like dragging and dropping, pass more than one argument to the target function. No matter...just provide the requisite number of arguments in the lambda definition (i.e. lambda w,x,y,z: ....) and pass them along to the target function as desired. Or, as in one particular case where I wanted to substitute my own data in place of what the drag operation passed, you can take advantage of the other nifty thing I learned through my experiments: a lambda can accept arbitrary argument lists, just like normal functions.

# other GUI code here
mc.button(l="blah, dragCallback=lambda *x:boo(myData))


That's all for now. Happy Python GUI building!

Monday, October 06, 2008

Python decorators: getting a little closer...

After further searching, I finally ran across a very well-written article that attempts to explain Python decorators from a very basic level. While it hasn't brought to mind any situations in which I would want to use decorators in my code, I at least have a handle on what exactly they are and how they work.

To clarify my earlier post, one of main things that (at first) didn't click for me with regard to decorators was how exactly they allowed the programmer to effectively modify a function after it was written. Sure, I'd learned that you could assign a function to a variable and pass it around like any other piece of data, but even in that context I saw the function itself as essentially static. If I passed a function as an argument, I only expected the target function to call the function I passed as-is. I never thought about the possibility that a target function could actually do something else with/to the function I had passed.

As with other things I've unearthed in the process of learning Python, I have a hunch that this would've been easier to learn if I had a more formal background in computer science, and that's probably the biggest frustration that still crops up as I dig through Python documentation. Bits of programming terminology are often casually thrown around with the assumption that the average reader knows what they mean. One example is the term "first-class objects" that can be found at the head of the article linked above. I've seen that term used in several places, but even though the sentence immediately following the phrase in the above article is related to the phrase's definition, I still had no idea what it meant until I dug it up via a Google search. The way it was phrased in that article, it felt like the status of Python functions as first-class objects was a separate concept from that which immediately followed it, so I was left thinking, "Okay...so what are first-class objects?"

That's probably not the best example of my frustration, though. I guess the point is that it feels like there's a gap somewhere. At one end there's good introductory material, such as the tutorial by Guido von Rossum that covers a lot of ground-level topics. At the other end there is very nice reference material that covers the individual modules in the standard library, the data types that the language offers, etc. However, between Guido's tutorial and the other reference material there's a bit of a void, because some of the reference material makes reference to programming concepts that aren't explained in Guido's tut. I'm not necessarily implying that they should be covered by Guido's tutorial, but it's a gap all the same, and one that many in the Python community seem to ignore. Perhaps that is an inaccurate observation, and I'd be more than happy to be shown evidence to the contrary. However, from my observations, many experienced Python programmers seem to assume that you're either a newbie learning the basics, or you're a fellow pro. The folks in the middle who aren't totally new but aren't full-on professional programmers seem to get overlooked.

The most frustrating part is that I feel like I'm square in that gap. Yeah, I've been scripting/programming since I was about 12, but sadly that doesn't necessarily mean that I have a complete understanding of modern programming terminology and concepts. I've taken very few formal programming courses, and those few took place a long time ago. Everything else I know has been the result of self-study and on-the-job experience. However, the farther I delve into this stuff, the more I feel like self-study (at least what I'm currently doing) isn't cutting it.

So here comes the million-dollar question: do I stick with self-study and see how far it gets me, or do I go back to school? I've been around enough self-taught animators to know that it's possible to get to the very top of that game without formal instruction, but I don't know if the same holds true for the world of programming.

Wednesday, September 17, 2008

Still can't figure out decorators

After a couple months (roughly) of Python programming, I'm very happy with how far I've come and what I'm able to accomplish. When all this started my biggest struggle was the basic object-oriented programming mindset. After further study and a good bit of trial and error, though, I feel I've got a pretty good handle on it. I'm not a whiz by any stretch, but I'm a lot more comfortable with it now than I was when this all began.

That leads me to the next programming concept I'd like to grok: decorators.

I just stumbled across a blog post in which the author claims that Python's decorators are "radically simple." I'm sorry, but I've read a good number of explanations behind decorators -- including the Wikipedia article on the subject, which includes a pseudo-example in Python -- and this "radically simple" outline (i.e. the slides that Mr. Diederich used in his PyCon UK talk) hasn't made the concept any clearer for me.

Does anyone know where I can find a truly clear description of Python decorators? Perhaps this is something that could be more easily understood if I had a solid computer science background instead of an animator-who-fell-in-love-with-programming-on-the-side background.

Yarg....

Friday, September 12, 2008

Python and list copies

While working on a tool at Reel FX, I couldn't figure out why the data lists across several different class instances were all returning the same information. Normally working with lists in Python is tons-o'-fun (especially when compared to the gymnastics required to do similar operations in MEL), but that fun can come to a screeching halt if you forget one little important detail: copying a list doesn't necessarily copy the list.

For example, say there's a list assigned as follows:
   data = ["apples","oranges","pears"]
Some time later you want stuff to be a copy of data:
   stuff = data
If you've come to Python with some previous programming/scripting experience, you might assume (as I've done more than once) that this would do the trick. However, that just tells stuff to reference the list assigned to data. If you end up changing data later on, and then look at stuff, you'll notice that it shows the same changes. That's because they're both pointing to the same list in memory.

So how does one get around this little annoyance? Pretty simply, actually:
   stuff = data[:]
That bit on the end tells Python to make stuff equal to the entire contents of data, instead of making it just another pointer. To be more accurate, it duplicates the list to which data is pointing, and then points stuff to that new list.

Dictionaries also exhibit this same point-instead-of-copy behavior, but the syntax is different if you want to make a true copy:
   newdict = olddict.copy()
(Pardon the crazy formatting. I've never tried to insert code blocks into a blog post, and getting it to look decent with the controls available in the Blogger editor is driving me nuts. Methinks it's time to consider an alternative blogging system...)

Thursday, August 21, 2008

Here there be Pythons!

I recall first hearing about Python when it was added many years ago to Blender, a cool little 3D app that I used for a stretch near the end of my days at the Art Institute. However, its inclusion in Blender was largely meant (from what I can remember, anyway) as a means to assist in the development of interactive 3D applications...namely, games. I wasn't interested in doing games, so I ignored it. I discovered Animation:Master not long before graduation, so Blender went bye-bye, and so did any thoughts of Python.

The next time I recall hearing about Python was some time in the last year or so, when I heard that we use it at Reel FX. It's becoming very popular to embed Python support into 2D and 3D production software, and number of the tools that we use are apparently making the shift to Python from their own proprietary scripting languages. I noticed its inclusion in recent versions of Maya, but didn't dig into it right away for some unknown reason. I think that part of my hesitancy was because I heard that Python was an object-oriented language, and the few times I had tried to wrap my head around object-oriented programming, something just wasn't clicking.

One of the things that was discussed in my annual review was broadening my knowledge base in the area of programming/scripting languages. It was recommended that I start with Python, so after we wrapped animation on Open Season 2 and things settled down a bit, I started digging into it. I've only been working with it for a short stretch -- less than a month, I think -- but I feel it's pretty safe to say that I've become a Python junkie.

I just can't get enough of it! I don't think I've ever found a language that was so powerful, and yet so easy to learn. While I spent a good bit of time reading about it before I sat down to start actually using it, I found that I was able to jump into real, usable programs very quickly because the core syntax is so intuitive and easy to learn, and because there are so many things you can do with the collection of modules that come packaged with it. After using it for only a couple days, I was able to start writing some fairly in-depth tools for work, and in some cases it's the kind of stuff that would be literally impossible to do with MEL (the Maya Embedded Language). The majority of what I've done with it so far has been organized in a very "structured" way, similar to how I would build a MEL script. However, I'm starting to explore classes and objects as I work on porting our existing MEL-based pose library tool into a Python-based animation library tool, and it's coming along really well.

I certainly wouldn't call myself a "Pythonista" at this point, but I'm very pleased with what I've been able to do with it so far, and I'm super excited about all the possibilities that it opens up. In addition to using it at work, I plan on using Python for a number of personal projects. One idea I'm toying with is rewriting some (or possibly all) of my tweenMachine tool in Python, which I hope would speed up some of its operation, and also would make it easier to add some of the new features that I've had in the back of my mind for a while. I also plan on using Python for a personal web project that was originally going to be driven by PHP, and might convert some existing PHP-based web stuff to Python as well.

Yeah, I now...I can see some folks making the hammer/nail comparison when I talk about it like that (i.e. when a guy gets a new hammer, all the world becomes a nail). However, Python isn't just a hammer. It's an entire toolbox, and that's what I think I like about it the most. As the Python community likes to say, it comes with "batteries included," and I'm eager to put those batteries to use!

P.S. Yes, this is one of the reasons why I haven't yet posted a link for purchasing the last few Animate a Face sets. I do hope to get that up soon, though, and I appreciate those who have written so far to express interest in it. Watch for an announcement here in the hopefully-not-too-distant future! :)