-
Notifications
You must be signed in to change notification settings - Fork 46
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.
# 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
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
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:
- 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)
- It forces the use of
print_function
(for py2+py3 compatibility) - It uses
TaurusApplication
instead ofQApplication
- It loads the .ui file dynamically instead of using a compiled
ui_Foo.py
(it usesuic.loadUi
, but using theUILoadable
decorator fromtaurus.qt.qtgui.util.ui
would also be ok) - It uses
os.path
to create paths (platform independent) - 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) - It uses new-style signals (more later)
- It uses modern-style pyhton string formatting
- It does not import PyTango: it uses
taurus.Device
instead of aPyTango.DeviceProxy
, which could produce problems on exit and would defeat some taurus optimizations.
Other stuff to note:
- It uses
taurus.external.qt
(although using an specific binding would also be ok, see below) - It uses the consolidated
Qt
submodule. UsingQtGui
would also be ok (see below) - 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 oldcall__init__()
andcall__init__wo_kw()
methods.
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).
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)
All pyhton packages should:
- have a setuptools-based
setup.py
. - use bumpversion (or another similar tool) and use semantic versioning conventions
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
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)
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:
- create a separate branch for a "taurus4" version of your code
- work step-by-step on refactoring your "taurus4" version until it works without deprecation warnings.
- 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
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.
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:
- You may want to eventually use an eval attribute to do tests or to make dynamic operations on tango attributes
- You may want to retrieve values from tango-archiving
- You may want to compare against a value from a hdf5 dataset
-
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
-
if not explicit, the scheme name is
"tango"
- FALSE: it is controlled in
tauruscustomsettings.DEFAULT_SCHEME
(which defaults to"tango"
)
- FALSE: it is controlled in
-
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
, nottango://a/b/c
)
- FALSE: the separator is the first occurrence of
-
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 name
tgarch:a/b/c/d?db=tdb?t0=-5d
)
- FALSE: some schemes may use queries for their models (e.g. taurus_tangoarchiving's attribute name
-
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: intango:a/b/c/d#wvalue
, the attribute name is justtango: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.
- FALSE: strictly speaking, the fragment (whatever comes after an unescaped
-
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
- FALSE: It depends on the scheme (e.g., eval or epics are case-sensitive). It can be checked with
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
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']
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
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
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!
(...)
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.
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
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
- 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
- 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)
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
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.
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 |
- 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
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
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 |
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
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)
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, byonApply
- 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()
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())
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 withself.clicked.connect(functools.partial(self.print_msg, message='clicked'))
This may give problems due to the different implementations of str
in py2 and py3. Use "QString"
instead.
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)
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)
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.
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
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
)
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.
This is a bit personal... but I prefer to keep the hacks at a minimum, so I recommend to favour refactoring over "overusing" builtins
.
-
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, ...)
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 aseval: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
See this tutorial
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})
A poor-man's clock:
taurusform 'eval:@datetime.*/datetime.now().isoformat()'
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 )
See some usage examples in this tutorial
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
You can change the formatter in the following ways (listed from less to more intrusive):
- (GUI) set a custom formatter in TaurusForm (globally or per attribute) via a context menu option. TaurusForms will store this preference in their configs.
- (GUI/PROG) use the
TaurusBaseWidget.onSetFormatter()
slot to set a custom formatter on other widgets - (PROG) change the
defaultFormatDict
of a widget or class that uses thedefaultFormatter
- (PROG) set a custom formatter on any taurus widget (using
TaurusBaseComponent.setFormat
) - (PROG) set a custom formatter on any taurus class (by overwriting
.FORMAT
) - (CLI) set the defaultFormatter via a TaurusApplication command line argument (
--default-formatter
) - (CONF) set the defaultFormatter via tauruscustomsettings.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)
TL;DR: Use taurus-pyqtgraph
See details in the Plotting Guide: http://taurus-scada.org/devel/plotting_guide.html
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)
See https://github.com/taurus-org/taurus/issues/734
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)
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
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