Source code for node_fx_selector

# Copyright (c) 2016, System Engineering Software Society
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the System Engineering Software Society nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.
# IN NO EVENT SHALL SYSTEM ENGINEERING SOFTWARE SOCIETY BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""The F(x) nodes all have a similar role as the :ref:`Calculator List`
node. But where the :ref:`Calculator List` node shines when the calculations
are simple, the f(x) nodes are better suited for more advanced calculations
since the code is kept in a separate python file. You can place this python
file anywhere, but it might be a good idea to keep it in the same folder as
your workflow or in a subfolder to that folder.


The script file
^^^^^^^^^^^^^^^
When writing a "function" (it is actually a python class) you need to inherit
from ``Fx``. The ``Fx`` provides access to the input and output
with ``self.arg`` and ``self.res`` respectively. These variables are of the
same type as the input on port2. Consult the API for that type to figure out
relevant operations.

The field ``arg_types`` is a list containing string representations of types
(as shown in port tooltips) that you intend your script to support and
determines the types for which the function is available.

Example using class to define calculation::

    from sympathy.api import fx

    class MyCalculation(fx.Fx):
        arg_types = ['table']

        def execute(self):
            spam = self.arg['spam']

            # My super advanced calculation that totally couldn't be
            # done in the :ref:`Calculator Lists` node:
            more_spam = spam + 1

            self.res['more spam'] = more_spam


Alternative example using decorator::

    from sympathy.api import fx

    @fx.decorator(['table'])
    def my_calculation(arg, res):
        spam = arg['spam']

        # My super advanced calculation that totally couldn't be
        # done in the :ref:`Calculator Lists` node:
        more_spam = spam + 1

        res['more spam'] = more_spam


The same script file can be used with both :ref:`F(x)` and :ref:`F(x) List`
nodes.

A quick way to get the skeleton for a function is to use the function wizard
that is started by clicking *File->Wizards->New Function*.


Script without separate file
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For short scripts that are not intended to be shared between different nodes it
can convenient not having to create an external file.  To enable this
feature, the datasource port first needs to be deleted.  Once there is no
datasource port, the node will show a code editor when configured.

The format is the same, the only exception is that all calculations (matching
input type) will be executed.


Debugging your script
^^^^^^^^^^^^^^^^^^^^^
F(x) scripts can be debugged in spyder by following these simple steps:

#. Open the script file in spyder and place a breakpoint somewhere in the
   execute method that you want to debug.
#. Go back to Sympathy and right-click and choose *Debug* on the f(x) node with
   that function selected.
#. Make sure that the file *node_fx_selector.py* is the active file in spyder
   and press *Debug file* (Ctrl+F5).
#. A third python file will open as the debugging starts. Press *Continue*
   (Ctrl+F12) to arrive at the breakpoint in your f(x) script. From here you
   can step through your code however you want to.


Configuration
^^^^^^^^^^^^^
When *Copy input* is disabled (the default) the output table will be empty
when the functions are run.

When the *Copy input* setting is enabled the entire input table will get
copied to the output before running the functions in the file. This is useful
when your functions should only add a few columns to a data table, but in this
case you must make sure that the output has the same number of rows as the
input.

"""
from __future__ import (print_function, division, unicode_literals,
                        absolute_import)
from sympathy.api import node as synode
from sympathy.api import fx
from sympathy.api.nodeconfig import Port, Ports
from sylib.fx_selector import (FxSelector, FxSelectorList)
from sympathy.api.nodeconfig import Tag, Tags


def _base_params():
    parameters = synode.parameters()
    editor = synode.Util.multilist_editor()
    parameters.set_boolean(
        'copy_input', value=False, label='Copy input',
        description=('If enabled the incoming data will be copied to the '
                     'output before running the nodes.'))
    parameters.set_list(
        'selected_functions', value=[], label='Select functions',
        description=('Choose one or many of the listed functions to apply to '
                     'the content of the incoming item.'), editor=editor)
    parameters.set_string(
        'code',
        label='Python code',
        description='Python code block, input is called arg, output res.',
        value="""from sympathy.api import fx

@fx.decorator(['<a>'])
def function(arg, res):
    raise NotImplementedError('Replace with your function')
""",
        editor=synode.Util.code_editor().value())
    return parameters


[docs]class Fx(synode.Node): """ Apply functions to an item. Functions based on Fx will be invoked once on the item. The functions available are the ones where ``arg_types`` of the function matches the type of the item port (port2). :Ref. nodes: :ref:`F(x) List` """ name = 'F(x)' description = 'Select and apply functions to item.' nodeid = 'org.sysess.sympathy.data.fx' author = 'Erik der Hagopian' version = '1.0' icon = 'fx.svg' parameters = _base_params() tags = Tags(Tag.DataProcessing.Calculate) _cls = fx.Fx inputs = Ports([ Port.Custom( 'datasource', 'Path to Python file with scripted functions.', name='port1', n=(0, 1, 1)), Port.Custom( '<a>', 'Item with data to apply functions on', name='port2')]) outputs = Ports([ Port.Custom( '<a>', 'Item with the results from the applied functions', name='port3')]) def __init__(self): super(Fx, self).__init__() self._base = FxSelector() def adjust_parameters(self, node_context): return self._base.adjust_parameters(node_context) def exec_parameter_view(self, node_context): return self._base.exec_parameter_view(node_context) def execute(self, node_context): self._base.execute(node_context, self.set_progress)
[docs]class FxList(synode.Node): """ Apply functions to a list of items. Functions based on Fx will be invoked once for each item in the list with each item as argument. The functions available are the ones where ``arg_types`` of the function matches the type of the individual items from the list port (port2). :Ref. nodes: :ref:`F(x)` """ name = 'F(x) List' description = 'Select and apply functions to List.' author = 'Erik der Hagopian' version = '1.0' icon = 'fx.svg' nodeid = 'org.sysess.sympathy.data.generic.fxlist' parameters = _base_params() tags = Tags(Tag.DataProcessing.Calculate) _cls = fx.Fx inputs = Ports([ Port.Custom( 'datasource', 'Path to Python file with scripted functions.', name='port1', n=(0, 1, 1)), Port.Custom( '[<a>]', 'List with data to apply functions on', name='port2')]) outputs = Ports([ Port.Custom( '[<a>]', 'List with function(s) applied', name='port3')]) def __init__(self): super(FxList, self).__init__() self._base = FxSelectorList() def exec_parameter_view(self, node_context): return self._base.exec_parameter_view(node_context) def adjust_parameters(self, node_context): return self._base.adjust_parameters(node_context) def execute(self, node_context): self._base.execute(node_context, self.set_progress)