Python

From The Foundry MODO SDK wiki
Revision as of 21:56, 15 May 2015 by Jangell (Talk | contribs) (sys.exit)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Python is a powerful object-oriented scripting language that is available on every platform and has extensive community support.The official Python documentation can be found at www.python.org.

As with Macros, Python uses commands to interact with modo, but unlike macros, Python can be used to query those commands for their values and usefully interpret the results. Because it can query commands, Python can make excellent use of the ScriptQuery system provided by the query command to obtain lower-level information from the various subsystems, including extracting specific mesh information that is not otherwise accessible through commands themselves.

modo currently uses the Python 2 line. The Python 3 line is not compatible with Python 2.x, and is not currently supported.

Python API

modo 701 introduces a deeply integrated Python API. This provides SDK-level access to the modo through Python, allowing full-on plug-ins to be created entirely through scripting, without any knowledge of C++ itself.

Much of this article concerns the older fire-and-forget approach to Python scripting. This can be mixed with the new Python API, but it is important to be aware that commands can only be executed at certain times when it is safe, and the Python API is usually much faster than command queries. For these reasons you will likely find that lx.eval and other functions in this article aren't used as much anymore.

Python Header

All Python scripts start with a simple header so that the interpreter can recognize them.

 # python

The lx Module

The modo extensions to Python are encapsulated in the lx module. In older versions of modo, you first needed to import the lx module like so, but starting with modo 301 both this and sys are now implicitly imported.

 import lx

The Python implementation in modo is a bit different than that of Lua or Perl. As Python is an object oriented language, all of the standard functions are encapsulated as methods in the lx object. Error handling is performed with exceptions instead of result codes. This is explained in detail later on.

lx.trace

modo 401 introduced lx.trace support for Python. The lx.trace method can be used to toggle tracing on and off, causing the results of many of the lx methods to be output to the Scripting sub-system of the Event Log Viewport. Passing True turns tracing on, while False turns it off. It can also be used to test if tracing is on or not by not passing no arguments. Note that these booleans states are case sensitive Python boolean keywords, and must be properly capitalized to avoid syntax errors.

 tracing = lx.trace();  # See if tracing is on
 lx.trace( True );      # Turn on tracing


lx.out

Text can be output to the Event Log with 'lx.out. Any arguments passed will be concatenated before being written out. An empty argument list outputs a blank line.

 # python
 
 # Print an blank line
 lx.out()
 
 # Print a label followed by the Python version string
 lx.out( "Python Version: ", sys.version )

lx.eval

lx.eval is used to both execute and query commands in Python. Simply pass in a command string with the appropriate arguments using standard command syntax, and the command is executed or queried.

 # Execute
 lx.eval( "layout.togglePalettes" )
 
 # Query
 q = lx.eval( "layout.togglePalettes ?" )

Unlike Lua and Perl, all error reporting is done through exceptions. When executing a command, lx.eval will always return None. When querying a command, this returns either an array of elements or a single value, depending on the number of elements in the query. To see if the result of lx.eval is an array of values or a single value, you can call the Python type() function on the variable.

 # python
 
 q = lx.eval( "material.name ?" )
 
 if type( q ) == tuple:
     # Array of values
 else:
     # Single Value

lx.eval1 and lx.evalN

There are many cases where a query may return one or many elements, often depending on what is currently selected in the application. Since lx.eval'’s return value depends on the number of elements, you would need to handle both cases in a separate branches of code, which can get tedious. To avoid this issue, you can use lx.eval1 and lx.evalN. lx.eval1 always returns None or one element directly, even if the query returned a list of elements (in which case the first element is returned). lx.evalN will always return None or an array, even if there is only one element.

 # python
 
 # Query and get a single value
 q1 = lx.eval1( "material.name ?" )
 
 # Query and get an array of values
 qN = lx.evalN( "material.name ?" )

Executing a command with lx.eval1 or lx.evalN operates identically to lxeval.

lx.command

Rather than execute a command with a string, you can pass the command name and each of it’s arguments directly through lx.command. This is often simpler than having to construct a command string from scratch.

lx.command makes use of name/value pairs by taking advantage of Python’s ability to specify arguments and their values by name.

 # python
 
 lx.command( "view3d.shadingStyle", style="wire" )

lx.test

Python allows for easy testing of ToggleValue commands with lx.test. This is used in a similar manner to lxqt in Perl and Lua, returning True if the ToggleValue is on and False if it is off.

 # python
 
 isActive = lx.test( "tool.set prim.cube on" )

lx.option() and lx.setOption()

The lx.option and lx.setOption methods allow the script to set properties that determine how the other lx methods operate. This operates similarly to Perl, where you pass the property to be modified and the way in which it is to be modified. The primary difference between Python and Perl is that queryAnglesAs (the only property currently supported) defaults to radians for backwards compatibility with previous versions of modo.

 # python
 
 lx.setOption( "queryAnglesAs", "radians" )
 lx.out( lxoption( "queryAnglesAs" ) )

lx.arg and lx.args

Argument parsing is available through the lx.arg and lx.args methods. lx.arg returns the raw argument string that was passed into the script. lx.args parses the argument string and returns an array of arguments for easier processing.

 # python
 
 argsAsString = lx.arg()
 argsAsTuple = lx.args()


ScriptQuery Service

Accessing ScriptQuery interfaces from Python can be accomplished using the query command as normal. However, Python also provides a lower level system through the lx module’s lx.Service method and the associated service object. This also has less overhead than using the query command.

lx.Service

The first step in using a ScriptQuery interface is obtaining a Service object with lx.service. This takes the service’s name string as it’s only argument.

 # python
 
 s = lx.Service( "layerservice" )

The Service object has five methods: name, select, query, query1 and queryN. You can have as many service objects as like, each with their own selections. This is in contrast to the query command, which shares its selection as global state among all clients, and thus has but a single selection.

s.name

The name method returns the name of the Service. This is the same string that was passed into lx.Service.

 # python
 
 s = lx.Service( "layerservice" )
 lx.out( "Service Name: ", s.name() )

s.select

The select method takes the place of the query command’s select argument, and is used to pass selectors to the ScriptQuery interface. This take two arguments, the attribute class name string and the selector string. If the attribute doesn’t require a selector it can be omitted. Both arguments can be omitted to clear the selection.

The attribute class name is the part of the attribute before the period. For example, the class of the layer.name attribute is layer. The Service object will also accept the full attribute name and extract the class itself. This example sets the selectors for the layer class attributes to the foreground layers.

 # python
 
 s = lx.Service( "layerservice" )
 s.select( "layer", "fg" )

s.query

The query method queries the Service object for the value of a previously selected attribute. This returns either a single value or an array of values depending on the number of values in the query.

 # python
 
 s = lx.Service( "layerservice" )
 s.select( "layers", "fg" )
 name = s.query( "layer.name" )

s.query1 and s.queryN

The query1 method can be used to always get the first element of a query, while queryN will always return an array of queries. This allows for consistent handling of attributes that may return a variable numbers of elements.

Error Handling with Exceptions

Python error handling in modo is done entirely through exceptions. If one of the lx methods fails because of an unknown command or service, a NameError exception is thrown. If a command fails to execute, a RuntimeError exception is throw. These can be handled with standard Python try and except keywords. The LxResult code of the command failure can be read with sys.exc_info().

 # python
 
 # Set up our try block
 try:
     # First execution: user.defNew creates a new user value
     lx.command( "user.defNew", name="MyValue" )
 
     # Second execution: user.defNew fails because a value with that name already exists
     lx.command( "user.defNew", name="MyValue" )
 
 # Handle exceptions
 except RuntimeError:
     lx.out( "Command failed with ", sys.exc_info()[0] )


Monitors

Progress bars in Python are handled through the Monitor object. This provides the same progress bar functionality as in Perl and Lua.

lx.Monitor

A Monitor object is obtained through a call to lx.monitor. The optional argument is the total number of steps in the progress bar.

 # python
 
 m = lx.Monitor( 42 )

The Monitor object has two methods, init and step. Although you can create as many monitors as you like, the internal mechanism for progress bars will cause only the first one to do anything. However, this will change in the future to allow multiple monitors to be used simultaneously.

m.init

The init method sets the total number of steps in the monitor and resets the current position to 0.

 # python
 
 m.init( 100 )

m.step

The step method increments the current monitor position by 1 if the argument is omitted; otherwise, it increments by that number of steps. It also lets the application check for input, and will thrown an exception if the user clicked the abort button.

 # python
 
 m.step( 2 )

Monitor Example

Here we have a simple example of a monitor in Python. The loop is simply to allow enough time to pass for the monitor to appear. (Note: you may need to tweak the range of the inner loop depending on the speed of your system; on very fast systems, the loop may complete before the monitor appears. Or you can be a good programmer and use a proper time delay function instead of a busy loop, but this is sufficient for our example).

 # python
 
 # Create the monitor. We could pass the total number of steps here, too
 m = lx.Monitor()
 
 # Set the total number of steps
 m.init(1000)
 
 # Do a loop, iterating over our 10000 monitor steps 
 for i in range(0,10000):
 
     # Step the monitor.  We could omit the argument to step by 1 rather than explicitly specifying it.
     m.step(1)
 
     # Pause briefly via a busy loop, so the monitor will be displayed. Monitors only appear if the
     # operation takes a sufficiently long time (a couple of seconds or more).
     for i in range(0, 1000000):
         a=6

sys.exit

Python scripts can be exited by using the standard Python sys.exit() call. Simply calling sys.exit() with no arguments exits the script with no error.

 # python
 
 sys.exit()


Failure can also be reported by passing an argument string in the form of "code:message", although either the code or message can be omitted. The code is a standard message code used in the SDK, each with a different meaning. This can be passed as an integer, a hexadecimal string, or one of the following common codes.

Code Use
LXe_OK Success
LXe_FAILED Failure
LXe_ABORT User Abort
LXe_DISABLED Script Disabled
LXe_NOTFOUND Not Found
LXe_OUTOFBOUNDS Value Out Of Bounds
LXe_INVALIDARG Invalid Argument

Here are some examples of the default "ok" code.

 sys.exit()                 # The default is LXe_OK
 sys.exit( "0" )            # Equivalent to LXe_OK
 sys.exit( "0x00000000" )   # Also equivalent to LXe_OK
 sys.exit( "LXe_OK" )

A code can be combined with a message by adding a colon. For failure codes, this results in an error dialog that displays the failure along with the message.

 # python
 
 sys.exit( "LXe_FAILED:Script failed" )

A message can also be set without a code, although this isn't generally useful as the message isn't currently displayed anywhere.

 # python
 
 sys.exit( ":Insert Message Here" )

Executing Python Scripts from Other Python Scripts

modo supports running one Python script from another Python script by simply using the standard @ syntax with lx.eval(). Since Python normally has a single main interpreter, modo makes use of the Python API’s sub-interpreter mechanism. This allows an almost completely independent state to exist for each executing script. Care should be taken when using functions in low-level modules, such as os.close(), which may affect both the currently running script and the script(s) that executed it. However, for general day-to-day scripting, this is unlikely to be an issue. More information on sub-interpreters can be found at the official Python web site.

Calling Python Scripts as a startup command

When running Modo (CL or GUI) with a script as a startup command argument, paths should be absolute. If relative paths are given, Modo will likely be unable to find the script/command.

External Modules

Python support in modo includes the ability to load external Python modules. You may include extra modules with your scripts by placing them in any imported resource directory. These are specified using the <import> tag in a config file. All of these paths are added to the Python sys.path module search path, thus ensuring that your specific modules will be found. The path to the script itself is also added to sys.path.

Using External Python Modules and the site-packages Directory

Using "traceback" to override modo's default error reporting for accurate line numbers

Most people find, when they start scripting in modo, that getting the correct line number for where an exception occurs can be a bit "hit & miss". When an exception occurs inside a function or method modo will most often report the line number where the fuction/method is called, not where the exception occurs. By structuring your scripts as follows it's possible to get modo to provide the actual line number of the exception consistently:

# python
 
import traceback
 
 
def main():
   # main body of script goes here. Any exceptions raised in classes or functions
   # called from here should report the correct line number for the error in 
   # modo's even log
   pass
 
 
if __name__ == '__main__':
   try:
      main()
   except:
      lx.out(traceback.format_exc())

See also the Python API section on remote debugging for how to use a remote debugger to debug python scripts in modo 701+.

More Information