Skip to content
This repository has been archived by the owner on Mar 17, 2021. It is now read-only.

Best Practices for Taurus 4

reszelaz edited this page Feb 3, 2020 · 10 revisions

Best practices for Taurus 4

The following is a set of recipes of current "best practices" (for Taurus v4.6). It assumes that you are already familiar with Taurus (at least at v3)

This is the materials prepared by @cpascual, @mrosanes and @cfalcon for an internal ~6h seminar which took place at ALBA in 2019-04-03 and it has received minor updates.


Installation and dependencies

Debian

# for python 2 (with qt4 and qt5)
apt-get install python-taurus python-taurus-pyqtgraph python-qwt5-qt4 python-pyqt5

# for python 3
apt-get install python3-taurus python3-taurus-pyqtgraph python3-pyqt5

Using conda to install development/test environments

See instructions for py3+Qt5

See instructions for py2+Qt4

Selecting a Python Qt binding

Since v4.5, taurus will adapt to whatever Qt binding is already loaded. E.g., if we run:

import PySide.QtCore
from taurus.qt.qtgui.display import  TaurusLabel

we get:

MainThread     INFO     2019-03-27 20:10:02,615 TaurusRootLogger: Using PySide (v1.2.2 with Qt 4.8.7 and Python 2.7.10rc1)```

If no binding is previously loaded the first time taurus.external.qt is imported, the binding will be selected according to the QT_API environment variable:

QT_API=pyqt5 taurusform 
MainThread     INFO     2019-03-27 19:54:18,039 TaurusRootLogger: Using PyQt5 (v5.7 with Qt 5.7.1 and Python 2.7.13)

Supported values are: pyqt, pyqt5, pyside2, pyside

If the QT_API environment variable is not declared, Taurus will fall back to the value set in taurus.tauruscustomsettings.DEFAULT_QT_API.

For more details, see TEP18

General good practices (most of them applicable to taurus3 as well)

An example of good practices

Consider the following example of a widget implementation:

from __future__ import print_function
import os
import sys
import taurus
from taurus.external.qt import Qt, uic
from taurus.qt.qtgui.application import TaurusApplication


class Foo(Qt.QWidget):
    """A Foo Widget that blah blah..."""    

    def __init__(self, parent=None):

        # call the parent class init
        Qt.QWidget.__init__(self, parent=parent)
        
        # load the UI from ./ui/Foo.ui
        uipath = os.path.join(os.path.dirname(__file__), "ui", "Foo.ui")
        uic.loadUi(uipath, self)

        # connect signals  (note: myButton is defined in Foo.ui)
        self.myButton.clicked.connect(self.onButtonClicked)

    def onButtonClicked(self):
        """slot to be called when the button is clicked"""
        dev = taurus.Device("sys/tg_test/1")
        print("The status of the test device is {}".format(dev.status()))


if __name__ == "__main__":
    app = TaurusApplication(cmd_line_parser=None)
    w = Foo()
    w.show()
    sys.exit(app.exec_())

Good practices to note:

  1. It follows PEP8: <80 char/line, double quotes for docstrings,... note that camelCase is used for method names, but this is within a PyQt context)
  2. It forces the use of print_function (for py2+py3 compatibility)
  3. It uses TaurusApplication instead of QApplication
  4. It loads the .ui file dynamically instead of using a compiled ui_Foo.py (it uses uic.loadUi, but using the UILoadable decorator from taurus.qt.qtgui.util.ui would also be ok)
  5. It uses os.path to create paths (platform independent)
  6. It uses os.path.dirname(__file__) to learn where it is installed (not hardcoding and not using ".", which refers to the execution path, not the installation path)
  7. It uses new-style signals (more later)
  8. It uses modern-style pyhton string formatting
  9. It does not import PyTango: it uses taurus.Device instead of a PyTango.DeviceProxy, which could produce problems on exit and would defeat some taurus optimizations.

Other stuff to note:

  1. It uses taurus.external.qt (although using an specific binding would also be ok, see below)
  2. It uses the consolidated Qt submodule. Using QtGui would also be ok (see below)
  3. It uses an explicit call to the parent class constructor. Whether this is the best approach or if using super(Foo, self).__init__() would be better is not yet decided, and it may depend on many details. But, at least, DON'T use the old call__init__() and call__init__wo_kw() methods.

PyQt5 vs taurus.external.qt

Should we import the Qt submodules directly from a specific binding (PyQt4 / PyQt5 / ...) or should we use taurus.external.qt?

From TEP18:

Until v 4.4, we have recommended taurus users to always import QtCore, QtGui, etc from taurus.external.qt. But with the improved support of multiple bindings provided by this TEP, this recommendation can be revised as follows:

  • For code that is going to be part of Taurus (and consequently potentially used as library by other other people), Qt, QtGui, QtCore, etc. should still be imported from taurus.external.qt. The same applies to plugins to taurus that intend to be used as a library (otherwise, the plugins should be capable of failing gracefully in case of incompatible bindings).

  • For an end-user application based on taurus it is probably better to import directly from a specific binding (PyQt5 is the best supported) and let taurus to adapt to that choice. In this way, one can write idiomatic code that better matches the chosen binding. Using the taurus.external.qt shim is also possible if one wants to make the code binding-agnostic, but in that case one must keep in mind that the resulting code will be less idiomatic and that the shim's API may be eventually altered to better fit with taurus own requirements (and that those changes may not be aligned with the application needs).

Qt vs QtGui, QtCore

Should we import the consolidated Qt submodule or QtGui, QtCore, etc?

It is up to you. In taurus we tend to use the consolidated module. It makes it easier to write code that works simultaneously for Qt4 and Qt5 since it is not affected by the submodules reorganization (e.g. the split of QtGui into QtGui and QtWidgets). On the other hand, in small applications, it make add a slight performance penalty (but since taurus will import Qt anyway, in most cases it will already be loaded)

Use setuptools and bumpversion

All pyhton packages should:

TIP: You can autogenerate a setup.py with PyCharm (Tools -> create setup.py)

TIP: See a bumpversion.cfg template: Example of bumpversion file

TIP: You can use cookiecutter for creating stubs of your projects already supporting many features. For example, to create a stub with support for setuptools, bumpversion, pypi uploads, unit testing, sphinx, travis.yml, etc., just do:

sudo apt install cookiecutter
cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git

Taurus 3 compatibility layer in Taurus 4

See Taurus 3.x to 4.x migration guide

The major version change from 3 to 4 implies that full backwards-compatibility is not preserved.

Still, we provide a backwards-compatibility layer that tries to make taurus 3 code work in Taurus4 whenever it is possible, and issue deprecation warnings to guide the developer in converting.

TIP: the verbosity of the deprecation warnings can be controlled with the value of _MAX_DEPRECATIONS_LOGGED (in the tauruscustomsettings module). By default, it is 1 (each deprecation is printed only once, and a summary is given at the end. Put it to 0 to get only the summary. Put it to None to print all warnings, or put it to -1 to raise exceptions instead of warnings (good when refactoring)

DON'T... attempt to maintain a single codebase for taurus3 and taurus4

Since the compatibility layer does a lot of work already, you could be tempted to try to support taurus3 and 4 simultaneously in your application: We strongly discourage it

In practice, anything but trivial code will be very difficult to maintain for working in v3 and v4 simultaneously.

We are doing it in sardana, and it is a PITA because:

  • you cannot avoid deprecation warnings
  • you cannot use many features of taurus4 that would simplify your life
  • you end up with forks in the code
  • your code will stop working once the deprecation layer is removed (think of taurus 5)

Instead, we recommend to:

  1. create a separate branch for a "taurus4" version of your code
  2. work step-by-step on refactoring your "taurus4" version until it works without deprecation warnings.
  3. if possible, freeze the "taurus3" version into maintenance mode (i.e., develop new features only for the "taurus4" version and cherry-pick only critical bug fixes or features into the "taurus3" branch)

TIP: have a look at the API changes table to learn about how to refactor the code for taurus 4

Scheme-agnosticism (avoid tango-centric code)

Keep in mind: Taurus is not just Tango:

  • taurus.core is completely scheme-agnostic.

  • taurus.qtgui can be used without Tango, and the main widgets (TaurusForm, TaurusPlot, TaurusLabel,...) are scheme agnostic, but some widgets or some features may still be available only for Tango.

Reasons not to write tango-centric code

Even if you are writing a widget/application that will just interact with a Tango system, you should try hard to write scheme-agnostic code because:

Some common wrong assumptions about model names

  1. an attribute name contains the device name
    • FALSE: e.g., in eval, the device name is likely to be implicit, and in EPICS, there may even not be a unique attr->device association
  2. if not explicit, the scheme name is "tango"
    • FALSE: it is controlled in tauruscustomsettings.DEFAULT_SCHEME (which defaults to "tango")
  3. the separator between a scheme name and the rest is ://
    • FALSE: the separator is the first occurrence of :. The // is actually part of the authority name (e.g. tango:a/b/c, not tango://a/b/c)
  4. the name of an attribute (or device, or...) does not contain ?
    • FALSE: some schemes may use queries for their models (e.g. taurus_tangoarchiving's attribute nametgarch:a/b/c/d?db=tdb?t0=-5d)
  5. The fragment is part of an attribute name
    • FALSE: strictly speaking, the fragment (whatever comes after an unescaped # in an URI) is not part of the URI itself: in tango:a/b/c/d#wvalue, the attribute name is just tango:a/b/c/d, and #wvalue is a "hint" that may (or not) be honored by the client side. For example, the taurus core does not store the fragment (it just keeps model singletons), but the widgets do store (and may act on) the fragment name.
  6. Model names are case-insensitive
    • FALSE: It depends on the scheme (e.g., eval or epics are case-sensitive). It can be checked with taurus.Factory(scheme).caseSensitive

Agnostic helpers

taurus.core provides an API to help you writing scheme-agnostic code:

  • taurus.Authority()
  • taurus.Device()
  • taurus.Attribute()
  • taurus.Object()
  • taurus.Factory()
  • taurus.Manager()
  • taurus.isValidName()
  • taurus.getValidTypesForName()
  • taurus.getSchemeFromName()

Learn this API by reading the docs on Model Access

Name validators

Taurus4 introduced model name validators:

  • They are provided by each scheme for each element type (e.g. TangoAttributeNameValidator, EvaluationDeviceNameValidator, ...)
  • They can be obtained from the scheme Factory (e.g. Factory('eval').getAttributeNameValidator)
  • They provide:
    • .isValid(name) to validate model names
    • .getUriGroups(name) to parse the URI and retrieve parts of it
    • .getNames(name) to compute the full, normal and simple names from a given name.

Some examples:

  • Validate an Evaluation attribute:

    v = taurus.Factory('eval').getAttributeNameValidator()
    v.isValid('eval:rand(5)')  # --> True
  • Obtain the authority associated to a tango attribute name (without creating the atribute):

    v = taurus.Factory('tango').getAttributeNameValidator()
    name = 'tango://tghost:10000/a/b/c/d'
    urigroups = v.getUriGroups(name)  # if name is not a valid tango attr, we get None 
    print(urigroups) # -->
    # {'__STRICT__': True,
    #  '_devalias': None,
    #  '_devslashname': 'a/b/c',
    #  '_shortattrname': 'd',
    #  'attrname': '/a/b/c/d',
    #  'authority': '//tghost.mydomain.org:10000',
    #  'cfgkey': None,
    #  'devname': 'a/b/c',
    #  'fragment': None,
    #  'host': 'tghost.mydomain.org',
    #  'path': '/a/b/c/d',
    #  'port': '10000',
    #  'query': None,
    #  'scheme': 'tango'}
    auth = urigroups['authority'] # if authority is not explicit in `name`, we get None
  • Get the full name of a device (without creating it)

    scheme = taurus.getSchemeFromName(name)
    v = taurus.Factory(scheme).getDeviceNameValidator()
    full_name, _, _ = v.getNames(name) 
    
    # # and now, we can, e.g., find the authority from it 
    # **even if it was not explicit in `name`**
    auth = v.getUriGroups(full_name)['authority']

Some common BAD practices...

DON'T... count slashes

e.g., to find if a given model name corresponds to a device or an attribute, an old code could do:

n_slashes = name.split('://')[-1].count('/')
if n_slashes == 0 or n_slashes == 2:  # 'alias' or 'a/b/c'
    model_type = 'device'
elif nslashes == 1 or n_slashes == 3: # 'alias/attr' or 'a/b/c/d'
    model_type = 'attribute'

... but that is tango-centric (e.g., it won't work with eval:rand(5)), so instead one could do:

try:
    taurus.Attribute(name)
    model_type = 'attribute'
except:
    taurus.Device(name)
    model_type = 'device'

... which is scheme-agnostic but has the problem that it attempts to create the model objects (expensive and may fail if they are not accessible)

Instead, one should do:

from taurus.core import TaurusElementType
_t = taurus.getValidTypesForName(name)[0]
model_type = TaurusElementType[_t]

TIP: Note that the isValidName helper may also be useful to determine if something is an attribute (or a device or an authority)

from taurus.core import TaurusElementType as ET
from taurus.core.taurushelper import isValidName

isValidName('tango:foo')  # --> True
isValidName('tango:a/b/c', [ET.Attribute])  # --> False
isValidName('tango:a/b/c', [ET.Attribute, ET.Device])  # --> True

DON'T... parse model names assuming tango rules

For example, if you are dealing with Tango attributes, instead of:

dev_name = attr_name.rsplit('/', 1)[0]

Instead, do:

v = taurus.Factory('tango').getAttributeNameValidator()
urigroups = v.getUriGroups(attr_name)
dev_name = urigroups["devname"]  # in tango, attr_name contains the dev name

DON'T compare names using lowercase or non-full names

The following code to compare if two names correspond to the same attribute assumes case-insensitivity and may also fail if the names are not both full names:

if (name1.lower() == name2.lower()):
    (...)

So, instead you can do:

def get_full_name(name):
    scheme = taurus.getSchemeFromName(name)
    v = taurus.Factory(scheme).getAttributeNameValidator()
    return v.getNames(name)[0]

if get_full_name(name1) == get_full_name(name2):
    (...)

... or, if you do not mind creating the model objects, you can make use of the fact that models are singletons:

attr1 = taurus.Atribute(name1)
attr2 = taurus.Atribute(name2)
if attr1 is attr2:  # attrs are singletons!
    (...)

DON'T... use normal_name or simple_name as keys

A model has 3 names... but only the full name is unambiguous:

  • full. (e.g.tango://host.domain:1234/a/b/c/d) This is the only that is guaranteed to be unambiguous.
  • normal (e.g. a/b/c/d, or //host:1234/a/b/c/d if host is not your default Tango DB). This is the full name minus the optional parts if they coincide with the current system's default values. It is only unambiguous as long as it is used in the same system and its configuration is not changed.
  • simple (e.g. d). This is just something that can be useful in many situations as a short descriptor of the attribute.

Therefore if you want to store a dictionary where the keys are model names, it is highly likely that you should use full names as the keys.

DON'T... use normal_name as a way of removing the scheme or the authority parts of a name

Consider the following snippet:

for name in ('tango://controls01:10000/sys/tg_test/1',
             'tango://controls02:10000/sys/tg_test/1'):
    print(taurus.Device(name).getNormalName())

Its output may be surprising:

sys/tg_test/1
//controls02.cells.es:10000/sys/tg_test/1

... until you realize that:

$> echo $TANGO_HOST
controls01:10000

Working with units (the pint module)

See the working with quantities section of the Taurus 3.x to 4.x migration guide

Taurus4 specifies that all numerical values (read values, write values) and associated properties (limits, warning levels, etc.) are pint Quantity objects (instead of just int or float).

Taurus provides support via taurus.core.units

Things to keep in mind about Quantities

  • they have .magnitude and .units
  • they are associated to a pint UnitRegistry. Taurus' unit registry and its associated Quantity factory can be imported with:
    from taurus.core.units import UR, Q_  # Q_ is an alias for Quantity
  • they can be created by using the Quantity factory (e.g. Q_("3 mV")) or by multiplying a number with a unit from the registry (e.g. 5 * UR.meters)
  • they can be converted to compatible units: Q_("3 inches").to("mm")
  • they can be used directly without need of conversion: Q_("1 nm") + Q_("4 angstroms")
  • unitless quantities are dimensionally-compatible with python scalars: Q_(3) + 2

Quantities and Tango

  • In Tango, the units are just free-content strings in the attribute configuration. They apply to both the read and write values, as well as the limits.
  • In taurus.core.tango when reading a numerical attribute, the unit string is parsed and it is used to construct the quantities of the rvalue, wvalue and the limits of the given attribute.
  • If the unit is not recognized by pint, a warning is issued and the attribute is considered unitless.
  • when writing to the attribute, the argument of .write() can be
    • a quantity: which will be transparently converted to the units in the Tango DB
    • a python scalar which assumed to already match the units of the attribute (just as in taurus3)

DON'T... operate with magnitudes (use Quantities)

For example, the following taurus 3 code (in which we assume that ampli is in meters)...

v = taurus.Attribute('sys/tg_test/1/ampli').read()
foo = 5 + v.value  # here "5" is implicitly assumed to mean "5 meters"

... would raise the following warning in taurus 4:

DeprecationWarning: _get_value is deprecated since 4.0. Use .rvalue instead

A quick but not recommended refactoring to avoid the deprecation warning could be:

v = taurus.Attribute('sys/tg_test/1/ampli').read()
foo = 5 + v.rvalue.magnitude

That is exactly what the automated backwards compatibility layer already does for you. However, the recommended refactoring should use Quantities rather than magnitudes, e.g:

from taurus.core.units import UR  # import the taurus unit registry
v = taurus.Attribute('sys/tg_test/1/ampli').read()
foo = 5 * UR.meters + v.rvalue  # use explicit units

Taurus State vs Tango State

In taurus3 the device states were those of PyTango.DevState, and were accessed with device.getState().

In Taurus 4, a scheme-agnostic set of states (Ready, NotReady and Undefined) was introduced with TaurusDevState, and it is accessed via the TaurusDevice.state property.

TIP: you can still access the Tango State of a TangoDevice by reading the value of its state tango attribute: TangoDevice.stateObj.read().rvalue

TIP: some widgets (leds, labels, ...) that previously displayed Tango States were made scheme-agnostic and now use TaurusDevState... with the consequence that they offer less fine-grained info. If the richer set of Tango states are required (and the price of tango-centrism is assumed), one can always use a Tango specialization of those widgets.

Adapting state-related APIs from taurus3 to taurus4

From the API changes table:

taurus 3 taurus 4
TangoDevice.getState TangoDevice.stateObj.read().rvalue tango or TaurusDevice.state agnostic
TangoDevice.getStateObj TangoDevice.stateObj tango or .factory.getAttribute(state_full_name) agnostic
TangoDevice.getSWState TangoDevice.state
TangoDevice.getValueObj TangoDevice.state agnostic or stateObj.read tango
TangoDevice.getDisplayValue TangoDevice.state().name
TangoDevice.getHWObj TangoDevice.getDeviceProxy
TangoDevice.isValidDev (TangoDevice.getDeviceProxy() is not None)
TangoDevice.getDescription TangoDevice.description

Mapping between Tango and Taurus DevStates

  • ON, OFF, CLOSE, OPEN, INSERT, EXTRACT, MOVING, STANDBY, RUNNING, INIT ->Taurus Ready
  • FAULT, DISABLE, INIT -> Taurus NotReady
  • UNKNOWN -> Taurus Undefined

For the color mappings, see http://taurus-scada.org/users/ui/ui_colors.html

Model fragments

From RFC3986:

A fragment identifier component is indicated by the presence of a number sign ("#") character and terminated by the end of the URI.

and:

Fragment identifiers have a special role in information retrieval systems as the primary form of client-side indirect referencing (...) to specifically identify aspects of an existing resource that are only indirectly provided by the resource owner (...) the fragment identifier is not used in the scheme-specific processing of a URI; instead, the fragment (...) is dereferenced solely by the user agent, regardless of the URI scheme.

... which for taurus model URIs is translated as (from TEP14):

  • The model object is fully referenced by the model URI without the fragment name. i.e., the scheme Factory will ignore the fragment component of the URI when providing the model object.
  • The fragment may optionally be used by the client (e.g. a widget) to identify "aspects" of the model (e.g. members of the model object, or slices of values,...)
  • The model objects provide the getFragmentObj() method to dereference a fragment name. In its current implementation, the fragment is computed from a the model itself by evaluating the expression "."

See a more detailed explanation from TEP14

And also see the support for slices added by TEP15

examples of fragments usage

Some widgets use the fragments... but not all do:

The following code ...

# suppose that the rvalue of tango:a/b/c/d  is ["a","b","c","d","e","f"]
name = 'tango:a/b/c/d#rvalue[:4:2]'
label = TaurusLabel()  # <- uses fragments
table = TaurusValuesTable()  # <- ignores fragments
label.setModel(name)
table.setModel(name)

would result in the label showing ['a','c'] and the table showing something like:

a
b
c
d
e
f

PyQt new-style signals

Taurus 4 changed all its signals to new-style signals.

Applications using old-style signals still work with taurus4, but they will not be able to run under bindings other than PyQt4 (which is soon to be unsupported).

So it is about time to migrate your signals to new-style.

In most cases this can be best done manually, but for larger projects, you may be interested in using the fixsignals semiautomated fixer

Please read the new-style signals documentation and familiarize yourself with it

A summary in an example:

Consider the following code that uses old-style signals:

class MyWidget(Qt.QWidget):

    def foo(self):
        self.connect(self, Qt.SIGNAL('mysigname'), self.myslot)
        self.emit(Qt.SIGNAL('mysigname'), 1, 2)

    def myslot(self, a, b):
        print(a, b, a + b)

It should be converted to:

class MyWidget(Qt.QWidget):

    mysigname = Qt.pyqtSignal('int', 'int')

    def foo(self):
        self.mysigname.connect(self.myslot)
        self.mysigname.emit(1, 2)

    def myslot(self, a, b):
        print(a, b, a + b)

Typical complications when migrating from old to new style signals

Signal name collides with an attribute or method name

For example, the following code works well with old-style signals because the old-style signature "applied" is just a signature, not a member of the class:

class MyDialog(QWidget):
    def __init__(self):
        (...)
        self.connect(self, Qt.SIGNAL('applied'), self.applied)
    def applied(self):
        (...)

... but when trying to define the new-style signal as applied = Qt.pyqtSignal(), we get it overwritten by the definition of the applied method.

The strategy to solve it depends on each case:

  • if the applied method is in practice just used as a slot and is not called by other classes, then the best would be to rename it to something like _applied or _onApply or, if it needs to be accessible from outside, by onApply
  • If the the method belongs to a public API, but the signal is mostly used internally, then rename the signal (e.g. appliedsig = Qt.pyqtSignal()

wrong arguments received from overloaded signals

Consider

class MyCombo(QComboBox):
    def __init__(self):
        (...)
        self.currentIndexChanged.connect(self._onChanged)
    def _onChanged(self, text):
        print text.lower()

This code will not work as expected, because instead of receiving a string, the _onChanged method will receive an integer.

Fix this by explicitly connecting the right overloaded signal:

self.currentIndexChanged["QString"].connect(self._onChanged)

It can also be solved by specifying the accepted type with a pyqtSlot decorator:

@pyqtSlot("QString")
def _onChanged(self, text):
    print(text.lower())

problems with overloaded signals if the slot has optional arguments

Consider this code in which the print_msg method (which may be called in other ways) is also used as the slot for the clicked signal of the button.

class MyButton(QPushButton):
    def __init__(self):
        super(MyButton, self).__init__()
        # we want to print "clicked" when the button is clicked
        self.clicked.connect(self.print_msg)
        self.print_msg(message="ready!")
        
    def print_msg(self, message="clicked"):
        print(message)

The problem in this case is that instead of the default message, when the button is pressed, we print: "False" .

This is because the clicked signal has 2 overloads:

  • () (no args, the default overload)
  • bool (the checked state of the button).

And even if the default overload is compatible with the print_msg slot, the other overload is selected automatically.

The problem is that the API for selecting the right overloaded signal in this situations has changed between PyQt version 4 and 5 and also between 5.2 and 5.3+.

For example, doing the connection like this:

self.clicked[()].connect(self.print_msg)

... would work with PyQt <= 5.2, but not in 5.3+

TIP: If possible, avoid the issue by not using methods with optional arguments as a slot.

The following is a summary of recommended strategies that work with any Qt version (see them applied in this commit):

  • do not explicitly select an overloaded signal when the slot does not have optional arguments
  • decorate slots with pyqtSlot to select appropriate arg types:
    @pyqtSlot()
    def print_msg(...
  • create dummy slots without optional arguments
    class MyButton(QPushButton):
      def __init__(self):
          super(MyButton, self).__init__()
          self.clicked.connect(self._onClicked)  # <- changed!
          self.print_msg(message="ready!")
          
      def _onClicked(self):  # <- added!
          self.print_msg(message="clicked")
          
      def print_msg(self, message="clicked"):
          print(message)
  • use functools.partial to "freeze" optional arguments of the slot (and, by all means... DON'T use lambda in the connect). e.g. doing the connect with
    self.clicked.connect(functools.partial(self.print_msg, message='clicked'))

DON'T use str to define the arg type of a signal

This may give problems due to the different implementations of str in py2 and py3. Use "QString" instead.

defining signals with python objects as args

Defining a signal that accepts a python list could be done as:

sig = pyqtSignal(list)

But what if we want also to accept a tuple? One could think of doing :

sig = pyqtSignal([list], [tuple])  # defining an overloaded signal

or

sig = pyqtSignal(object)  # using a base python object

... but this does not not always work as expected (even if it may seem to work in some cases).

A better approach is to use "PyQt_PyObject" for the argument type in the definition, as in

sig = pyqtSignal('PyQt_PyObject')

This is ok if we are writing idiomatic PyQt4 or PyQt5 code, but if we want to support PySide(2) as well, we should the following instead:

from taurus.external.qt import compat 
sig = pyqtSignal(compat.PY_OBJECT)

Defining signals in Mixin classes that are not QObjects

From the PyQt docs:

New signals should only be defined in sub-classes of QObject. They must be part of the class definition and cannot be dynamically added as class attributes after the class has been defined.

This makes it difficult to convert the the following code to new-style signals:

class FooMixin(object):
    def emit_foo(self)
        Qt.QObject.emit(Qt.SIGNAL('foo(int)'), 1)

class MyClass(FooMixin, Qt.QObject):
    pass

a = MyClass()
a.emit_foo()

This mixin would be fine with old-style signals, but if we tried naively to convert it to new style signals, it would not work:

class FooMixin(object):
    foo = pyqtSignal(int)  # <-- illegal in non-QObject class
    
    def emit_foo(self)
        self.foo.emit(1)

In order to solve this in taurus (e.g. for TaurusBaseComponent), we implemented baseSignal (in taurus.qt.qtcore.util.signal), which in the previous example would be used as:

class FooMixin(object):
    foo = baseSignal('foo', int) 
    
    def emit_foo(self)
        self.foo.emit(1)

Writing code compatible with python 2.7 and 3.5+

Support only py3 (unless you really need to support py2)?

Python 2 is dead, so you really should write code that works with python3.

But, if you do not need to support python2, the simplest thing is to only support python 3.5+ (debian9 provides python3.5)

TIP: consider if you can leave a python2 version of your program in a "mainteinance" branch, and then convert your code to python3-only with 2to3 and develop new features for python3 only.

Taurus is already py3-compatible (although note that the qwt5-based widgets are py2-only, so for plotting you will need to use taurus_pyqtgraph), and sardana is now py3-only.

Supporting py2 and py3 simultaneously

Taurus uses the future module (do not confuse with the standard __future__ module) to provide a single code base that is simultaneously compatible with py2 and py3.

If you need to simultaneously support py2 and py3 (e.g., if you are writing code that is going to be contributed to taurus or sardana) you can use the futurize utility

In such case, you definitely should carefully read the future cheat sheet, the library reorganization docs and the info in here

DON'T... use six (at least for taurus-related code)

six is an earlier alternative to future to write py2+py3 compatible code. You may find many recipes using it (e.g. in Stack Overflow). In your own code, feel free to use it (although in general future allows you to write more idiomatic code). But avoid it if you are contributing the code to taurus (for code consistency and to avoid depending on six)

DON'T use the past module

The futurize script may add the past module to your imports to do safe conversions of code (e.g. it may add from past.utils import old_div).

This makes code less idiomatic and can in general be avoided with trivial refactorings of the code.

DON'T... use the future.builtins module unless really necessary

This is a bit personal... but I prefer to keep the hacks at a minimum, so I recommend to favour refactoring over "overusing" builtins.

Things to keep in mind to facilitate the py2-py3 transition

  • use from __future__ import print_function

  • use from __future__ import absolute_import

  • use from __future__ import division (or just take care of casting your divisions appropriately)

  • learn the difference about bytes and unicode, and how and when to convert. Also understand what from builtins import str, bytes do in py2 and in py3

    • in py2: builtins.bytes != bytes == str != unicode ~ builtins.str
    • in py3: builtins.bytes == bytes != str == builtins.str
  • Remember that many things that were lists or tuples are now iterators (range, zip, ...)

Improved eval scheme

The eval scheme has progressed a lot since taurus 3.

  • The syntax has changed: e.g., evaluation://x+y?x=1;y=2 should be written in the current syntax as eval:x=1;y=2;x+y (the old syntax is supported by the backwards compatibility layer, but it is deprecated, less robust and much more limited than the new one)
  • arbitrary nesting of attr references is now allowed.
  • Usage of arbitrary members of objects in the evaluation expresion is now allowed
  • arbitrary import of modules is allowed
  • writable evaluation attributes are supported

See details and examples for all this in the docs of the eval scheme

examples of use cases

See this tutorial

To process an existing attribute

Add an offset to the tango a/b/c/d attribute: eval:{tango:a/b/c/d}+Q("10mm")

Cast a boolean attr to an int to plot it in a trend: eval:int({sys/tg_test/1/boolean_scalar})

To make a quick GUI wrapping some existing module:

A poor-man's clock:

taurusform 'eval:@datetime.*/datetime.now().isoformat()'

Formatter API

The formatter API was introduced to provide the developer and the user control over how a value is displayed by a given widget or set of widgets

A typical use case for this is to alter the number of decimal digits shown for a float, or to use exponential notation.

The user/developer can change the "formatter" used by a widget instance, by all instances of a class or even by all Taurus widgets in the current system.

The "formatter" is used by the .displayValue() method of TaurusBaseComponent for transforming the value into a string representation.

The "formatter" can be a python format string or a callable that returns a python format string (for details, see )

using Formatters

See some usage examples in this tutorial

existing formatters

taurus currently provides the following ready-made formatters:

  • defaultFormatter: a generic formatter (it supports all types of values). For numerical values, it uses the .precision of the model object to decide the number of decimal places to be used.
  • tangoFormatter. A specialiced formatter that uses the Tango "format" configuration (Display.Format configuration from the Tango DB). It can only be used with tango attributes. Useful to emulate the behaviour of some widgets in taurus3

Changing the formatter:

You can change the formatter in the following ways (listed from less to more intrusive):

  1. (GUI) set a custom formatter in TaurusForm (globally or per attribute) via a context menu option. TaurusForms will store this preference in their configs.
  2. (GUI/PROG) use the TaurusBaseWidget.onSetFormatter() slot to set a custom formatter on other widgets
  3. (PROG) change the defaultFormatDict of a widget or class that uses the defaultFormatter
  4. (PROG) set a custom formatter on any taurus widget (using TaurusBaseComponent.setFormat)
  5. (PROG) set a custom formatter on any taurus class (by overwriting .FORMAT)
  6. (CLI) set the defaultFormatter via a TaurusApplication command line argument (--default-formatter)
  7. (CONF) set the defaultFormatter via tauruscustomsettings.DEFAULT_FORMATTER

DON'T... change the default formatter

Note that options 6 and 7 above deal with the default formatter and therefore will affect all widgets (except those that explicitly changed it). Therefore they should be used with great care and only with very generic formatters.

In other words: Do not use options 6 or 7 unless you REALLY know what you are doing. Option 4 should also be used with care if applied to a base class (since all child classes will be affected)

The plots (PyQwt5, guiqwt, pyqtgraph,...)

TL;DR: Use taurus-pyqtgraph

See details in the Plotting Guide: http://taurus-scada.org/devel/plotting_guide.html

Icons (taurus.qt.qtgui.icon)

Taurus4 implied a refactoring of the icon API. Icon resource files are no longer used, and a new API is in place.

See the Icon Guide: http://taurus-scada.org/devel/icon_guide.html

Usages of the previous API should be supported by taurus4's backwards-compatibility layer (and warning messages issued to help in adapting to the new API)


Other topics that were not covered but which may be interesting:

Do not use useParentModel API

See https://github.com/taurus-org/taurus/issues/734

Delayed events API

Related code on: http://taurus-scada.org/_modules/taurus/core/tango/tangoattribute.html#TangoAttribute.subscribePendingEvents taurus.core.tango.tangoattribute.TangoAttribute.subscribePendingEvents

http://taurus-scada.org/devel/api/taurus/core/tango/_TangoFactory.html taurus.core.tango.tangofactory.TangoFactory.set_tango_subscribe_enabled

taurus.qt.qtcore.util.emitter.DelayedSubscriber (Note: emitter is not yet included in API documentation)

Plugins

TEP13 https://github.com/taurus-org/taurus/blob/develop/doc/source/tep/TEP13.md

Setuptools entry points: https://setuptools.readthedocs.io/en/latest/setuptools.html#extensible-applications-and-frameworks

PR: Taurus event queue

Delegates the control of the event queue to Taurus instead of Tango when the serialization mode is Serial. https://github.com/taurus-org/taurus/pull/738

Clone this wiki locally