Decorating your Python code

October 10th, 2008 by Patrick Boucher - Viewed 18316 times -

I have touched on Python decorators in the past in this article (although that decorator was a bit convoluted). Decorators can be extremely useful in many situations and here are a few that I propose may help streamline script development.

Note: For those of you who would like a bit of background information on decorators you can check out this page.

Keeping things quiet

Let’s look at the code and we’ll go for a few explanations afterward.

1
2
3
4
5
6
7
8
9
10
11
def suspendXSILogging(func):
    def closure(*args, **kwargs):
        logPrefs = getXSILoggingPrefs()
        try:
            ret = func(*args, **kwargs)
        except Exception, e:
            setXSILoggingPrefs(logPrefs)
            raise
        setXSILoggingPrefs(logPrefs)
        return ret
    return closure

The first function suspendXSILogging can be used as a decorator and will disable all logging features of XSI. This has the advantage of making large scripts that may be command dependent a bit quicker.

You could always implement a plugin with a custom command but sometimes a script is simpler. Even in the command context this decorator could be useful if you wanted part of the command to log to the script editor while keeping other parts silent.

Another advantage of this decorator is that it puts your original function in a large trap that, if an error should occur, will make sure that logging is reset to the users original preferences.

Time for decorating

1
2
3
4
5
6
7
8
9
10
11
12
13
def timeExecution(func):
    def closure(*args, **kwargs):
        startTime = time.time()
        try:
            ret = func(*args, **kwargs)
        except Exception, e:
            delta = time.time() - startTime
            log.error('Failed in %f seconds' % delta)
            raise
        delta = time.time() - startTime
        log.info('Finished in %f seconds' % delta)
        return ret
    return closure

This second function, timeExecution, can decorate almost any function and will print to the script editor the amount of time the decorated function took to execute. This can be used as a debugging or performance tracking tool or as a way to provide feedback to your users. Again, a trap is implemented that will log execution time even if an error occurs.

Please not that this decorator uses a logging system described here. If you would rather use simple logging you should replace log.info( instances by log(, xsi.LogMessage( or Application.LogMessage( depending on your scripting habits.

Usage example

1
2
3
4
5
@timeExecution
@suspendXSILogging
def main():
    # Do some fun useful stuff - well... more useful than this!
    xsi.CreatePrim("Cube", "MeshSurface", "", "")

You can chain decorators without hesitation like in this example, one thing to note in this particular case is that you’ll want timeExecution higher in the decorator chain than suspendXSILogging otherwise the time information will never make it to the script editor. Duh!

Support code

Before I sign off on this latest article, here are two functions that, if useful on their own, are essential to the functioning of the suspendXSILogging decorator.

1
2
3
4
5
6
7
8
9
10
11
def getXSILoggingPrefs():
    prefs = xsi.Preferences
    vals = {}
    for n in ['scripting.cmdlog', 'scripting.msglog', 'scripting.msglogverbose']:
        vals[n] = prefs.GetPreferenceValue(n)
        prefs.SetPreferenceValue(n, False)
    return vals
 
def setXSILoggingPrefs(vals):
    for key, val in vals.iteritems():
        xsi.Preferences.SetPreferenceValue(key, val)

Cheers!

Logging and being proactive

October 5th, 2008 by Patrick Boucher - Viewed 12788 times -

You write tools for artists under a deadline (yours and theirs). You live in a production oriented world. Unit testing, beta testing and anticipating can only go so far, and that is only if you have time to properly test. You have to accept that eventually, your code will break. How it breaks and how you react to such a break becomes as important as your capacity to create the tool in the first place.

You’ve probably seen the following idiom used many times on this website and in other places that do XSI scripting in Python:

1
2
3
from win32com.client import constants as c
xsi = Application
log = xsi.LogMessage

It is a shortcut that allows you to use log as if it was the native LogMessage call:

1
log('This is an info message.', c.siInfo)

Here is a way to extend this by using the standard logging module in a way that can help you be more proactive. I suggest you get familiar with the logging module from the standard Python docs, it’s bound to eventually be helpful.

Logging in XSI

Here is a construct I’ve started using lately that I am starting to enjoy, its advantages are:

  • Concise and self documenting
  • It has a trap to catch any unexpected error and log it
  • Logs both to the script editor and to file
  • Easily extensible to other logging mechanisms (email, event viewer)
  • You can pass non string messages and it will convert to string for you (Yay!)
  • Easily useable in scripts and other modules
1
2
3
4
5
6
7
8
9
10
11
12
13
from hookolo.xsi import *
 
def main():
	log.comment('comment')
	log.debug('debug')
	log.info('info')
	log.warning('warning')
	log.error('error')
	log.fatal('fatal')
	log.critical('critical')
	raise Exception('Totally unexpected exception!')
 
run('demoScript', main)

The code above would produce the following output in XSI’s script editor.

# comment
# VERBOSE : debug
# INFO : info
# WARNING : warning
# ERROR : error
# ERROR : fatal
# FATAL : critical
# ERROR : Trap reached in demoScript
# Traceback (most recent call last):
#   File "C:\hookolo\libs\hookolo\xsi\__init__.py", line 18, in run
#     func(*args, **kwargs)
#   File "<script Block >", line 11, in main
# Exception: Totally unexpected exception!
# ERROR : Traceback (most recent call last):
#   File "<script Block >", line 13, in <module>
#     run('demoScript', main)
#   File "C:\hookolo\libs\hookolo\xsi\__init__.py", line 21, in run
#     raise StopScriptError('Check the logs!')
# StopScriptError: Check the logs!
#  - [line 13]

The calls to log.comment, log.debug, log.info, log.warning, log.error and log.critical are all self explanatory as they are all equivalent to LogMessage calls with the appropriate severity argument. The call log.fatal is an addition of my own who’s severity is equivalent or just a tiny bit lower than critical. Fatal errors will not pop a dialog box.

In the setup I have here, both fatal and critical, will be logged to a file in the users’ XSI_HOME directory that looks like follows.

2008-10-04 23:46:13,720 - hookolo.xsi - FATAL - fatal
2008-10-04 23:46:13,720 - hookolo.xsi - CRITICAL - critical
2008-10-04 23:46:13,720 - hookolo.xsi - FATAL - Trap reached in demoScript
Traceback (most recent call last):
  File "C:\hookolo\libs\hookolo\xsi\__init__.py", line 18, in run
    func(*args, **kwargs)
  File "<script Block >", line 11, in main
Exception: Totally unexpected exception!

By having a file like this, I don’t have to wade through XSI’s scripting log as I have a file that only includes important script errors. I also don’t have to worry about a user coming to me and saying: “Your script exploded.” Followed by the inevitable: “No, I don’t remember the error and I don’t have it in my scripting window anymore.” Now I can just open up this file from their XSI_HOME directory and look for myself.

The usage of a main() function and a run() function allows to build a trap for any unexpected errors that might occur and allow for logging. By putting the runner in a library we can benefit from it with very little hassle in even the tiniest of scripts.

Pushing the envelope

A system such as this one could even easily be extended to allow for sending of fatal and critical errors via email. You would know that a script failed even before the artist had walked the corridor to your office to tell you about the failure. This extension wouldn’t even be that hard as the logging module includes an SMTPHandler for just this purpose.

Support code

Here is the library that makes this all possible.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import logging
import os
import sys
import types
 
from win32com.client import constants as c
from win32com.client import Dispatch
 
__all__ = ['xsi', 'log', 'c', 'run', 'XSIError']
 
COMPANY_NAME = 'Hookolo'
COMPANY_PREFIX = 'hookolo'
 
xsi = Dispatch('XSI.Application').Application
 
def run(scriptName, func, *args, **kwargs):
	try:
		func(*args, **kwargs)
	except:
		log.log(45, 'Trap reached in %s' % scriptName, exc_info=1)
		raise StopScriptError('Check the logs!')
 
class XSILogger(logging.Logger):
	def fatal(self, msg, *args, **kwargs):
		self.log(45, msg, *args, **kwargs)
 
	def comment(self, msg, *args, **kwargs):
		self.log(5, msg, *args, **kwargs)
 
class XSIHandler(logging.Handler):
	def emit(self, record):
		if record.levelno == logging.CRITICAL:
			xsiLvl = c.siFatal
		elif record.levelno in [logging.ERROR, 45]:
			xsiLvl = c.siError
		elif record.levelno == logging.WARNING:
			xsiLvl = c.siWarning
		elif record.levelno == logging.INFO:
			xsiLvl = c.siInfo
		elif record.levelno == logging.DEBUG:
			xsiLvl = c.siVerbose
		else:
			xsiLvl = c.siComment
 
		if isinstance(record.msg, types.StringTypes):
			msg = self.format(record)
		else:
			record.msg = str(record.msg)
			msg = self.format(record)
 
		xsi.LogMessage(msg, xsiLvl)
 
def getLogger(name=None):
	if name is None:
		return logging.getLogger(COMPANY_PREFIX + '.xsi')
	else:
		return logging.getLogger(COMPANY_PREFIX + '.xsi.' + name)
 
if not hasattr(sys, 'XSI_LOGGING_CONFIGURED'):
	logging.setLoggerClass(XSILogger)
	logging.addLevelName(45, 'FATAL')
	logging.addLevelName(5, 'COMMENT')
	log = logging.getLogger()
	xsiHandler = XSIHandler()
	xsiHandler.setLevel(0)
	fileHandler = logging.FileHandler(os.path.join(os.environ['XSI_USERHOME'], 'xsiScriptLog.txt'))
	fileHandler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
	fileHandler.setLevel(45)
	log.addHandler(xsiHandler)
	log.addHandler(fileHandler)
	log.setLevel(0)
	sys.XSI_LOGGING_CONFIGURED = True
 
log = getLogger()
 
class XSIError(Exception):
	pass
 
class StopScriptError(XSIError):
	pass

Gerstner Waves 102

October 1st, 2008 by Patrick Boucher - Viewed 11490 times -

During the Gerstner Wave 101 video I alluded to stacking multiple waves together to create more complex surfaces. The idea is that for each successive wave or octave that is added onto the effect, the number of waves be greater, the amplitude be smaller and the speed be slower.

Note: To see your full effect, make sure when you stack multiple waves onto each other, that the mute and solo checkboxes be clear and that the “last wave” checkbox be only active on the Gerstner Wave compound plugged lowest into the terminal node or your ICE tree.

embedded by Embedded Video

vimeo - Direct link to

If you wish to purchase this set of wave tools, you can go here.

Have fun!

Gerstner Waves 101

September 30th, 2008 by Patrick Boucher - Viewed 15698 times -

With the set of Gerstner Waves available for purchase it was obvious that there should be some usage videos posted here so here is the first one describing how to install it and the basic parameters. Of course the compound installation methodology applies to any compound you might download off the Softimage Community Site or from anywhere else.

embedded by Embedded Video

vimeo - Direct link to

Gerstner Waves for sale

September 30th, 2008 by Patrick Boucher - Viewed 4847 times -

After some hard work and some great feedback from a few people I’ve decided to put my Gerstner wave compounds up for sale.

At their current price of 40$ they’re a steal, as far as I’m concerned.

If you want to get your mitts on your own copy, check out the purchase page where you can securely buy via credit card or PayPal.

For those of you who don’t care about waves but would still like to show your appreciation for XSIBlog you can still donate.

Cheers,
Patrick