7. GUI Development

The Python Application Tutorial is hardly a complete application. It is intended to introduce the most basic WAF structure needed to produce a working product. For very simple applications this could suffice, but most likely it will fall short.

This chapter will introduce a most complex application and go into details of implementation.

7.1. CLI Options and Arguments

We recommend the use of click to parse arguments for the application. This document does not intended to teach you how to use click. There are several resources available for that.

Tip

To learn more about click, please refer to Click Quickstart

Taurus includes several predefined command line options, so that every application matches in style. There are basic options we recommend you to use are located in taurus.cli.common

Some examples:

  • --log-level to set the log level of the application

  • --config to set the configuration file

  • --polling-period to set the polling period

When capturing options from the click parser, if you have for one option, a predetermined ammount of posibilities, prefer the use of enumerations instead of strings.

import click
import enum

class RepositoryOptions(str, enum.Enum):
    RTR = "rtr"
    PTS = "pts"
    OLDB = "oldb"

@click.command("appentry")
@taurus.cli.common.log_level
@click.argument("selected_repo", type=click.Choice(RepositoryOptions))
def appentry(selected_repo: RepositoryOptions, log_level):
    print("Selected Repository {}".format(selected_repo))
    pass


if __name__ == "__main__":
    appentry()

– Adapted from EnumChoice

The click module can work as a single command, or multiple commands. Single command example would be ls. It executes one task, and for that it collects options and arguments. A click-based application can also have multiple commands, like git. Git has an entrypoint command git and several commands like git commit or git add, sharing options, or having command-specific options and arguments.

  1. Decide if you application is to be a single or multiple command and list them.

  2. Add help for each command in the docstring

  3. Enumerate their options and argument

  4. Set their datatypes

  5. Set default values

  6. Accept each option and argument as argument to the method that handles the command

  7. Save all of these into a temporary object

  8. Parse configuration files.

  9. Compare the values from configuration files against the ones in the temporary object. Keep them as you defined which one has priority.

  10. Create a final ParsedConfiguration object, and pass this object as reference to your MainWindowImpl class.

7.2. Data Manipulation and Taurus

Taurus Model and Widgets offers a quick way to declare databinding through the UI definition. This comes with some restrictions, as code cannot be introduced in the UI definition.

taurus.qt.qtgui.base.taurusbase.TaurusBaseComponent . insertEventFilter() provides a way to manipulate values reported by the polling or subscription mechanisms supported by any Taurus Model Plugin.

The example below shows how to use EventFilters.

 1#!/usr/bin/env python3
 2
 3import sys
 4from taurus import Attribute
 5from taurus.qt.qtgui.application import TaurusApplication
 6from taurus.qt.qtgui.display import TaurusLabel
 7from taurus.core import TaurusEventType
 8from taurus.core.taurusbasetypes import TaurusAttrValue
 9from taurus.core import DataType
10from taurus.external.qt import Qt
11
12
13class EventValueMap(dict):
14    """A filter destined to change the original value into another one
15    according to a given map. Example:
16
17        filter = EventValueMap({1:"OPEN", 2:"CHANGING", 3:"CLOSED"})
18
19    this will create a filter that changes the integer value of the event
20    into a string. The event type is changed according to the python type in
21    the map value.
22
23    For now it only supports simple types: str, int, long, float, bool
24    """
25
26    def __call__(self, s: Attribute, t: TaurusEventType, v: TaurusAttrValue):
27        # It needs to be Change (subscription) or Periodic (polling) event
28        if t not in (TaurusEventType.Change, TaurusEventType.Periodic):
29            return s, t, v  # Otherwise, we do not apply mapping
30        if v is None:  # If there is not value, no mapping can be applied
31            return s, t, v  # Otherwise, we do not apply mapping
32
33        # get from dict: applies transformation
34        newvalue = self.get(v.rvalue)
35        if(newvalue):
36            v.rvalue = newvalue
37
38        v.type = DataType.from_python_type(type(v.rvalue))
39        return s, t, v
40
41
42if __name__ == "__main__":
43    app = TaurusApplication()
44
45    panel = Qt.QWidget()
46    layout = Qt.QVBoxLayout()
47    panel.setLayout(layout)
48
49    w1, w2, w3, w4 = TaurusLabel(), TaurusLabel(), TaurusLabel(), TaurusLabel()
50    f1 = EventValueMap({1: "OPEN", 2: "CHANGING", 3: "CLOSED"})
51    f2 = EventValueMap({False: "ABSENT", True: "PRESENT"})
52    f3 = EventValueMap({True: "TRUE", False: "FALSE"})
53    f4 = EventValueMap({1: 1001, 2: 1002, 3: 1003, 4: 1004, 5: 1005, 6: 1006})
54    w1.insertEventFilter(f1)
55    w2.insertEventFilter(f2)
56    w3.insertEventFilter(f3)
57    w4.insertEventFilter(f4)
58    layout.addWidget(w1)
59    layout.addWidget(w2)
60    layout.addWidget(w3)
61    layout.addWidget(w4)
62    w1.model, w1.bgRole = 'cii.oldb:///cut/demoservice/instance1/uint64-scalar-sin', ''
63    w2.model, w2.bgRole = 'cii.oldb:///cut/demoservice/instance1/boolean-scalar-fixed', ''
64    w3.model, w3.bgRole = 'cii.oldb:///cut/demoservice/instance1/boolean-scalar-twosecs', ''
65    w4.model, w4.bgRole = 'cii.oldb:///cut/demoservice/instance1/uint64-scalar-sin', ''
66
67    # First read is not mapped.
68    # Could it be that first read is not triggering an event?
69
70    panel.show()
71    sys.exit(app.exec_())

A class EventValueMap is declared. It uses a dict as method of translation between the source value and the returned value. There are three types of Events taurus sends: Configuration, Periodic, and Change. If we want to alter values, then we need to catch events in the Periodic (Polling) and Change (Subscription) types (see line 28).

If the type of the data changes, it is important that the value also has its datatype changed. (see line 38).

Installing the filter needs to be done using code. Developers may still define the GUI in a declarative manner (i.e.: UI files), but the creation of the Filtel object and installing the filter needs to be done as shown in lines 50 and 54.

Tip

An EventFilter is a collections.abc.Callable. Therefore you can also use a function or class method.

As an alternative, a developer may implement their own widget that uses Taurus capabilities. For this, please follow custom-widgets

7.3. Data Units and Conversion

The Taurus library uses Quantity (from the python pint library) to express a datapoint from the OLDB. Please include the original unit in the metadata of the datapoint.

Then you can convert the rvalue to other units using the method pint.Quantity.to(), and pass as argument the new unit. The Pint library has conversions for common units already available.

>>> import taurus
>>> taurus.Attribute("cii.oldb:///elt/telif/ccs/target_observed_altaz")
MainThread WARNING 2023-11-09 08:16:45,488 TaurusRootLogger: epics scheme not available:
ModuleNotFoundError("No module named 'epics'")
>>> lala = taurus.Attribute("cii.oldb:///elt/telif/ccs/target_observed_altaz")
>>> lala
TaurusCiiAttribute(cii.oldb:///elt/telif/ccs/target_observed_altaz)
>>> lala.rvalue
<Quantity([0.78539816 0.78539816], 'radian')>
>>> lala.rvalue.to("degrees")
<Quantity([45. 45.], 'degree')>

Please take a look at Pint Tutorial for more information on Pint, or at taurus.core.units to see how taurus makes the Quantity and UnitRegistry classes available.

If a conversion is not available, you can add it to the UnitRegistry.

7.4. WAF and Project Structure for Bigger Applications

  • use python package structure

  • one subpackage for widgets

  • one subpackage for comms

  • one subpackage for domain specific code if needed

7.5. Connecting to MAL or OLDB

Try to reuse MalAdapter and OldbAdapter.

If you don’t remember to do it async, and push to other threads, because you will have to wait for connection timeout otherwise.

7.6. Logging

Remember to always include the CII Logger to $CII_LOGS. Any other log output will be lost during operations.

7.7. Widgets and Application properties as Configurable

Taurus provides a base class that allows to register properties within classes. Then an application can save all registered properties to a file, creating a state.

7.8. Complex UI

If a UI is too comple, refactor it into smaller Widgets. If the widget can be reused, perfect. If not, then it do it just for maintenance sake.

Use always UI file first, customize through code second.

UIs should always have a starting state. Think what that state is. (enable/disable, values at 0?, etc, certain actions enabled, other disabled)?

7.9. Declarative UI

7.10. Widget Promotion

7.11. Auto Slot Connection

name clashes

7.12. Declarative Databinding

7.13. Models

The MVC pattern can be used and applied to your application in many different forms.

CUT only includes a Model for single attributes. Getting multiple values and presenting them in complex ways is not possible to make it in a generic form.

The developer of complex apps will end up creating model. This does not mean that they will look similar all the time, or that the datasource is the same. You will need to adapt.

QStardardItemModel is the easiest.

A List can also be a model. But I suggest to use a QStandardItemModel because you can later user the ModelMapper.

Remember that you can also use taurus.Attribute to get values without doing declarative databinding.

7.14. Taurus Model Plugin Development

7.15. Debugging

7.15.1. Taurus Log Level

Any taurus command can change its logging output level using the option:

[eltdev@srtc1 cut]$ taurus --help
Usage: taurus [OPTIONS] COMMAND [ARGS]...

  The main taurus command

Options:
  --log-level [Critical|Error|Warning|Info|Debug|Trace]
                                  Show only logs with priority LEVEL or above
                                  [default: Info]

A good first step to debug a TaurusWidget or Scheme Plugin is to enable trace level logs and see what is going on.

7.15.2. OLDB GUI

Sometimes is can be good to compare the values presented in Taurus with the one in the database. You can do this using the oldb-gui. Its will also present you the metadata of the datapoint, which is used by cii.oldb plugin to fill more details into the taurus.core.taurusattribute.TaurusAttribute object.

../_images/oldb-gui_01.png

OLDB GUI. In the left there is a browsable tree view of the database contents, and the user can select a datapoint. On the right hand side are the details of the selected datapoint.

The OLDB GUI also allows you to subscribe to datapoints changes.

7.15.3. GDB

Since python is programmed in C, and we are using several bindings, some errors can come from the libraries developed in C++. To obtain more information, a developer may still use gdb:

$ which <script_name>
$ gdb
...
(gdb)$ file python
(gdb)$ run <return_of_which_command_above>

7.15.4. Python faulthandler

In case needed, you can get a more complete error or exception output using this:

python3 -q -X faulthandle [script_name]

You can use it in the interactive script console, or while executing a python file.

7.15.5. Python Debugger

Since Python 3.7, the breakpoint() method is part of the language. You can add them to your code at any point, and the python debugger will start immediately.

You can run any script with the python debugger.:

$ python3 -m pdb <path_to_python_script.py>
...
(Pdb)$ run
(Pdb)$ continue

7.15.6. Profiler

We recommend to use Plop. Plop comes in two modules, one needed for collection of data, and the other one use to present the data. Here is how to use it:

$ su - -c 'pip install plop'
$ python3 -m plop.collector <script.py>
$ python3 -m plop.viewer --datadir=profiles

The output of Plop is a very nicely constructure graph view of the calls. The size of the bubble indicate the ammount of time spend of the method, and the arrows indicate the backtrace to it.

../_images/profiler_01.png

Plop viewer in action. It presents the information in a dynamic and interactive graph plot.