2. Interacting with the core of Exopy

This section will focus on the functionality offered by the plugins constituting the core of the Exopy application and how custom plugin can use and or extend those functionalities.

2.1. Providing application wide commands and sharing state

One usual need in plugin application is to make available to all other part of the application some function (for example the possibility to request the use of a driver) or to let other part of the application what is the state of a plugin (for example a list of all the available drivers).

One could of course directly access the plugin to get those informations but in a plugin application it is a good to avoid such interferences. Those informations are actually delegated to two plugin responsible for managing them :

  • the ‘enaml.workbench.core’ plugin is in charge of managing commands which are the equivalent of application wide available function. Each command has an id which is used to invoke it using the invoke_command of the CorePlugin (this is the only case in which one needs to access directly to a plugin). When invoking a command one must pass a dictionary of parameters and can optionally pass the invoking plugin. To know what arguments the command expect you should look at its description in the manifest of the plugin contributing it.

  • the ‘exopy.app.states’ plugin is in charge of managing states which allow to get access to a read-only representation of some of the attributes of a plugin. The state of a plugin can be requested using the Command ‘exopy.app.states.get’ with an id parameters identifying the plugin constituting the state. If you need to access to such a state you should observe the alive attribute which becomes False when the plugin contributing the state is unregistered.

2.1.1. Declaring a Command

In order to declare a command, you must contribute a Command object to the ‘enaml.workbench.core.commands’ extension point. A Command must have :

  • an id which must be unique (this a dot separated name)

  • a handler which is a function taking a argument an ExecutionEvent instance. The execution event allows to access to the application workbench (‘workbench’ attribute) and to the parameters (‘parameters’ attribute) passed to the invoke_command method. IF the command need to access to the plugin you can do so easily using the workbench.

  • a description which is basically the docstring of the command and should be formatted as such (see Style guide).

2.1.2. Declaring a State

In order to share the state of your plugin you must contribute a State object to the ‘exopy.app.states.state’ extension point. A State must have :

  • an id which must be unique and can be the id of the plugin but does not have to.

  • the names of the members of the plugin the state should reflect (as a list).

  • an optional description.

2.2. Customizing application start up and closing

In some cases, a plugin needs to perform some operation at application start up (for example discover extension packages, or adding new logger handlers) or some special clean up operations when the application exits. It may also need to have a say so about whether or not the application can exit (if a measurement is running the application should not exit without a huge warning). The ‘exopy.app’ plugin is responsible for handling all those possibilities. It relies on three extension points (one for each behaviour) :

  • ‘exopy.app.startup’ accepts AppStartup contributions and deal with the start up of the application.

  • ‘exopy.app.closing’ accepts AppClosing contributions and deal with whether or not the application can be closed.

  • ‘exopy.app.closed’ accepts AppClosed contributions to run clean up operation before starting to unregister plugins.

Note

The customisation of the start up and exit of the application should only be used for operations not fitting into the start() and stop() methods of the plugin. This customization fits operations that must be performed at application start up and cannot be deferred to plugin starting, or clean up operations requiring the full application to be active (ie not dependent only on the plugin state).

2.2.1. Declaring an AppStartup extension

In order to customize the application start up, you need to contribute an AppStartup object to the ‘exopy.app.startup’ extension point. An AppStartup must have :

  • an id which must be unique and can be the id of the plugin but does not have to.

  • a run attribute which must be a callable taking as single argument the workbench.

  • a priority, which is an integer specifying when to call this start up.

Note

Start up are called from lowest priority value to highest and by their order of discovery if they have the same priority. The default priority is 20.

2.2.2. Declaring an AppClosing extension

In order to customize how the application determine whether or not it can exit, you need to contribute an AppClosing object to the ‘exopy.app.closing’ extension point. An AppClosing must have :

  • an id which must be unique and can be the id of the plugin but does not have to.

  • a validate attribute which must be a callable taking as arguments the main window instance (from which the workbench can be accessed) and the EventClose associated with the attempt to close the application. If the plugin determine that the application should not be closed, it should call the reject method of the EventClose.

2.2.3. Declaring an AppClosed extension

In order to customize the application closing, you need to contribute an AppClosed object to the ‘exopy.app.closed’ extension point. An AppClosed must have :

  • an ‘id’ which must be unique and can be the id of the plugin but does not have to.

  • a ‘clean’ attribute which must be a callable taking as single argument the workbench.

  • a priority, which is an integer specifying when to call this start up.

Note

Closed are called from lowest priority value to highest and by their order of discovery if they have the same priority. The default priority is 20.

2.3. Using the built in preferences manager

If any of your plugin need to retain user preferences from one application run to the next it should use the built-in preferences management system, which is straightforward. First your plugin should inherit from HasPrefPlugin and should call the parent class start method in its own start method. Second all members which should be saved should be tagged with the ‘pref’ metadata (use the tag method). The value of the metadata can be True or any of the values presented in :ref: TODO. All value thus tagged are loaded from the preference file if found, and saved when the user request to save the preferences. Finally, a Preferences object to the ‘exopy.app.preferences.plugin’ extension point. A single Preferences object can be contributed per plugin.

Note

The preferences system saves object by writing their repr to a file so any object whose repr can be evaluated by literal_eval can be saved (literal_eval is used for security reasons).

A Preferences object has the following members :

  • ‘auto_save’: list of the names of members whose update should trigger an automatic saving of the preferences.

  • ‘edit_view’: an enaml Container used to edit the preferences of the plugin. If no such object is conytributed the default templating mechanism presented below is used.

  • ‘saving_method’: name of the plugin method to use to retrieve the values of the members which should be saved.

  • ‘loading_method’: name of the plugin method to use to update the values of the saved members.

A Preferences object can be left blank as the default values are fine most of the time.

2.4. Declaring error handlers

During the application lifetime errors can occurs and the user needs to be informed about them. Exopy provides a command to do so ‘exopy.app.errors.signal’. This command expects a ‘kind’ keyword specifying which handler to use to for reporting this error. The selected handler determine the expected keywords.

By default Exopy provides the following handlers, which displays the error and log it:

  • ‘error’ : To report an error which does not deserve a more complex handler. It expects a single ‘message’ keyword.

  • ‘extensions’ : To report an error related to the loading of an extension. It expects a ‘point’ keyword referring to the extension point where the error occurred, and an ‘infos’ dictionary describing the issues that occurred.

In some situations, it is desirable to wait before reporting errors that the execution of some code completed. To this effect the error plugin provides the ‘exopy.app.errors.enter_error_gathering’ which will hold the processing of the errors till ‘exopy.app.errors.exit_error_gathering’ is called.

Plugins can contribute new error handler to the ‘exopy.app.error.handler’ extension point. The contribution should be an ErrorHandler object.

An ErrorHandler needs to declare :

  • ‘id ‘: a unique id which will be used as ‘kind’ when calling ‘exopy.app.errors.signal’

  • ‘handler’ : a method handling the error. Note that to deal with error gathering it must be able to handle list of dictionary and not only dictionary. The handler shoudl log that an error occurred and return a widget to be displayed if it makes sense.

  • ‘report’ : a method which should provide a summary of the errors that occurred it is meaningful.

Note

As using this mechanism will cause a window to be displayed for the user sakes these commands should be called only from function/methods directly invoked at the level of the GUI.

2.5. Declaring dependencies

When loading and transferring complex object over the network Exopy needs to collect all the base classes needed for reconstructing the object in an environment lacking an active workbench. These are considered to be build dependencies. In the same way some resources can be necessary to execute some part of the application and need to be queried beforehand to allow the system to run in a situation where the workbench is absent. Those are considered to be run-time dependencies.

If your plugin introduces a new type of object which can, for example, be used in tasks either as a build or as a runtime dependency you need to contribute either a BuildDependency object to the ‘exopy.app.dependencies.build’ extension point or a RuntimeDependecyCollector object to the ‘exopy.app.dependencies.runtime_collect’ extension point. In the case of runtime dependencies, the collector is not responsible for the analysis of the dependencies of an object this is left to an associated RuntimeDependecyAnalyser, which allow to use the same kind of dependeny in object with totally different structures and for which the same scheme of analysis cannot be used. RuntimeDependecyAnalyser can be contributed to the ‘exopy.app.dependencies.runtime_analyse’ extension point.

After analyses dependencies are stored into dedicated container class. Those containers can then be used to request the identified dependencies. Once again the same kind of container is returned which store the dependencies as a nested dict in its ‘dependencies’ member. The top level of that dict corresponds to the id of the dependency collector. Under each collector id the dependencies are stored simply by id.

Note

An object introducing a new kind of build dependency should have a dep_type attribute which should be an atom.Constant and which must be saved if the object can be saved under the .ini format.

A BuildDependency needs:

  • an ‘id’ which must be unique and must match the name used for dep_type attribute value of the object this dependency collector is meant to act on.

  • ‘analyse’: a method used to determine the dependencies of the object under scrutiny. Build dependencies should be added to the dependencies dictionary and runtime dependencies analysers ids should be returned (they will be called by the framework at a later time).

  • ‘validate’: a method checking that all dependencies corresponding to this collector can be collected (they exist).

  • ‘collect’: a method collecting the build dependencies previously identified by the analyse method.

A RuntimeDependecyCollector needs:

  • an ‘id’ which must be unique.

  • ‘validate’: a method checking that all dependencies corresponding to this collector can be collected (they exist).

  • ‘collect’: a method getting the runtime dependencies previously identified by the analyse method. This method should request the privilege to use the dependencies if it makes sense.

A RuntimeDependecyAnalyser needs:

  • an ‘id’ which must be unique.

  • a ‘collector_id’ which should match a declared RuntimeDependencyCollector id.

  • ‘analyse’: a method used to determine the runtime dependencies of the object under scrutiny. The dependencies should not be collected.

Please refer to the API documentation for more details about those objects and the signature of the methods that need to be implemented.

2.6. Customizing logging

By default Exopy use two logs:

  • a log collecting all levels and directed to a file (in the application folder under logs) and which is rotated daily or every time the application starts.

  • a log collecting INFO log and above and stored in a string with a max of 1000 lines. This string is meant to be used for displaying the log in the GUI, and is available from the state of the log plugin (‘exopy.app.logging’).

If you need to add handlers, formatters or filters, you should do so in the start() method of your plugin by calling the corresponding commands (found in LogManifest).

2.7. Contributing to the application interface

2.7.1. Adding entries in the main window menu bar

Plugins can also add new entries to the menu bar of the application main window. To do so they should contribute MenuItem and ActionItem to the ‘enaml.workbench.ui’ plugin.