Overview of the Scripting Framework

Index of All Documentation » Wing Pro Reference Manual » Scripting and Extending Wing »


Scripts are Python modules or packages containing one or more Python functions which implement the script's functionality. Any top-level function with a name that starts with a character other than underscore _ is added to Wing's command set, so it becomes accessible from menus, key bindings, and the toolbar. Scripts can also use the scripting API to hook into IDE functionality in other ways, for example to perform an action every time an editor is saved to disk.

When Wing starts up, it will search for scripts in all directories in the path configured with the IDE Extension Scripting > Search Path preference. By default this path contains a directory named scripts within the Settings Directory. Scripts can also be placed in scripts inside the Install Directory shown in Wing's About box, but this is not recommended since it is harder to manage across updates of Wing.

Scripts can be modules named *.py and packages, which are directories that contain a file named __init__.py file and any number of other *.py files or sub-packages. For packages, Wing loads only the modules that are imported in the __init__.py file.

Script files within each directory are scanned in alphabetical order. When multiple script-defined commands with the same name are found, the command that is loaded last overrides any loaded earlier under the same name. However, scripts cannot replace internally defined commands, as detailed below.

Naming Commands

Commands added by scripts can be referred to either by their short name or their fully qualified name (FQN).

The short name of a command is the same as the function name, optionally with underscores replaced by dashes (cmdname.replace('_', '-')).

The FQN of a command always starts with .user., followed by the module name, followed by the short name.

For example, if a function named do_it is defined inside a module named xpext.py, then the short name of the command created will be do-it and the FQN will be .user.xpext.do-it.

Overriding Internal Commands

Wing will not allow a script to override any of the commands documented in the Command Reference. If a script is named the same as a command in Wing, it can only be invoked using its fully qualified name. This is a safeguard against breaking the IDE by adding a script.

One implication of this behavior is that a script may be broken if a future version of Wing ever adds a command with the same name. This can generally be avoided by using appropriately descriptive and unique names and/or by referencing the command from key bindings and menus using only its fully qualified name.

Execution Context

Scripts are run in the same process space as the IDE, using Wing's private Python 2.7 interpreter. Because they are in the same process space, scripts have the potential for breaking the IDE. For example, a script entering into an infinite loop will lock up Wing.

To avoid this, script-provided functionality must be written within the framework for cooperative asynchronous multi-tasking that Wing uses internally. In this approach, lengthy computations are split into small units that are interleaved with the main event loop. This is supported in the scripting API by InstallTimeout in CAPIApplication. This calls a given function periodically until it is removed with RemoveTimeout, until it returns a value where bool(value) is False, or until the script that installed it is reloaded.

This example implements a command that counts down from 10 in the status area at the bottom of the screen:

import wingapi
def start_counting():
  counter = [10]
  def count():
    counter[0] -= 1
    wingapi.gApplication.SetStatusMessage("Time left: {}".format(counter[0]), timeout=1)
    return counter[0]
  wingapi.gApplication.InstallTimeout(1000, count)

To interact asynchronously with a sub-process, use this approach in combination with AsyncExecuteCommandLine*. Here is an example that runs ping for ten seconds and shows status messages at the bottom of the IDE window:

import wingapi
import sys
import time
def process_example():
  cmdline = ['ping', '-t', '9', 'wingware.com']
  handler = wingapi.gApplication.AsyncExecuteCommandLine(cmdline[0], None, *cmdline[1:])
  timeout = time.time() + 10
  def poll(timeout=timeout):
    kill = time.time() > timeout
    if kill or handler.Iterate():
      stdout, stderr, err, status = handler.Terminate(kill)
      if kill:
        msg = "Time out"
      elif err is not None:
        msg = "Process failed to start;  exit_status={}, errno={}".format(status, err)
      else:
        msg = "Process exited; stdout len={}; stderr len={}".format(len(stdout), len(stderr))
      wingapi.gApplication.SetStatusMessage(msg)
      return False
    else:
      if handler.stdout:
        msg = "Last output line: {}".format(handler.stdout[-1].splitlines()[-1])
      else:
        msg = "No output yet"
      wingapi.gApplication.SetStatusMessage(msg)
      return True

  wingapi.gApplication.InstallTimeout(100, poll)

For additional examples, see the scripts folder inside the Install Directory listed in Wing's About box.

Signals

Another important concept in writing extension scripts is the use of signals to control when script-provided functionality is implemented. Each of the classes in the scripting API provides signals that notify of different events in the user interface. Signals may be connected to handlers that are called when the signal is emitted with the defined set of parameters for that signal.

For example, CAPIApplication emits project-open(filename) when a new project has been opened. The signal can be connected to a handler function as follows:

import wingapi
def _proj_open(filename):
  wingapi.gApplication.SetStatusMessage("Project opened: {}".format(filename))
gSignalID = wingapi.gApplication.Connect('project-open', _proj_open)

Once this script is loaded into Wing, _proj_open will be called every time a new project is opened. This example displays message in the status area at the bottom of the IDE window. The message includes the filename, which is the single parameter sent with this particular signal.

Disconnecting from the signal later would be accomplished as follows for the above example:

wingapi.gApplication.Disconnect(gSignalID)

Signals for each class are documented in wingapi.py. For additional examples, see the scripts folder inside the Install Directory listed in Wing's About box.

Reloading Scripts

Wing watches script files and automatically reloads them when they are edited inside Wing and saved to disk. The only exception to this occurs when a new script is added. In this case, Wing will not load the new script until Reload All Scripts in the Edit menu is executed or the IDE is restarted.

Reloading will not work for any file that sets _ignore_scripts at the top level, or for modules outside of the script path. For details on how reloading works, see Advanced Scripting.