Home » Featured, Python, XSI

Solutions for Python in XSI (Part 2)

13 July 2010 8,733 views 3 Comments

This is the second of two articles that will show you solutions to common problems you’ll face when using Python in XSI.

Importing Python Modules

Python modules offer a great way to reuse code. Often you’ll find that you want to share code between your plugins, especially in a pipeline situation.

XSI isn’t particularly flexible with how it lets you import modules. At the moment, you have to install your custom written modules in a folder and use the PYTHONPATH environment variable to point to that location (or add it to the list of paths stored in sys.path at run time.)

Wouldn’t it be good if we were able to import Python modules so that:

  • You can keep your modules with the plugin that uses them.
  • You never have to modify your Python path, even when you move the installed plugin to a different location.
  • The module is only loaded when you need it.

Of course, you can just use a standard XSI plugin and register some commands, but there are situations where it might be preferable to use a module. For example:

  1. If the functions to be included are very small helper functions that need to be called repeatedly. It might be best to avoid registering commands since they have a slight performance overhead to their execution.
  2. If the functions are non XSI specific and don’t make much logical sense to be added as XSI commands.
  3. If the functions need to be pass or return Python types such as classes, tuples, sets, lists and dictionaries. There are workarounds for these, but they don’t really assist with making your code clean and maintainable.
  4. If you want to keep your code hidden, you can put your implementation inside the module and only distribute the .pyc compiled version.

A new way to import modules

The good news is that you can use the following Python class to mimic the behaviour of an imported module.


import win32com.client
from win32com.client import constants

import os.path
import imp

#Note: You only need the following line if you're
#putting this code in a file that isn't an XSI plugin.
Application = win32com.client.Dispatch("XSI.Application").Application

class Import:

    def __init__(self, pluginName, moduleName):
        self.pluginName = pluginName
        self.moduleName = moduleName
        self.module = None

    def __getattr__(self, name):
        #Has the module already been loaded?
        if self.module:
            return self.module.__dict__[name]

        #Try and load the module
        path=""
        try:
            path = Application.Plugins(self.pluginName).FileName
        except:
            Application.LogMessage("[Plugin Python Module: " \
                             +self.pluginName+"] " \
                             +"Plugin name doesn't exist or hasn't been loaded" \
                             ,constants.siError)
            return None

        #Import the module
        result = imp.find_module(self.moduleName, [os.path.dirname(path)])
        self.module = imp.load_module(self.moduleName, result[0], \
                                      result[1],result[2])

        if not self.module:
            Application.LogMessage("[Plugin Python Module: " \
                             +self.pluginName+"] " \
                             +"Couldn't load module: "+self.moduleName \
                             ,constants.siError)
            return None

        #Evaluate the attribute
        return self.module.__dict__[name]

Assuming you’ve placed the above code into a globally accessible module called xsiimport, then you would use it like this:

Note: I’ve highlighted the specific bits of the code that are relevant


import win32com.client
from win32com.client import constants

import xsiimport

g_PluginName = "TestCommandPlugin"

#Think of this next line as a normal import statement
testpymodule = xsiimport.Import(g_PluginName, "testpymodule")

def XSILoadPlugin( in_reg ):
    in_reg.Author = "Andy"
    in_reg.Name = g_PluginName
    in_reg.Major = 1
    in_reg.Minor = 0

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

    return true

def MyCommand_Execute():
    #Use testpymodule as you would any other python module

    #Call functions
    result = testpymodule.DoSomething("inputString")

    #Create classes
    myClassInst = testpymodule.MyClass()

As shown, the easiest way to use this class is to import it from a module located in your standard Python path. As a result this would generally only work in a studio environment where you are able to make sure your art team are working on the same file system and with the same environment variables.

If you intend to distribute your plugin, then the alternative is to copy the class definition to each of your plugins that needs to use it. While untidy, the benefits it offers are probably worth it, but I’ll leave it to you to decide.

How it works

So if you’re interested in what’s happening under the hood, I’ll briefly run through what’s happening.

This line in our plugin:


testpymodule = xsiimport.Import(g_PluginName, "testpymodule")

doesn’t actually do anything apart from create an instance of the Import class. In its constructor, it just stores the name of the plugin and the name of the module we want to import.

The reason the class doesn’t load the module at this stage is because the plugin probably hasn’t been registered with XSI yet (don’t forget, we’re still running code in global scope here). Therefore, we can’t ask XSI for the file path of the plugin using the Application.Plugins collection.

The module only gets loaded when we ask it to do something in our command like this:


    #Call functions
    result = testpymodule.DoSomething("inputString")

    #Create classes
    myClassInst = testpymodule.MyClass()

At this point, the __getattr__ member function of the Import class is called. It checks to see if the module has already been loaded and loads it if it hasn’t. Either way, the class returns the attribute that is asked for, where it is either called as a function or used to create an instance of a class (as shown above).

This delayed loading mechanism is the reason that this system works and why it’s so robust. All other methods that I’ve tried suffer because the plugin isn’t loaded, which forces you to import modules inside function declarations (which feels a bit hacky)

To load the module, I’m using the imp module. It exposes the lower level commands inside the import mechanism. By calling imp.find_module() followed by imp.load_module(), it forces a reload of the module and flushes any cache stored for that module. Generally, this behaviour is desirable as XSI tends to be quite aggressive with it’s caching.

And in case you’re wondering. Yes. This method for importing works fine with the callback mechanism in the previous article.


Update: 25 Oct, 2010

Autodesk’s new release of the XSI 2011 Advantage Pack contains a new python module called siutils that gives you some of the functionality described in this article. Read more here.

The ability to use the __sipath__ variable now means that you don’t need to use the plugin path lookup to find the location of the current plugin. Assuming this value is valid before the plugin is registered (I’ve not tested it yet) then this would remove the need to use the delayed load method discussed in this article.


1 Star2 Stars3 Stars4 Stars5 Stars (2 votes, average: 5.00 out of 5)
Loading ... Loading ...

3 Comments »

  • Angel said:

    Hello, Andy

    I write this in the mail-list but write in your web too.

    In Kandor solve it using import hooks. Python allows you to change the default import (and reload) methods and use your own ones.
    Using this you load and reload modules using the normal import and reload and without change pythonpath.

    You can find information in this link and can ask me any question.

    http://www.python.org/dev/peps/pep-0302/
    http://orestis.gr/blog/2008/12/20/python-import-hooks/

    P.S: Thanks to Jose Arias that do it (workmate at Kandor)

  • AndyN (author) said:

    Hi Angel
    I don’t think hooks help you in this case. From looking at the links you gave, the hooks are called when import is called. The problem is that at that stage, the plugin isn’t loaded yet so you can’t ask XSI where the plugin path is located.

    That’s why I’m creating this delayed module loading proxy class. The module isn’t actually loaded until you make the first call to one of it’s functions. At that point, you can ask XSI where it’s located to be able to import a module from the same directory.

    Hooks are fine if you always know where the module is located, but the idea behind this system is to allow you to move the plugin and module around together, and have XSI always find it. Since XSI doesn’t set the __path__ variable to a correct value, the only way (as far as I know) to get the path is to interrogate the plugins collection.

  • Angel said:

    Hello Andy

    Yes you are right, but I use it at addons level. My modules are associated to addons not to plugins. Xsi Sdk don’t have Addon object, but is easily to search it at workgroups.

    P.S: Very interesting both python articles

Leave your response!

Add your comment below, or trackback from your own site. You can also subscribe to these comments via RSS.

You can use these tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

This is a Gravatar-enabled weblog. To get your own globally-recognized-avatar, please register at Gravatar.

Security Code: