Source code for node_fx_selector

# This file is part of Sympathy for Data.
# Copyright (c) 2016, Combine Control Systems AB
#
# SYMPATHY FOR DATA COMMERCIAL LICENSE
# You should have received a link to the License with Sympathy for Data.
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
from sympathy.utils import preview
from sympathy.api import qt2 as qt_compat

QtCore = qt_compat.QtCore


DOCS = """
The F(x) nodes have a similar role as the :ref:`Calculator` node. But where
the :ref:`Calculator` node shines when the calculations are simple expressions,
the F(x) nodes are better suited for more advanced calculations since the code
may span multiple lines, include statements and may even be kept in an external
python file.

Defining a function
===================
The python function that should be called by the node needs to be decorated
with ``fx.decorator``. The function should also take exactly two positional
arguments representing the input and output ports respectively. It is
recommended to name the arguments ``arg`` and ``res``. These variables are of
the same type as the input on port2. Consult the :ref:`API<datatypeapis>` for
that type to figure out relevant operations.

The argument to ``fx.decorator`` is a list of types (as shown in port tooltips)
that you intend your script to support. Each decorated function is only
available for use if the port type matches a listed type (e.g., `table` below).

Example::

    from sympathy.api import fx

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

        # My advanced calculation:
        more_spam = spam + 1

        res['more spam'] = more_spam


Script configuration
====================

The node has two different configuration modes which depend on the existence of
the :ref:`optional input-port <node_section_ports>` that provides a script
file path. If the port exists, the script is kept in the `external` script
file, otherwise it is kept `internal` in the node's configuration.

Internal script mode is suitable for shorter scripts, and it executes all
available functions.

External script mode is suitable for longer scripts and scripts shared between
different nodes, and it only executes selected functions (from all
available). You can place the python file anywhere, but it might be a good idea
to keep it in the same folder as your flow file or in a subfolder to that
folder. A quick way to get the skeleton for a function script is to use the
function wizard that is started by clicking *File->Wizards->New Function*.

.. versionchanged:: 6.0.0
   the optional input-port that provides the script path is no longer added to
   new nodes.

Editor
======

The internal editor is a basic code editor which supports syntax highlighting
and has basic support for indentation in python code, i.e. indentating using 4
spaces after pressing TAB and ENTER. It is included in Sympathy.

Script files can be edited using either the `internal` or the `external`
editor.

The external editor is the global editor in Sympathy, it is normally the
default program configured to open python files (.py) and only supports
external script files.

Debugging your functions
========================
When the functions are in an external file they can be debugged by following
the normal process for debugging nodes. See :ref:`general_debug` for
instructions.

List behavior
=============
The same function can be used with both :ref:`F(x)` and :ref:`F(x) List` nodes.
For example a function specified to run for type 'table' can be used with an
F(x) node connected to a single table or with an F(x) List node connected to a
list of tables. In the latter case the function will be executed once per item
in the list.

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

When the *Copy input* setting is enabled the entire input data structure will
get copied to the output before running the functions in the file. This is
useful when your functions should only add some data to the input data.

Alternative function definition
===============================
Another syntax for writing a "function" is to define a class which inherits
from ``fx.Fx``. The ``fx.Fx`` class 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. The field ``arg_types`` should contain the
list of types that you intend your script to support.

Example::

    from sympathy.api import fx

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

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

            # My advanced calculation:
            more_spam = spam + 1

            self.res['more spam'] = more_spam

This syntax is available mostly for backwards compatibility. For new functions
it is recommended to use the syntax with decorated functions.
"""


def _base_params():
    parameters = synode.parameters()
    editor = synode.editors.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.editors.code_editor())
    return parameters


class DatasourceWatcher(QtCore.QFileSystemWatcher):
    def __init__(self, filename, parent):
        super().__init__(filename, parent)
        self.fileChanged.connect(self.filename_changed)

    def filename_changed(self, filename):
        if filename not in self.files():
            self.addPath(filename)


def _exec_parameter_view(obj, widget_factory, node_context):

    preview_widget = preview.PreviewWidget(
        obj, node_context, node_context.parameters)
    fx_widget = widget_factory(node_context)
    filename = fx_widget.datasource_filename()
    if filename:
        watcher = DatasourceWatcher([filename], preview_widget)
        watcher.fileChanged.connect(preview_widget.update_preview)
    widget = preview.ParameterPreviewWidget(
        fx_widget, preview_widget)
    return widget


[docs] class Fx(synode.Node): __doc__ = DOCS name = 'F(x)' description = 'Apply arbitrary python function(s) to data.' nodeid = 'org.sysess.sympathy.data.fx' author = 'Erik der Hagopian' icon = 'fx.svg' parameters = _base_params() tags = Tags(Tag.DataProcessing.Calculate) related = ['org.sysess.sympathy.data.generic.fxlist'] inputs = Ports([ Port.Custom( 'datasource', 'Path to Python file with scripted functions.', name='port1', n=(0, 1, 0)), 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', preview=True)]) _cls = fx.Fx def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._base = FxSelector() def adjust_parameters(self, node_context): return self._base.adjust_parameters(node_context) def exec_parameter_view(self, node_context): return _exec_parameter_view( self, 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): __doc__ = DOCS name = 'F(x) List' description = 'Apply arbitrary python function(s) to each item of a List.' author = 'Erik der Hagopian' icon = 'fx.svg' nodeid = 'org.sysess.sympathy.data.generic.fxlist' parameters = _base_params() tags = Tags(Tag.DataProcessing.Calculate) related = ['org.sysess.sympathy.data.fx'] inputs = Ports([ Port.Custom( 'datasource', 'Path to Python file with scripted functions.', name='port1', n=(0, 1, 0)), Port.Custom( '[<a>]', 'List with data to apply functions on', name='port2')]) outputs = Ports([ Port.Custom( '[<a>]', 'List with function(s) applied', name='port3', preview=True)]) _cls = fx.Fx def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._base = FxSelectorList() def exec_parameter_view(self, node_context): return _exec_parameter_view( self, 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)