Solutions for Python in XSI (Part 2)
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:
- 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.
- If the functions are non XSI specific and don’t make much logical sense to be added as XSI commands.
- 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.
- 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, \ result,result) 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.