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.
Decide if you application is to be a single or multiple command and list them.
Add help for each command in the docstring
Enumerate their options and argument
Set their datatypes
Set default values
Accept each option and argument as argument to the method that handles the command
Save all of these into a temporary object
Parse configuration files.
Compare the values from configuration files against the ones in the temporary object. Keep them as you defined which one has priority.
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.
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.