.. This file is part of Sympathy for Data.
..
.. Copyright (c) 2010-2012 Combine Control Systems AB
..
.. Sympathy for Data is free software: you can redistribute it and/or modify
.. it under the terms of the GNU General Public License as published by
.. the Free Software Foundation, either version 3 of the License, or
.. (at your option) any later version.
..
.. Sympathy for Data is distributed in the hope that it will be useful,
.. but WITHOUT ANY WARRANTY; without even the implied warranty of
.. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.. GNU General Public License for more details.
.. You should have received a copy of the GNU General Public License
.. along with Sympathy for Data. If not, see .
.. _advanced_nodewriting:
Advanced node writing
=====================
.. _adjust_parameters:
Adjust parameters
-----------------
Sometimes you want to adjust the configuration parameters to the input data
that your node receives. This is especially useful to update the choices in a
list parameter.
As an example let us consider a node that takes a table as input. The node has
among its parameters a list of all the columns in the input table. In this list
the user can choose which column the node will operate on. To make sure that
the list is always updated when the columns in the input data change, it could
implement ``adjust_parameters`` something like this::
def adjust_parameters(self, node_context):
"""Update the configuration with current input table columns."""
synode.adjust(node_context.parameters['chosen_column'],
node_context.input['input_port'])
The ``adjust`` function picks up names from the input depending on its data
type. See the ``names()`` method of each :ref:`data type`.
This method will be called before opening the GUI, but after
:ref:`update_parameters`. See also :ref:`All parameters example` for other
examples of ``adjust_parameters``.
.. _controllers:
Controllers
-----------
To make the GUI of your nodes easier to use, you can add controllers that
clarify the interconnection between different parameters. Controllers can make
sure that when some option is chosen some other option becomes
available/unavailable. For example::
from sympathy.api import node as synode
class HelloWorldNode(synode.Node):
"""Prints a custom greeting to the node output."""
name = 'Hello world!'
nodeid = 'com.example.boblib.helloworld'
author = 'Bob '
copyright = '(C) 2014 Example Organization'
version = '3.0'
parameters = synode.parameters()
parameters.set_boolean(
'use_custom_greeting',
value=False,
label='Use custom greeting',
description='While unchecked the node will always print '
'"Hello World!" ignoring any custom greeting.')
parameters.set_string(
'greeting',
value='Hello World!',
label='Greeting:',
description='Choose what kind of greeting the node will print.')
controllers = synode.controller(
when=synode.field('use_custom_greeting', 'checked'),
action=synode.field('greeting', 'enabled'))
def execute(self, node_context):
if node_context.parameters['use_custom_greeting'].value:
greeting = node_context.parameters['greeting'].value
else:
greeting = "Hello World!"
print(greeting)
By disabling elements of the GUI that are not relevant to the current
configuration you can make the configuration GUI easier to understand.
Each controller can have multiple actions and multiple controllers can be added
by simply wrapping them in a tuple. The code below, which contains multiple controllers, is
not directly compatible with the controller example above::
controllers = (
synode.controller(
when=synode.field('use_regex', state='checked'),
action=(synode.field('regex_pattern', state='enabled'),
synode.field('wildcard_pattern', state='disabled'))),
synode.controller(
when=synode.field('use_magic', state='checked'),
action=synode.field('more_magic', state='enabled')))
For another example of how to use controllers, see :ref:`Controller Example`.
.. _custom_ports:
Using custom port types
-----------------------
Previously we learned how to add input and output ports to your nodes::
inputs = Ports([Port.Table('Input Table', name='foo')])
outputs = Ports([Port.Table('Table with some added bar', name='foobar')])
This is the most convenient way to add ports with the most common data types
like Table, Datasource, ADAF, and so on. If you want add a generic type, lambda, or
any other type which does not have its own :class:`Port` method you need to use
the method :meth:`Custom`. As its first argument :meth:`Custom` takes a textual
representation of the port type. The other two arguments are the same as in the
other :class:`Port` methods. The textual representation of the port type can
contain combinations of the following:
* type aliases (e.g. ``adaf`` or ``table``)
* lists (e.g. ``[table]``, meaning a list of tables)
* lambdas (represented as an arrow from input type to output type, e.g. ``table
-> adaf`` meaning a lambda with ``table`` input and ``adaf`` output)
* generic types (e.g. ```` meaning any type or ``[]`` meaning a list of
arbitrary items)
Here are some examples of valid port types:
* ``[table]`` (a list of tables)
* ``[[table]]`` (a list of lists of tables)
* ``adaf -> [adaf]`` (a lambda whose input is an adaf and whose output is a
list of adafs)
* ``[adaf -> [adaf]]`` (a list of such lambdas)
* ```` (Any type)
* `` -> `` (a lambda whose output is of the same type as its input)
* `` -> `` (a lambda with arbitrary input and output)
* ``[ -> ]`` (a list of such lambdas)
* ``(, ) -> `` (a lambda where the input is a tuple in which both elements are of the same type)
If you use generic types, all ports with the same identifier (the ``a`` in
````) have to be of the same type. For example in the node :ref:`Append List`::
inputs = Ports([Port.Custom('', 'Item', name='item'),
Port.Custom('[]', 'List', name='list')])
outputs = Ports([Port.Custom('[]', 'List', name='list')])
The two input ports can be, for example, Table and [Table], or ADAF and [ADAF], but not
Table and [ADAF]. Another example of this is in the :ref:`Map` node::
inputs = Ports([
Port.Custom(' -> ', 'Lambda Function', name='Function'),
Port.Custom('[]', 'Argument List', name='List')])
outputs = Ports([
Port.Custom('[]', 'Output List', name='List')])
Where the input and output type of the lambda determine what type the other
ports must have. Or, if you connect the other ports first, they determine what
types the lambda's input and output must have.
Port.Custom accepts n as an optional keyword argument to create a range of ports
from the same definition::
# Exactly 3 ports.
inputs = Ports([
Port.Custom('[]', 'Argument List', name='List', n=3)])
# Minimum of 3 ports with no upper bound, though 6 ports in total is
# The current limit.
inputs = Ports([
Port.Custom('[]', 'Argument List', name='List', n=(3,))])
# Minimum of 3 ports up to a maximum of 5 ports.
inputs = Ports([
Port.Custom('[]', 'Argument List', name='List', n=(3,5))])
# Minimum of 0 ports up to a maximum of 5 ports, starting out with a
# default of 2 ports.
inputs = Ports([
Port.Custom('[]', 'Argument List', name='List', n=(0,5,2))])
As you can see, n accepts either a single integer or a tuple of up to 3 integer
components: minimum, maximum, and default. When maximum and default are not supplied,
they assume the same value as minimum.
When the n argument is used, a name is also required and all port names need to be
unique. This is a good practice, in general.
In order to make nodes reasonably compatible between different versions of
Sympathy it is important that the default ports remains the same. If the default
ports (which you will get by dragging a new node from the library into a
workflow) change, consider changing the nodeid and start a new node.
.. _update_parameters:
Managing node updates
---------------------
When developing a node over time it is not uncommon that the set of node
parameters change slightly from one version of the node to the next.
Default value (the arguments ``value``, ``value_names``, ``list``, ``plist``)
can always be updated without risk of breaking old workflows. The change simply
wont affect old workflows at all.
As of Sympathy 1.2.5 newly added parameters are automatically added to old
instances of nodes when they are configured, executed and so on. So simply add the
new parameter to the node definition and you can expect the new parameter to
always be there when you reach any node method, such as ``execute``.
As of Sympathy 1.3.0 any changes to the label or description of an existing
parameter are automatically applied to nodes.
If you need more fine-grained control you can implement the node method
``update_parameters(self, old_params)`` (available as of Sympathy
1.2.5). This method can create new parameters where the default value of the
new parameter depends on the value of some of the old parameters. You do this
by making changes to the argument ``old_params``. Any parameters that are still
missing after this method are added automatically from the parameter
definition.
Here is an example of ``update_parameters`` from :ref:`Calculator List`::
def update_parameters(self, old_params):
# Old nodes without the same_length_res option work the same way as if
# they had the option, set to False.
if 'same_length_res' not in old_params:
old_params['same_length_res'] = self.parameters['same_length_res']
old_params['same_length_res']['value'] = False
.. _custom_gui:
Custom GUIs
-----------
For most basic nodes the configuration GUI can be created automatically.
This is very convenient but is of course a bit limited. More advanced nodes
can also choose to implement their own custom configuration GUIs without
such limitations. All GUIs in Sympathy are created using Qt
(http://www.qt-project.org).
To create a custom GUI implement the method ``exec_parameter_view(self,
node_context)`` to return a custom widget which will be run when configuring
the node.
This is probably a good place for an example. Let us continue with the Hello
World example and add a custom GUI in which the user can not only set the
greeting, but also click a button to test it::
from sympathy.api import node as synode
from sympathy.api import ParameterView
from sympathy.api import qt
QtGui= qt.import_module('QtGui')
class MyWidget(ParameterView):
def __init__(self, parameters, parent=None):
super(MyWidget, self).__init__(parent=parent)
self._parameters = parameters
greeting_edit = self._parameters['greeting'].gui()
button = QtGui.QPushButton('Test greeting')
button.clicked.connect(self.test_greeting)
layout = QtGui.QHBoxLayout()
layout.addWidget(greeting_edit)
layout.addWidget(button)
self.setLayout(layout)
def test_greeting(self):
QtGui.QMessageBox.information(
self, 'A greeting...', self._parameters['greeting'].value,
QtGui.QMessageBox.Ok)
class HelloWorldNode(synode.Node):
"""Prints a custom greeting to the node output."""
name = 'Hello World!'
nodeid = 'com.example.boblib.helloworld'
author = 'Bob '
copyright = '(C) 2014 Example Organization'
version = '4.0'
parameters = synode.parameters()
parameters.set_string(
'greeting',
value='Hello World!',
label='Greeting:',
description='Choose what kind of greeting the node will print.')
def exec_parameter_view(self, node_context):
return MyWidget(node_context.parameters)
def execute(self, node_context):
greeting = node_context.parameters['greeting'].value
print(greeting)
The editors/widgets created in the parameter definition can also be used in a
custom GUI, but one has to add them to the layout one by one, as it is done
with regular Qt widgets. The benefit of using widgets defined in the parameter
definition, is that the signals emitted from the widgets are taken care of, and
the parameters are updated automatically when the user makes changes in the
GUI.
If one has created a list of parameters with the name ``'combo_example'``,
the command to access its editor widget would look like::
example_combo = self._parameters['combo_example'].gui()
Since the user might decide to open the GUI even when there is no data
ready on the input ports (e.g. when no node has been connected to the input
port), we need to check that there actually is data ready on that port before
using it. To test if the input data is available you can use the method
:meth:`is_valid` on the port. If it returns ``True`` you can safely use the
input data.
If the widget keeps an internal model of the parameters it should define a
method called ``save_parameters`` which updates ``node_context.parameters``.