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):
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 data type.
This method will be called before opening the GUI, but after
Managing node updates. See also Adjust example
for other examples of using adjust_parameters
.
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'
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')))
In addition to fields, the existence of node ports can also be used in as when condition:
return node.controller(
when=node.port(name, state='exists', value=True),
action=[node.field(field_name, state='disabled') for
field_name in field_names])
For another example of how to use controllers, see Controller example.
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 Port
method you need to use
the method Custom()
. As its first argument Custom()
takes a textual
representation of the port type. The other two arguments are the same as in the
other Port
methods. The textual representation of the port type can
contain combinations of the following:
type aliases (e.g.
adaf
ortable
)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 withtable
input andadaf
output)generic types (e.g.
<a>
meaning any type or[<a>]
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)<a>
(Any type)<a> -> <a>
(a lambda whose output is of the same type as its input)<a> -> <b>
(a lambda with arbitrary input and output)[<a> -> <a>]
(a list of such lambdas)(<a>, <a>) -> <b>
(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
<a>
) have to be of the same type. For example in the node Append List:
inputs = Ports([Port.Custom('<a>', 'Item', name='item'),
Port.Custom('[<a>]', 'List', name='list')])
outputs = Ports([Port.Custom('[<a>]', '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 Map node:
inputs = Ports([
Port.Custom('<a> -> <b>', 'Lambda Function', name='Function'),
Port.Custom('[<a>]', 'Argument List', name='List')])
outputs = Ports([
Port.Custom('[<b>]', '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('[<a>]', '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('[<a>]', 'Argument List', name='List', n=(3,))])
# Minimum of 3 ports up to a maximum of 5 ports.
inputs = Ports([
Port.Custom('[<a>]', '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('[<a>]', '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.
List nodes¶
Many nodes in the standard library also have a list version which simply loops over the input list(s) and output list(s) and does the work of the non-list version of the node for each item in the list.
Sympathy has a special helper function for creating such list nodes:
sympathy.api.node_helper.list_node_decorator
. For example, if the first
input and output ports should be turned into lists it can be used like this:
@list_node_decorator([0], [0])
class MyListNode(MySingleNode):
nodeid = 'some.node.id.list'
name = 'My List Node'
For more details see Node helper functions.
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, 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 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 Calculator List:
def update_parameters(self, 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 params:
params['same_length_res'] = self.parameters['same_length_res']
params['same_length_res']['value'] = False
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 qt2
QtWidgets = qt2.QtWidgets
class MyWidget(ParameterView):
def __init__(self, parameters, parent=None):
super().__init__(parent=parent)
self._parameters = parameters
greeting_edit = self._parameters['greeting'].gui()
button = QtWidgets.QPushButton('Test greeting')
button.clicked.connect(self.test_greeting)
layout = QtWidgets.QHBoxLayout()
layout.addWidget(greeting_edit)
layout.addWidget(button)
self.setLayout(layout)
def test_greeting(self):
QtWidgets.QMessageBox.information(
self, 'A greeting...', self._parameters['greeting'].value,
QtWidgets.QMessageBox.Ok)
class HelloWorldNode(synode.Node):
"""Prints a custom greeting to the node output."""
name = 'Hello World!'
nodeid = 'com.example.boblib.helloworld'
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 on
the input ports (e.g., when no node has been connected to the input port), we
need to be prepared to handle empty data for the port type. Before 5.0.0 it was
also required to check that the port was valid using is_valid()
on the
port, this is no longer the case.
If the widget keeps an internal model of the parameters it should define a
method called save_parameters
which updates node_context.parameters
.