Python decorators


Patrick Boucher recently posted a SetValue decorator for Softimage that can temporarily change any value for the duration of a method call and restore it afterward.

So, what’s a decorator?
After glancing at the SetValue decorator code and taking a quick look at the docs at python.org, I needed a simple example to help me wrap my head around the syntax (which turned it to be simpler than what I was expecting).

So, here’s a simple example of a decorator.
func_timer is the decorator function, and all it does is time how long a function takes.

import time

# Define the decorator function
def func_timer(func):
	def wrapper(*arg):
		t = time.clock()
		res = func(*arg)
		print func.func_name, time.clock()-t
		return res
	return wrapper

# Wrap myFunction with the decorator
@func_timer
def myFunction(n):
	for i in range(n):
		Application.CreatePrim("Cube", "MeshSurface", "", "")

# Call myFunction
myFunction(3)


# Do the equivalent, but without using a decorator:
def myFunction1(n):
	for i in range(n):
		Application.CreatePrim("Cube", "MeshSurface", "", "")

myFunction1 = func_timer( myFunction1 )(40)

I found this Python Decorators Don’t Have to be (that) Scary article helpful.

And from the python.org glossary, I learned that:

a decorator is “merely synatic sugar” that is a “function returning another function, usually applied as a function transformation using the @wrapper syntax.”

From the Python language reference, I learned that:

A function definition may be wrapped by one or more decorator expressions. Decorator expressions are evaluated when the function is defined, in the scope that contains the function definition. The result must be a callable, which is invoked with the function object as the only argument. The returned value is bound to the function name instead of the function object. Multiple decorators are applied in nested fashion. For example, the following code:

@f1(arg)
@f2
def func(): pass

is equivalent to:

def func(): pass
func = f1(arg)(f2(func))

By the next day, it all made sense 😉

Selecting rows in a GridWidget


Here’s a basic Python example that shows how to:

  • Create a dynamic, on-the-fly custom property
  • Add a grid to the PPG
  • Set the PPG Logic in Python
  • Select rows in a GridWidget
import win32com.client
from win32com.client import constants

oProp = Application.ActiveSceneRoot.AddProperty( "CustomProperty", False, "GridWidgetDemo" )
oProp.AddParameter2("Row",constants.siInt4,1,1,20,0,100,constants.siClassifUnknown,constants.siPersistable + constants.siKeyable)
			
oParameter = oProp.AddGridParameter( "DemoGrid" )
oGridData = oParameter.Value
oGridData.ColumnCount = 3
oGridData.RowCount = 20

for i in range(0,oGridData.RowCount):
	for j in range(0,oGridData.ColumnCount):
		oGridData.SetCell( j, i, str(i) + "." + str(j) )

oPPGLayout = oProp.PPGLayout
oGridPPGItem = oPPGLayout.AddItem( "DemoGrid" )

oGridPPGItem.SetAttribute( constants.siUINoLabel, True )
oGridPPGItem.SetAttribute( constants.siUIGridSelectionMode, constants.siSelectionHeader )
oGridPPGItem.SetAttribute( constants.siUIGridColumnWidths, "25:100:75:100" )
oGridPPGItem.SetAttribute( constants.siUIGridReadOnlyColumns, "1" )

oPPGLayout.AddRow() ;
oPPGLayout.AddItem( "Row" )
oPPGLayout.AddButton( "SelectRow", "Select Row" )
oPPGLayout.EndRow()



oPPGLayout.Language = "Python"
oPPGLayout.Logic = '''
def SelectRow_OnClicked():
	Application.LogMessage( "Select Row" )
	oGridData = PPG.DemoGrid.Value
	oGridWidget = oGridData.GridWidget
	oGridWidget.ClearSelection()
	oGridWidget.AddToSelection( -1, PPG.Row.Value-1 )
'''


Application.InspectObj( oProp )

Python: Using Plugin.UserData to pass data to PPG callbacks


UPDATE: Please see the comment from Patrick for a tip. Thanks!

You can use Plugin.UserData to pass data into a plugin.

For example, from outside the plugin, you can store a list in the UserData:

p = Application.Plugins("MyTestPropertyPlugin")
p.UserData = ['a', 'b', 'mpilgrim', 'z', 'example']

In the plugin callbacks, you would access the UserData like this:

def MyTestProperty_Test_OnClicked( ):

    p = Application.Plugins("MyTestPropertyPlugin")

    if p.UserData == None:
        Application.LogMessage( "UserData is empty" )
    else:
        Application.LogMessage( p.UserData )
        Application.LogMessage( p.UserData[2] )

This will work with lists, but not with dictionaries.
Softimage cannot convert Python dictionaries into a COM Variant type. If you try to store a dict in User Data:

x = Application.Plugins("MyTestPropertyPlugin")
x.UserData = {"author":"blairs", "name":"testplugin"}

You’ll get this error:

# TypeError: Objects of type 'dict' can not be converted to a COM VARIANT

I’m afraid there’s no way around that error for Python dictionaries.
This also prevents you from saving dictionaries with SetGlobal or SetGlobalObject.

Updating a combo box from an OnClicked callback


To update the contents of a combo box from a button OnClicked callback, you use the PPGItem.UIItems property.

Here’s a simple example that shows how:

import win32com.client
from win32com.client import constants

null = None
false = 0
true = 1

def XSILoadPlugin( in_reg ):
    in_reg.Author = "blairs"
    in_reg.Name = "ComboTestPlugin"
    in_reg.Major = 1
    in_reg.Minor = 0

    in_reg.RegisterProperty("ComboTest")

    return true

def XSIUnloadPlugin( in_reg ):
    strPluginName = in_reg.Name
    return true

def ComboTest_Define( in_ctxt ):
    oCustomProperty = in_ctxt.Source
    oCustomProperty.AddParameter2("List",constants.siInt4,0,0,100,0,100,constants.siClassifUnknown,constants.siPersistable + constants.siKeyable)
    return true

def ComboTest_DefineLayout( in_ctxt ):
    oLayout = in_ctxt.Source
    oLayout.Clear()
    oLayout.AddEnumControl("List", ("chocolate", 0, "vanilla", 1, "strawberry", 2), "Flavor", constants.siControlCombo )
    oLayout.AddButton("Update")
   
    return true

def ComboTest_Update_OnClicked( ):
    Application.LogMessage("ComboTest_Test_OnClicked called")
    x = ("Coffee Heath Bar Crunch", 0, "Cherry Garcia", 1, "Dulce Delux", 2 )
    PPG.PPGLayout.Item("List").UIItems = x
    Application.LogMessage( PPG.PPGLayout.Item("List").UIItems )
    PPG.Refresh()

Adding sub-menus in Python


In Python, use Menu.AddSubMenu to add a submenu.

def SubMenuTest_Menu_Init( in_ctxt ):
	oMenu = in_ctxt.Source
	subMnu = oMenu.AddSubMenu( "Test SubMenu" )
	subMnu.AddCommandItem("Test", "Test")
	return true

Don’t use AddItem, because in Python the derived class methods of the returned object are not resolved properly (it’s an issue with late binding). Basically, with AddItem you end up with a Menu object that supports just the MenuItem interface. So, methods like AddCommandItem, which belong to the derived Menu class, are not resolved and you get errors like this:

# ERROR : Traceback (most recent call last):
#   File "<Script Block 2>", line 55, in Test_Menu_Init
#     subMnu.AddCommandItem("Duplicate Single", "Duplicate Single")
#   File "C:\Program Files\Autodesk\Softimage 2011\Application\python\Lib\site-packages\win32com\client\__init__.py", line 454, in __getattr__
#     raise AttributeError, "'%s' object has no attribute '%s'" % (repr(self), attr)
# AttributeError: '<win32com.gen_py.Softimage|XSI Object Model Library v1.5.MenuItem instance at 0x517703560>' object has no attribute 'AddCommandItem'

Notice how it says that MenuItem instance has no attribute ‘AddCommandItem’.
AddCommandItem is defined by the derived Menu class.

Before the AddSubMenu method was added, you had to workaround this with win32com.client.Dispatch:

subMnu = win32com.client.Dispatch( oMenu.AddItem("Test SubMenu", constants.siMenuItemSubmenu ) )

Python: importing modules into plugins


New in the Softimage 2011 Subscription Advantage Pack

The siutils Python module makes it easier to import modules into your self-installing plugins. Just put your modules in the same location as your plugin file , and you can use the __sipath__ variable to specify the module location.

__sipath__ is always defined in the plugin namespace, so no matter where you put a plugin, you can simply use __sipath__ to specify the location.

Here’s a simple example that shows how to import a module into your plugin.

  • Line 04: Import the siutils module
  • Line 39: Use add_to_syspath() to add __sipath__ to the Python sys path.
    If the module was located in a subfolder of the plugin location, you could use siutils.add_subfolder_to_syspath( __sipath__, ‘mysubfolder’ )

  • Line 40: Import the module
import win32com.client
from win32com.client import constants

import siutils

null = None
false = 0
true = 1

def XSILoadPlugin( in_reg ):
	in_reg.Author = "blairs"
	in_reg.Name = "TestPlugin"
	in_reg.Major = 1
	in_reg.Minor = 0

	in_reg.RegisterCommand("Test","Test")
	#RegistrationInsertionPoint - do not remove this line

	return true

def XSIUnloadPlugin( in_reg ):
	strPluginName = in_reg.Name
	Application.LogMessage(str(strPluginName) + str(" has been unloaded."),constants.siVerbose)
	return true

def Test_Init( in_ctxt ):
	oCmd = in_ctxt.Source
	oCmd.Description = ""
	oCmd.ReturnValue = true

	return true

def Test_Execute(  ):

	Application.LogMessage("Test_Execute called",constants.siVerbose)

#	print __sipath__ 

	siutils.add_to_syspath( __sipath__ )
	import TestModule
	TestModule.test( "hello world" )

	return true

VBScript compilation errors with Python PPG logic


On a machine that has Softimage 2011 or 2011 SP1 installed, but not Python, you’ll get VBScript compilation errors when you run a script or plugin that uses Python for the PPGLayout.Logic of a dynamic (on-the-fly) property. For example, running this script

xsi = Application
oRoot = xsi.ActiveSceneRoot
oProp = oRoot.AddProperty("CustomProperty", False, "test")
oLayout = oProp.PPGLayout
oLayout.Language = "Python"
oLayout.Logic ="""
Application.LogMessage('Hello world')
"""
xsi.InspectObj( oProp )

will give this VBScript syntax error:

# ERROR : Syntax error - [line 2]
# ERROR : Property Page Script Logic Error (Microsoft VBScript compilation error)
# ERROR :    [1] (null)
# ERROR :   >[2] Application.LogMessage('Hello world')
# ERROR :    [3] (null)
# ERROR : 	Syntax error

This happens because Softimage 2011 doesn’t detect the Python installed with Softimage, so it falls back to the default VBScript when it tries to execute the Python PPG.Logic code. However, Python works as usual in all other respects (note that the above errors are logged as Python comments!).

You can workaround this by setting PPGLayout.Language to “pythonscript”.

Or you could leave the Language set to “Python” and add these registry entries to your system.
This way you don’t have to update existing code.

[HKEY_CLASSES_ROOT\Python]
@="Python ActiveX Scripting Engine"

[HKEY_CLASSES_ROOT\Python\CLSID]
@="{DF630910-1C1D-11d0-AE36-8C0F5E000000}"

[HKEY_CLASSES_ROOT\Python\CurVer]
@="Python.AXScript.2"
[HKEY_CLASSES_ROOT\Python\OLEScript]

To add the registry entries, save the above in a .reg file and then double-click it.

Python not available in 2011 SP1 after you uninstall 2011


A customer reported this last Friday. After he installed Softimage 2011 SP1, he uninstalled 2011, and after that Python did not show up as a scripting language in 2011 SP1.

I uninstalled 2011 and sure enough Python was gone in 2011 SP1.
To get Python back, open a Softimage 2011 SP1 command prompt and run runonce.bat.

Note that on Windows 7 or Vista, you’ll have to use an elevated command prompt (right-click the Command Prompt shortcut and click Run as Administrator).

Python – Adding parameter to SCOP


Here’s a Python version of the CustomOperator.AddParameter example in the SDK docs, which shows how to add parameters to a scripted operator (SCOP).

from win32com.client import constants
from win32com.client import constants

null1 = Application.GetPrim( "Null" )
f = XSIFactory

s = """def MySOP_Update( ctx, out ):
   xsi = Application
   xsi.logmessage( 'update' )"""

sop = f.CreateScriptedOp( "MySOP", s, "Python" )


sop.AddOutputPort( null1.posx )

param1 = sop.AddParameter( f.CreateParamDef2("text", constants.siString, "hello") )
param2 = sop.AddParameter( f.CreateParamDef2("bool", constants.siBool, True) )
param3 = sop.AddParameter( f.CreateParamDef2("int", constants.siInt4, 10, 0, 100) )
param4 = sop.AddParameter( f.CreateParamDef2("dbl", constants.siDouble, 0.5, 0.0, 1.0) )

sop.Connect()

Application.InspectObj( sop )

Python – Getting rules from a connection mapping template


This snippet shows how to read a connection (or value) mapping template into a Python dictionary.

t = Application.Dictionary.GetObject( "Model1.Mixer.MappingTemplate1" )

n = Application.GetNumMappingRules( t )

d = {}
for i in range(1,n+1):
        r = Application.GetMappingRule( t, i )
        d[r.Value("From")] = r.Value("To")
        
for k, v in d.iteritems():
     Application.LogMessage( k + " > " +  v )

Line 8 GetMappingRule uses output arguments, so we have to take the returned ISIVTCollection and extract the From and To output arguments. We use From as the dictionary key.