From feac865e9d13cbe903827837e9caf23ddb99cdff Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 2 Jul 2019 12:46:59 +0200 Subject: [PATCH 001/373] Add possibility to provide icon via PanelDescription --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 3 ++- lib/taurus/qt/qtgui/taurusgui/utils.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 218080173..3264c1320 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1289,12 +1289,13 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): w.setModel(p.model) if p.instrumentkey is None: instrumentkey = self.IMPLICIT_ASSOCIATION + icon = p.icon # the pool instruments may change when the pool config changes, # so we do not store their config registerconfig = p not in poolinstruments # create a panel self.createPanel(w, p.name, floating=p.floating, registerconfig=registerconfig, - instrumentkey=instrumentkey, permanent=True) + instrumentkey=instrumentkey, permanent=True, icon=icon) except Exception as e: msg = "Cannot create panel %s" % getattr( p, "name", "__Unknown__") diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index 6812fd315..7c9274582 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -366,6 +366,7 @@ class PanelDescription(TaurusGuiComponentDescription): def __init__(self, *args, **kwargs): self.instrumentkey = kwargs.pop('instrumentkey', None) + self.icon = kwargs.pop("icon", None) TaurusGuiComponentDescription.__init__(self, *args, **kwargs) @staticmethod @@ -397,11 +398,13 @@ def fromPanel(panel): else: # ignore other "model" attributes (they are not from Taurus) model = None + icon = panel.icon return PanelDescription(name, classname=classname, modulename=modulename, widgetname=widgetname, floating=floating, sharedDataWrite=sharedDataWrite, - sharedDataRead=sharedDataRead, model=model) + sharedDataRead=sharedDataRead, model=model, + icon=icon) class ToolBarDescription(TaurusGuiComponentDescription): From 5a6580eb897539420049e8d7c533a8fb276594b8 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 2 Jul 2019 13:04:51 +0200 Subject: [PATCH 002/373] Add possibility to provide modelInConfig and ModifiableByUser properties in PanelDescription --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 17 +++++++++++------ lib/taurus/qt/qtgui/taurusgui/utils.py | 7 ++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 3264c1320..ef78d582d 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -629,7 +629,8 @@ def removePanel(self, name=None): self.debug('Panel "%s" removed' % name) def createPanel(self, widget, name, floating=False, registerconfig=True, custom=False, - permanent=False, icon=None, instrumentkey=None): + permanent=False, icon=None, instrumentkey=None, modelinconfig=False, + modifiablebyuser=False): ''' Creates a panel containing the given widget. @@ -698,6 +699,10 @@ def createPanel(self, widget, name, floating=False, registerconfig=True, custom= self.registerConfigDelegate(panel, name=name) self.__panels[name] = panel + if isinstance(widget, TaurusBaseComponent): + widget.setModifiableByUser(modifiablebyuser) + widget.setModelInConfig(modelinconfig) + # connect the panel visibility changes panel.visibilityChanged.connect(self._onPanelVisibilityChanged) @@ -833,13 +838,10 @@ def createCustomPanel(self, paneldesc=None): w.setCustomWidgetMap(self.getCustomWidgetMap()) if paneldesc.model is not None: w.setModel(paneldesc.model) - if isinstance(w, TaurusBaseComponent): - w.setModifiableByUser(True) - w.setModelInConfig(True) self.createPanel(w, paneldesc.name, floating=paneldesc.floating, custom=True, registerconfig=False, instrumentkey=paneldesc.instrumentkey, - permanent=False) + permanent=False, modelinconfig=True, modifiablebyuser=True) msg = 'Panel %s created. Drag items to it or use the context menu to customize it' % w.name self.newShortMessage.emit(msg) @@ -1290,12 +1292,15 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): if p.instrumentkey is None: instrumentkey = self.IMPLICIT_ASSOCIATION icon = p.icon + model_in_config = p.model_in_config + modifiable_by_user = p.modifiable_by_user # the pool instruments may change when the pool config changes, # so we do not store their config registerconfig = p not in poolinstruments # create a panel self.createPanel(w, p.name, floating=p.floating, registerconfig=registerconfig, - instrumentkey=instrumentkey, permanent=True, icon=icon) + instrumentkey=instrumentkey, permanent=True, icon=icon, + modelinconfig=model_in_config, modifiablebyuser=modifiable_by_user) except Exception as e: msg = "Cannot create panel %s" % getattr( p, "name", "__Unknown__") diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index 7c9274582..e85c22d5a 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -367,6 +367,8 @@ class PanelDescription(TaurusGuiComponentDescription): def __init__(self, *args, **kwargs): self.instrumentkey = kwargs.pop('instrumentkey', None) self.icon = kwargs.pop("icon", None) + self.model_in_config = kwargs.pop("modelinconfig", False) + self.modifiable_by_user = kwargs.pop("modifiablebyuser", False) TaurusGuiComponentDescription.__init__(self, *args, **kwargs) @staticmethod @@ -399,12 +401,15 @@ def fromPanel(panel): # ignore other "model" attributes (they are not from Taurus) model = None icon = panel.icon + model_in_config = panel.model_in_config + modifiable_by_user = panel.modifiablebyuser return PanelDescription(name, classname=classname, modulename=modulename, widgetname=widgetname, floating=floating, sharedDataWrite=sharedDataWrite, sharedDataRead=sharedDataRead, model=model, - icon=icon) + icon=icon, modelinconfig=model_in_config, + modifiablebyuser=modifiable_by_user) class ToolBarDescription(TaurusGuiComponentDescription): From b3489876e8e83226345ee54e3a71d622ce2b2d34 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 2 Jul 2019 13:13:17 +0200 Subject: [PATCH 003/373] Add possibility to provide formatter property in PanelDescription --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 7 +++++-- lib/taurus/qt/qtgui/taurusgui/utils.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index ef78d582d..65f033e15 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -630,7 +630,7 @@ def removePanel(self, name=None): def createPanel(self, widget, name, floating=False, registerconfig=True, custom=False, permanent=False, icon=None, instrumentkey=None, modelinconfig=False, - modifiablebyuser=False): + modifiablebyuser=False, formatter='taurus.qt.qtgui.base.taurusbase.defaultFormatter'): ''' Creates a panel containing the given widget. @@ -702,6 +702,7 @@ def createPanel(self, widget, name, floating=False, registerconfig=True, custom= if isinstance(widget, TaurusBaseComponent): widget.setModifiableByUser(modifiablebyuser) widget.setModelInConfig(modelinconfig) + widget.setFormat(formatter) # connect the panel visibility changes panel.visibilityChanged.connect(self._onPanelVisibilityChanged) @@ -1294,13 +1295,15 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): icon = p.icon model_in_config = p.model_in_config modifiable_by_user = p.modifiable_by_user + formatter = p.formatter # the pool instruments may change when the pool config changes, # so we do not store their config registerconfig = p not in poolinstruments # create a panel self.createPanel(w, p.name, floating=p.floating, registerconfig=registerconfig, instrumentkey=instrumentkey, permanent=True, icon=icon, - modelinconfig=model_in_config, modifiablebyuser=modifiable_by_user) + modelinconfig=model_in_config, modifiablebyuser=modifiable_by_user, + formatter=formatter) except Exception as e: msg = "Cannot create panel %s" % getattr( p, "name", "__Unknown__") diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index e85c22d5a..01b85909c 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -369,6 +369,7 @@ def __init__(self, *args, **kwargs): self.icon = kwargs.pop("icon", None) self.model_in_config = kwargs.pop("modelinconfig", False) self.modifiable_by_user = kwargs.pop("modifiablebyuser", False) + self.formatter = kwargs.pop("formatter", 'taurus.qt.qtgui.base.taurusbase.defaultFormatter') TaurusGuiComponentDescription.__init__(self, *args, **kwargs) @staticmethod @@ -403,13 +404,15 @@ def fromPanel(panel): icon = panel.icon model_in_config = panel.model_in_config modifiable_by_user = panel.modifiablebyuser + formatter = panel.formatter return PanelDescription(name, classname=classname, modulename=modulename, widgetname=widgetname, floating=floating, sharedDataWrite=sharedDataWrite, sharedDataRead=sharedDataRead, model=model, icon=icon, modelinconfig=model_in_config, - modifiablebyuser=modifiable_by_user) + modifiablebyuser=modifiable_by_user, + formatter=formatter) class ToolBarDescription(TaurusGuiComponentDescription): From 72435577d0054fa352f4cd910fb316f58e20d1ba Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 2 Jul 2019 13:18:08 +0200 Subject: [PATCH 004/373] Add docs --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 3 +++ lib/taurus/qt/qtgui/taurusgui/utils.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 65f033e15..6e7394301 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -642,6 +642,9 @@ def createPanel(self, widget, name, floating=False, registerconfig=True, custom= :param permanent: (bool) set this to True for panels that need to be recreated when restoring the app :param icon: (QIcon) icon for the panel :param instrumentkey: (str) name of an instrument to which this panel is to be associated + :param modelinconfig: (bool) whether to store model in settings file or not + :param modifiablebyuser: (bool) whether user can modify widget or not + :param formatter: (str) formatter for widget :return: (DockWidgetPanel) the created panel diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index 01b85909c..a3fba395f 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -365,6 +365,14 @@ class PanelDescription(TaurusGuiComponentDescription): ''' def __init__(self, *args, **kwargs): + """ + + :param args: + :param instrumentkey: (str) + :param modelinconfig: (bool) whther to store model in settigns file or not + :param modifiablebyuser: (bool) whether user can edit widget or not + :param formatter: (str) formatter used by this widget + """ self.instrumentkey = kwargs.pop('instrumentkey', None) self.icon = kwargs.pop("icon", None) self.model_in_config = kwargs.pop("modelinconfig", False) From dcff22e1a50e648c9959cff1bca30e808b36aea9 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 2 Jul 2019 14:32:47 +0200 Subject: [PATCH 005/373] Add possibility to add extra config options in PanelDescription The form is as key word arguments in PanelDescription constructor. If widget has such attribute, it will be set to proper value --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 17 +++++++++++++++++ lib/taurus/qt/qtgui/taurusgui/utils.py | 7 ++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 6e7394301..f573787dc 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1289,6 +1289,23 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): except AttributeError: pass w = p.getWidget(sdm=Qt.qApp.SDM, setModel=False) + extra_config = p.extra_config + if extra_config: + for key in extra_config: + if hasattr(w, key): + try: + value = extra_config[key] + setattr(w, key, value) + except Exception as e: + msg = "Cannot set attribute '%s' of widget '%s' " \ + "to value '%s'" % (key, p.classname, str(value)) + self.error(msg) + self.traceback(level=taurus.Info) + result = Qt.QMessageBox.critical(self, "Initialization error", "%s\n\n%s" % ( + msg, repr(e)), Qt.QMessageBox.Abort | Qt.QMessageBox.Ignore) + if result == Qt.QMessageBox.Abort: + sys.exit() + if hasattr(w, "setCustomWidgetMap"): w.setCustomWidgetMap(self.getCustomWidgetMap()) if p.model is not None: diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index a3fba395f..19e42156c 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -372,12 +372,16 @@ def __init__(self, *args, **kwargs): :param modelinconfig: (bool) whther to store model in settigns file or not :param modifiablebyuser: (bool) whether user can edit widget or not :param formatter: (str) formatter used by this widget + + Additionally, extra configuration options can be passed in constructor as key word arguments. + Proper widget attributes will be set to corresponding values """ self.instrumentkey = kwargs.pop('instrumentkey', None) self.icon = kwargs.pop("icon", None) self.model_in_config = kwargs.pop("modelinconfig", False) self.modifiable_by_user = kwargs.pop("modifiablebyuser", False) self.formatter = kwargs.pop("formatter", 'taurus.qt.qtgui.base.taurusbase.defaultFormatter') + self.extra_config = kwargs TaurusGuiComponentDescription.__init__(self, *args, **kwargs) @staticmethod @@ -413,6 +417,7 @@ def fromPanel(panel): model_in_config = panel.model_in_config modifiable_by_user = panel.modifiablebyuser formatter = panel.formatter + extra_config = panel.extra_config return PanelDescription(name, classname=classname, modulename=modulename, widgetname=widgetname, floating=floating, @@ -420,7 +425,7 @@ def fromPanel(panel): sharedDataRead=sharedDataRead, model=model, icon=icon, modelinconfig=model_in_config, modifiablebyuser=modifiable_by_user, - formatter=formatter) + formatter=formatter, **extra_config) class ToolBarDescription(TaurusGuiComponentDescription): From a2a670018d1de9f86d3108305a7b515466bba0d3 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 2 Jul 2019 15:23:33 +0200 Subject: [PATCH 006/373] Set widget's properties before creating panel --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index f573787dc..577935c9d 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -669,6 +669,11 @@ def createPanel(self, widget, name, floating=False, registerconfig=True, custom= self.info('Panel with name "%s" already exists. Reusing.' % name) return self.__panels[name] + if isinstance(widget, TaurusBaseComponent): + widget.setModifiableByUser(modifiablebyuser) + widget.setModelInConfig(modelinconfig) + widget.setFormat(formatter) + # create a panel panel = DockWidgetPanel(None, widget, name, self) # we will only place panels in this area @@ -702,11 +707,6 @@ def createPanel(self, widget, name, floating=False, registerconfig=True, custom= self.registerConfigDelegate(panel, name=name) self.__panels[name] = panel - if isinstance(widget, TaurusBaseComponent): - widget.setModifiableByUser(modifiablebyuser) - widget.setModelInConfig(modelinconfig) - widget.setFormat(formatter) - # connect the panel visibility changes panel.visibilityChanged.connect(self._onPanelVisibilityChanged) From f5903631f2ee979f2603af30e1ca8940bf1d792b Mon Sep 17 00:00:00 2001 From: Marc Rosanes Date: Thu, 25 Jul 2019 10:20:16 +0200 Subject: [PATCH 007/373] Update test on 'fixed range scale' Update test on 'fixed range scale' (oscilloscope mode) --- doc/how_to_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to_release.md b/doc/how_to_release.md index e09b1533c..7f9c07b78 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -209,7 +209,7 @@ http://taurus-scada.org/users/getting_started.html - [ ] Move curves between axes by clicking on legend (and test zoom on Y2) - [ ] Test plot configuration dialog - [ ] Test Forced reading mode -- [ ] Test autopanning mode +- [ ] Test X Axis 'fixed range scale' mode - [ ] Test autoscale x mode - [ ] Test Save & restore config (change curve properties, zoom, etc & check that everything is restored) - [ ] ... other features from [user's guide](http://taurus-scada.org/users/ui/index.html) From 06cc785060f10e12e8e1aca917f1f1540f96bb52 Mon Sep 17 00:00:00 2001 From: Marc Rosanes Date: Thu, 25 Jul 2019 16:55:13 +0200 Subject: [PATCH 008/373] Fix broken link for icons Fix broken link for Taurus Icon Catalog --- doc/how_to_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to_release.md b/doc/how_to_release.md index 7f9c07b78..59c865a44 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -151,7 +151,7 @@ http://taurus-scada.org/users/getting_started.html - [ ] Open an ini file with taurus config and check that it is loaded correctly. ### taurus icons catalog -- [ ] Launch `taurus icons`. Several tabs with an array of icons [should be displayed](http://taurus-scada.org/en/latest/devel/icon_guide.html#taurus-icon-catalog) +- [ ] Launch `taurus icons`. Several tabs with an array of icons [should be displayed](http://taurus-scada.org/devel/icon_guide.html#taurus-icon-catalog) - [ ] Check that tooltips give info on each icon - [ ] Click on some icons and check that they give a bigger view of the icon and more info. From 40b07e9174c935e6f934f546a169401ca5a34554 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 30 Jul 2019 11:58:28 +0200 Subject: [PATCH 009/373] (m) (doc) add click to conda create recipe --- doc/source/users/getting_started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/users/getting_started.rst b/doc/source/users/getting_started.rst index 8378176a0..4259faa85 100644 --- a/doc/source/users/getting_started.rst +++ b/doc/source/users/getting_started.rst @@ -48,7 +48,7 @@ First create a Conda_ environment with all the dependencies and activate it:: conda config --add channels conda-forge conda config --add channels tango-controls # for windows, use "tcoutinho" instead of "tango-controls" - conda create -n py3qt5 python=3 pyqt=5 itango pytango lxml future guidata guiqwt ipython pillow pint ply pyqtgraph pythonqwt numpy scipy pymca + conda create -n py3qt5 python=3 pyqt=5 itango pytango lxml future guidata guiqwt ipython pillow pint ply pyqtgraph pythonqwt numpy scipy pymca click conda activate py3qt5 Then install taurus and taurus_pyqtgraph using pip (as explained above) From 8bb952d26a4fefb6b5c2840de0b442f629c15f03 Mon Sep 17 00:00:00 2001 From: zreszela Date: Tue, 30 Jul 2019 15:22:49 +0200 Subject: [PATCH 010/373] Bump version 4.5.4-alpha to 4.5.5-alpha --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 15a16f3a2..ff349d0e8 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.5.4-alpha +current_version = 4.5.5-alpha parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index fb2cf6bf9..28c0c12f8 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.5.4-alpha' +version = '4.5.5-alpha' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: From be7ca9228fe6d15fda1a1d93ee0685d96021589a Mon Sep 17 00:00:00 2001 From: cfalcon Date: Tue, 6 Aug 2019 10:17:44 +0200 Subject: [PATCH 011/373] Create cli subcommand "taurus check-deps" Right now the status of taurus dependencies can be checked with: python -c 'import taurus; taurus.check_dependencies()' Add new click subcommand to execute the same function. Fix #966 --- lib/taurus/core/taurushelper.py | 10 ++++++++++ setup.py | 1 + 2 files changed, 11 insertions(+) diff --git a/lib/taurus/core/taurushelper.py b/lib/taurus/core/taurushelper.py index 07fb7d32d..3c69a338f 100644 --- a/lib/taurus/core/taurushelper.py +++ b/lib/taurus/core/taurushelper.py @@ -32,6 +32,7 @@ import re from taurus import tauruscustomsettings from .util.log import taurus4_deprecation +import click __all__ = ['check_dependencies', 'log_dependencies', 'getSchemeFromName', 'getValidTypesForName', 'isValidName', 'makeSchemeExplicit', @@ -409,6 +410,15 @@ def Object(*args): klass = factory.findObjectClass(name) return factory.getObject(klass, name) + +@click.command('check-deps') +def check_dependencies_cmd(): + """ + Check-list of requirements and marks those that are fulfilled + """ + check_dependencies() + + from taurus.core.util import log as __log_mod Logger = __log_mod.Logger diff --git a/setup.py b/setup.py index 31b6f86cd..e68f6c32a 100644 --- a/setup.py +++ b/setup.py @@ -111,6 +111,7 @@ def get_release_info(): 'demo = taurus.qt.qtgui.panel.taurusdemo:demo_cmd', 'logmon = taurus.core.util.remotelogmonitor:logmon_cmd', 'qlogmon = taurus.qt.qtgui.table.qlogtable:qlogmon_cmd', + 'check-deps = taurus.core.taurushelper:check_dependencies_cmd' ] model_selectors = [ From 94485bdf5dd16fefb96dd0a3fa403e20aa280a69 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 19 Aug 2019 15:13:59 +0200 Subject: [PATCH 012/373] Bump version 4.6.0 to 4.6.1-alpha --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e36420bbf..3510bb963 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.6.0 +current_version = 4.6.1-alpha parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index dcb202314..39dd68984 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.6.0' +version = '4.6.1-alpha' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: From 7ad0f0298cea96bb33921537e60b7bb7b0bf6440 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 19 Aug 2019 15:53:37 +0200 Subject: [PATCH 013/373] Add new "Unreleased" entry in CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eeaa67aa..bc88b0eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ Note: changes in the [support-3.x] branch (which was split from the master branch after [3.7.1] and maintained in parallel to the develop branch) won't be reflected in this file. +## Unreleased + +### Added +### Removed +### Changed +### Deprecated +### Fixed + ## [4.6.0] - 2019-08-19 [Jul19 milestone](https://github.com/taurus-org/taurus/milestone/13) From 4e07660e4794b61d3719161e2c178931cd8c51a3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 19 Aug 2019 21:08:17 +0200 Subject: [PATCH 014/373] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc88b0eae..d1848bc65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ develop branch) won't be reflected in this file. ## Unreleased ### Added +- check-deps subcommand (#988) + ### Removed ### Changed ### Deprecated From 3035747ab9b5fe936dceeecec7437fffa71c65d2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 19 Aug 2019 21:11:45 +0200 Subject: [PATCH 015/373] Improve check-deps subcommand description --- lib/taurus/core/taurushelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/taurushelper.py b/lib/taurus/core/taurushelper.py index 3c69a338f..eddc3b764 100644 --- a/lib/taurus/core/taurushelper.py +++ b/lib/taurus/core/taurushelper.py @@ -414,7 +414,7 @@ def Object(*args): @click.command('check-deps') def check_dependencies_cmd(): """ - Check-list of requirements and marks those that are fulfilled + Shows the taurus dependencies and checks if they are available """ check_dependencies() From 8cb47168b149e97395ce5cf3fc5822a9f1c5cf78 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 19 Aug 2019 21:34:58 +0200 Subject: [PATCH 016/373] Bump version 4.6.1 to 4.6.2-alpha --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index fde7f42a1..cb448e3f1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.6.1 +current_version = 4.6.2-alpha parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index 820800f57..887edff7f 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.6.1' +version = '4.6.2-alpha' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: From 687da28c58076e5b50dc9e82cef9a7972c78f2a9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 21 Aug 2019 16:34:25 +0200 Subject: [PATCH 017/373] Document usage of check-deps Update the getting started guide with a reference to the new check-deps subcommand --- doc/source/users/getting_started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/users/getting_started.rst b/doc/source/users/getting_started.rst index 4259faa85..9f39fda58 100644 --- a/doc/source/users/getting_started.rst +++ b/doc/source/users/getting_started.rst @@ -107,7 +107,7 @@ expected of Taurus (which are considered "extras"). For example: For a complete list of "extra" features and their corresponding requirements, execute the following command:: - python -c 'import taurus; taurus.check_dependencies()' + taurus check-deps How you install the required dependencies depends on your preferred From 0c75f5443b2aea862e765edd29b5a1d0c162c28c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 23 Aug 2019 16:07:25 +0200 Subject: [PATCH 018/373] Expose some API from taurus.cli Expose register_subcommands() and taurus_cmd() in taurus.cli --- lib/taurus/cli/__init__.py | 2 +- lib/taurus/cli/cli.py | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/taurus/cli/__init__.py b/lib/taurus/cli/__init__.py index beaad79a5..2321f0456 100644 --- a/lib/taurus/cli/__init__.py +++ b/lib/taurus/cli/__init__.py @@ -37,4 +37,4 @@ """ -from .cli import main \ No newline at end of file +from .cli import main, register_subcommands, taurus_cmd \ No newline at end of file diff --git a/lib/taurus/cli/cli.py b/lib/taurus/cli/cli.py index c6bbc4bf2..5ac14f882 100644 --- a/lib/taurus/cli/cli.py +++ b/lib/taurus/cli/cli.py @@ -81,11 +81,8 @@ def taurus_cmd(log_level, polling_period, serialization_mode, rconsole_port, setattr(tauruscustomsettings, 'DEFAULT_FORMATTER', default_formatter) -def main(): - # set the log level to WARNING avoid spamming the CLI while loading - # subcommands - # it will be restored to the desired one first thing in taurus_cmd() - taurus.setLogLevel(taurus.Warning) +def register_subcommands(): + """Discover and add subcommands to taurus_cmd""" # Add subcommands from the taurus_subcommands entry point for ep in pkg_resources.iter_entry_points('taurus.cli.subcommands'): @@ -106,7 +103,19 @@ def main(): continue # ----------------------------------------------------------- taurus.warning('Cannot add "%s" subcommand to taurus. Reason: %r', - ep.name, e) + ep.name, e) + + +def main(): + """Register subcommands and run taurus_cmd""" + + # set the log level to WARNING avoid spamming the CLI while loading + # subcommands + # it will be restored to the desired one first thing in taurus_cmd() + taurus.setLogLevel(taurus.Warning) + + #register the subcommands + register_subcommands() # launch the taurus command taurus_cmd() From 91ead8287f42d6d41961d072b33814c86a07afc5 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Sun, 25 Aug 2019 22:33:38 +0200 Subject: [PATCH 019/373] Use pytest to run tests on travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eb1592037..6ac3184d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ script: - python -c "import fcntl; fcntl.fcntl(1, fcntl.F_SETFL, 0)" - set -e - docker exec -t taurus-test /bin/bash -c "cd /taurus ; python setup.py install" - - docker exec -t taurus-test /bin/bash -c "TAURUS_STARTER_WAIT=5 taurus testsuite -e 'taurus\.core\.util\.test\.test_timer'" + - docker exec -t taurus-test /bin/bash -c "cd /taurus ; python -m pytest lib/taurus" - docker exec -t taurus-test /bin/bash -c "cd /taurus ; sphinx-build -qW doc/source/ build/sphinx/html" # deploy sphinx-built docs to taurus-doc repo - if [[ "$DOCKER_IMG" == "cpascual/taurus-test:debian-stretch" && "$TRAVIS_REPO_SLUG" == "taurus-org/taurus" ]]; then From 8679c39132c327ec22b48876423b2ff0fdfc3790 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 16:48:47 +0200 Subject: [PATCH 020/373] Update CHANGELOG Use tox for testing instead of taurus-test - Use tox (with tox-conda) for creating test environments - Use the tango-cs-docker project to provide a Tango service - Use pytest instead of unittest to run the tests - Use flake8 --- .travis.yml | 55 ++++++++++++------------------------- .travis.yml-astropy | 39 ++++++++++++++++++++++++++ ci/tango_docker-compose.yml | 30 ++++++++++++++++++++ tox.ini | 38 +++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 38 deletions(-) create mode 100644 .travis.yml-astropy create mode 100644 ci/tango_docker-compose.yml create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index 6ac3184d3..704c3bb8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,4 @@ -# Use old ubuntu image as a workarount to Tango DS launch problems -group: deprecated-2017Q4 - language: python - python: - 3.6 @@ -15,46 +11,29 @@ env: global: - secure: "CCVtBQ8xIrOwUJGXxd81wyD1ng72Hf6d9y2U+5X88aVGTrOa8/hut10C+Jmnyf0NTZmGh/49eVvoWRvLDhjpECFMuO/bLkiNtVjz0VtWAHT2W98QJYmeymPzx86tGa+iAZCwlgXeRQFJCw1eqQvBYnjumMZWb9kj3fqgpqpSRH5SWnuRCmbxOoelmtTTUC8YKkzasAHYs03faR0DCq0oBmDy9nU2cfcRN7oE5wXUfEnDwNaoHbiQA4wiJbzNpBV432bIDtzD7gsFdiIT6ExJVFHi1gWB32bGZdbiDPpej7I2fW5qunbzUXS5doVhoBU67qqhI241RJ+AOBVb+sQnF8gwwi9/5/mcORiSBX7yI59YsOXYwo2YW+8PGr3OlF3t0+z92Q3uPytUXysdtVO4dExnLbV8OEzgmWCkv2M3GIajjE3isYAaBItqSBJHJnRClza4nNg2WLwmqLPBgM4AuSUZEpB/8kbz9kTecVEb13WrlTCNc8KVRGR+EGa4KmWADwOOxurxeb/NsTteOnzdyfrP2TXKeGOkN2uqBGYJaI7OoefsgLG7VF/+Sz4MTETMs/gZojwpO6igKBS1sJlcXujz/kt125b8gcSnrAiU1TjbZIBew/D40H64tpBcuk+dqF6i6HCoV2QmZ1QEpHOSoDk9FEaKMlgKhQj59/cWcI8=" - matrix: - - DOCKER_IMG=cpascual/taurus-test:debian-stretch - - DOCKER_IMG=cpascual/taurus-test:debian-buster - - DOCKER_IMG=cpascual/taurus-test:debian-stretch-py3 - - DOCKER_IMG=cpascual/taurus-test:debian-stretch-py3qt5 - matrix: - allow_failures: - - env: DOCKER_IMG=cpascual/taurus-test:debian-stretch-py3 + include: + - python: 3.6 + env: TOXENV=flake8 + - python: 3.6 + env: TOXENV=py36 before_install: - - docker run -d --name=taurus-test -h taurus-test --volume=`pwd`:/taurus $DOCKER_IMG - - sleep 10 - -script: - - python -c "import fcntl; fcntl.fcntl(1, fcntl.F_SETFL, 0)" - - set -e - - docker exec -t taurus-test /bin/bash -c "cd /taurus ; python setup.py install" - - docker exec -t taurus-test /bin/bash -c "cd /taurus ; python -m pytest lib/taurus" - - docker exec -t taurus-test /bin/bash -c "cd /taurus ; sphinx-build -qW doc/source/ build/sphinx/html" - # deploy sphinx-built docs to taurus-doc repo - - if [[ "$DOCKER_IMG" == "cpascual/taurus-test:debian-stretch" && "$TRAVIS_REPO_SLUG" == "taurus-org/taurus" ]]; then - pip install doctr ; - docker exec taurus-test /bin/bash -c "touch /taurus/build/sphinx/html/.nojekyll" ; - if [[ "${TRAVIS_BRANCH}" == "develop" ]]; then - doctr deploy . ; - else - doctr deploy "v-$TRAVIS_BRANCH" ; - fi; + # start a docker-based tango system with tango-test + - if [[ "$TOXENV" == "flake8" ]]; then + echo "Skipping tango initialization"; + else + docker-compose -f ci/tango_docker-compose.yml up -d; fi + - export TANGO_HOST=localhost:10000 -doctr: - key-path : ci/github_deploy_key.enc - deploy-repo : taurus-org/taurus-doc - require-master: false - sync: true - built-docs: build/sphinx/html/ +install: + - pip install -U tox tox-conda -before_deploy: - - docker exec -t taurus-test /bin/bash -c "cd /taurus ; python setup.py sdist" +script: + - docker version + - docker-compose version + - tox deploy: # deploy to pypi when a version tag is pushed to the official repo diff --git a/.travis.yml-astropy b/.travis.yml-astropy new file mode 100644 index 000000000..ca3b05ce9 --- /dev/null +++ b/.travis.yml-astropy @@ -0,0 +1,39 @@ +language: python + +python: + - 3.6 + +sudo: required + +services: + - docker + +env: + global: + - secure: "CCVtBQ8xIrOwUJGXxd81wyD1ng72Hf6d9y2U+5X88aVGTrOa8/hut10C+Jmnyf0NTZmGh/49eVvoWRvLDhjpECFMuO/bLkiNtVjz0VtWAHT2W98QJYmeymPzx86tGa+iAZCwlgXeRQFJCw1eqQvBYnjumMZWb9kj3fqgpqpSRH5SWnuRCmbxOoelmtTTUC8YKkzasAHYs03faR0DCq0oBmDy9nU2cfcRN7oE5wXUfEnDwNaoHbiQA4wiJbzNpBV432bIDtzD7gsFdiIT6ExJVFHi1gWB32bGZdbiDPpej7I2fW5qunbzUXS5doVhoBU67qqhI241RJ+AOBVb+sQnF8gwwi9/5/mcORiSBX7yI59YsOXYwo2YW+8PGr3OlF3t0+z92Q3uPytUXysdtVO4dExnLbV8OEzgmWCkv2M3GIajjE3isYAaBItqSBJHJnRClza4nNg2WLwmqLPBgM4AuSUZEpB/8kbz9kTecVEb13WrlTCNc8KVRGR+EGa4KmWADwOOxurxeb/NsTteOnzdyfrP2TXKeGOkN2uqBGYJaI7OoefsgLG7VF/+Sz4MTETMs/gZojwpO6igKBS1sJlcXujz/kt125b8gcSnrAiU1TjbZIBew/D40H64tpBcuk+dqF6i6HCoV2QmZ1QEpHOSoDk9FEaKMlgKhQj59/cWcI8=" + +before_install: + # start a docker-based tango system with tango-test + - docker-compose -f ci/tango_docker-compose.yml + +install: + # use astropy's ci-helpers to set up conda + - git clone --depth 1 git://github.com/astropy/ci-helpers.git + - source ci-helpers/travis/setup_conda.sh + +script: + - docker version + - docker-compose version + - pytest + +deploy: + # deploy to pypi when a version tag is pushed to the official repo + - provider: pypi + user: taurus_bot + skip_existing: true + password: + secure: "QjqutDroKg1ZcSXUAEGtaut9kwxHifSQKkisE+Pvd8UXElr6+inJqUbtLGkBRDDJsjVrSZi4TeLu/NfZyey/9kTQvwqrSHio80KgQ7HzuktgdmnFKfU4TWFEt4wd8LzYF4O5ljtYj4/k6txQ0zMVsLN5/SAQl44E0KSRBZBifrbeEXL3k9YI6nhw7cBiV+9XVFJBuBP5IVGhk4mOAFDT0UGyuCpMBKacfmtgDtZnYqb1SnRkFb9vT2kSPy8j3ZfZ3YPfZECwVWZtvG98/ujz1S6+mZKyErGNZc5RocBNdQFAG6AP8Epl05k+UmHO0mtHkSC+Mmh3J2grZXCKmojqcsrgJ/oP82WOQQtzZvLjYylIBC6tJ8GI0AJxUa7yukXS+x7ihkK3Xd9SoUuQri6dRlbE7iEJr7z6ZqwoiossY0feqN/v03fyJgze3KOsZ/sR1jQ2A8jdN62NzzM674w+UGhK7Q7hRRsiaODNzNrwcOrhYh4mjIXk9T8Iij223AjTixSJ29l2GUMyFgFU3KsgEUhgx8ZcL4G0olirokoMAn3wnqCbocQt7nWwUFGvQE384Br27iW598mka2njvAuww05xGCW6+/n3/aPXZYoE+DgMtYQLF8yHy7Ucvc+8mVfkrlNSkPzCF5W05JgkvVpNYznIMvnzjRcO5yoXdVUDcRM=" + on: + repo: taurus-org/taurus + tags: true + condition: "$TRAVIS_TAG =~ ^[0-9]+.[0-9]+.[0-9]+$" diff --git a/ci/tango_docker-compose.yml b/ci/tango_docker-compose.yml new file mode 100644 index 000000000..8367e352c --- /dev/null +++ b/ci/tango_docker-compose.yml @@ -0,0 +1,30 @@ +version: '2' +services: + tango-db: + image: tangocs/mysql:9.2.2 + ports: + - "9999:3306" + environment: + - MYSQL_ROOT_PASSWORD=root + tango-cs: + image: tangocs/tango-cs:9.3.2-alpha.1-no-tango-test + ports: + - "10000:10000" + environment: + - TANGO_HOST=localhost:10000 + - MYSQL_HOST=tango-db:3306 + - MYSQL_USER=tango + - MYSQL_PASSWORD=tango + - MYSQL_DATABASE=tango + links: + - "tango-db:localhost" + depends_on: + - tango-db + tango-test: + image: tangocs/tango-test:latest + environment: + - TANGO_HOST=tango-cs:10000 + links: + - "tango-cs:localhost" + depends_on: + - tango-cs diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..e5ddf8836 --- /dev/null +++ b/tox.ini @@ -0,0 +1,38 @@ +[tox] +envlist = + py36 + flake8 + +[testenv:flake8] +basepython = python +conda_deps= +deps = flake8 +commands = flake8 taurus_pyqtgraph + +[testenv] + +conda_channels= + conda-forge + tango-controls + +conda_deps= + pyqt=5 + pytango + guidata + guiqwt + lxml + future + pillow + pint + ply + pyqtgraph + pythonqwt + numpy + scipy + pymca + click + +deps= + +commands= + pytest taurus/lib From 3674cb6202adaf4b92108eed222da36303538208 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 18:03:05 +0200 Subject: [PATCH 021/373] Install conda in travis Use recipe from https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/use-conda-with-travis-ci.html --- .travis.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 704c3bb8f..b639779a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,19 @@ matrix: env: TOXENV=py36 before_install: + # install conda + - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then + wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; + else + wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; + fi + - bash miniconda.sh -b -p $HOME/miniconda + - export PATH="$HOME/miniconda/bin:$PATH" + - hash -r + - conda config --set always_yes yes --set changeps1 no + - conda update -q conda + - conda info -a + # start a docker-based tango system with tango-test - if [[ "$TOXENV" == "flake8" ]]; then echo "Skipping tango initialization"; @@ -31,8 +44,6 @@ install: - pip install -U tox tox-conda script: - - docker version - - docker-compose version - tox deploy: From b14f08d1993ea02ec43e9c3a829476f2100a5f1b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 18:20:49 +0200 Subject: [PATCH 022/373] Fix tox commands --- tox.ini | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index e5ddf8836..c64990bc3 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ envlist = basepython = python conda_deps= deps = flake8 -commands = flake8 taurus_pyqtgraph +commands = flake8 --statistics [testenv] @@ -31,8 +31,10 @@ conda_deps= scipy pymca click + pytest deps= + commands= - pytest taurus/lib + python -m pytest \ No newline at end of file From 6c63523aeb9accaea5256de33b17f054a3be24b5 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 18:29:42 +0200 Subject: [PATCH 023/373] Allow flake8 failures in travis --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index b639779a2..27cb6cc15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,10 @@ matrix: env: TOXENV=flake8 - python: 3.6 env: TOXENV=py36 + allow_failures: + - python: 3.6 + env: TOXENV=flake8 + before_install: # install conda From 6c069706d827b9766b8228001d425520e0347a76 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 18:49:39 +0200 Subject: [PATCH 024/373] Add TANGO_HOST to tangorc in travis Fix issue when discovering tests in test_tangovalidator.py --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 27cb6cc15..9119a4fa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,8 +41,8 @@ before_install: echo "Skipping tango initialization"; else docker-compose -f ci/tango_docker-compose.yml up -d; + echo 'TANGO_HOST=localhost:10000' >> $HOME/.tangorc; fi - - export TANGO_HOST=localhost:10000 install: - pip install -U tox tox-conda From 5d25d1eb39470db8d5b6f8c9e0f7f73162b023f7 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 18:53:18 +0200 Subject: [PATCH 025/373] Limit pytest discovery to lib in tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c64990bc3..11151d36c 100644 --- a/tox.ini +++ b/tox.ini @@ -37,4 +37,4 @@ deps= commands= - python -m pytest \ No newline at end of file + python -m pytest lib/taurus \ No newline at end of file From 08e789c4002d597974440eabdab55eb1cbf3503e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 18:53:53 +0200 Subject: [PATCH 026/373] Add pyepix dependency in tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 11151d36c..e5f77c19a 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ conda_channels= conda_deps= pyqt=5 pytango + pyepics guidata guiqwt lxml From 143ad9287b4ab6adff57a375f401054b33979335 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 20:00:09 +0200 Subject: [PATCH 027/373] Enable xvfb in Travis and in tox --- .travis.yml | 1 + tox.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9119a4fa0..1622df7df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ sudo: required services: - docker + - xvfb env: global: diff --git a/tox.ini b/tox.ini index e5f77c19a..6038d12a4 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,7 @@ conda_deps= pymca click pytest + pytest-xvfb deps= From a7ce449ff54eafe42fc909d9f0c4ba78118e0c78 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 20:09:17 +0200 Subject: [PATCH 028/373] Turn off verboseness of wget in travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1622df7df..ff14e4d3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,9 +26,9 @@ matrix: before_install: # install conda - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; + wget -nv https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; else - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; + wget -nv https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; fi - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" From 9642badf9d67d920b4ac9c7755a0dd243a395d09 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 20:14:28 +0200 Subject: [PATCH 029/373] Add spyder dependency in tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 6038d12a4..ca3e8dda4 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,7 @@ conda_deps= scipy pymca click + spyder pytest pytest-xvfb From 233f4affc88afa6ff610d08fb4050d1ebc237282 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 20:43:22 +0200 Subject: [PATCH 030/373] Mark epics attr RW test as expected to fail This test fails in CI (likely due to initialization issues). It needs to be fixed. For now allow it to fail --- lib/taurus/core/epics/test/test_epicsattribute.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/taurus/core/epics/test/test_epicsattribute.py b/lib/taurus/core/epics/test/test_epicsattribute.py index 3a32c91f3..1716f810d 100644 --- a/lib/taurus/core/epics/test/test_epicsattribute.py +++ b/lib/taurus/core/epics/test/test_epicsattribute.py @@ -35,6 +35,7 @@ from taurus.test import insertTest, getResourcePath from taurus.core.taurusbasetypes import DataType, AttrQuality, DataFormat from taurus.core.taurusbasetypes import TaurusAttrValue +import pytest @@ -58,6 +59,7 @@ ) @unittest.skipIf(('epics' in sys.modules) is False, "epics module is not available") +@pytest.mark.xfail(reason='epics CV init issues need fixing in this test') class AttributeTestCase(unittest.TestCase): """TestCase for the taurus.Attribute helper""" _process = None From 0a408926877c9faebdd65d590963f48bb82188cc Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 20:54:21 +0200 Subject: [PATCH 031/373] Exclude QtWebkit from the test_import --- lib/taurus/test/test_import.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/taurus/test/test_import.py b/lib/taurus/test/test_import.py index 2db3cd20b..34fccb381 100644 --- a/lib/taurus/test/test_import.py +++ b/lib/taurus/test/test_import.py @@ -51,6 +51,7 @@ def testImportSubmodules(self): exclude_patterns = [r'taurus\.qt\.qtgui\.extra_.*', r'taurus\.qt\.qtgui\.qwt5', r'taurus\.external\.qt\.QtUiTools', + r'taurus\.external\.qt\.QtWebKit', r'taurus\.external\.qt\.Qwt5', ] From c085c73b748d25f575e8acf0e5098c85f778c4a3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 27 Aug 2019 23:09:18 +0200 Subject: [PATCH 032/373] reduce verbosity of pytest --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ca3e8dda4..a7ed23afc 100644 --- a/tox.ini +++ b/tox.ini @@ -40,4 +40,4 @@ deps= commands= - python -m pytest lib/taurus \ No newline at end of file + python -m pytest lib/taurus --show-capture=no From 60e9154c2ce0db1e3f95aaf2e25660c7971b0a93 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 28 Aug 2019 00:13:36 +0200 Subject: [PATCH 033/373] Add test environments for 27{,-qt}, 35, 37 --- .travis.yml | 16 ++++++++++------ tox.ini | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index ff14e4d3c..cea57d7b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,17 @@ env: matrix: include: - python: 3.6 - env: TOXENV=flake8 + env: TOXENV=py27-qt4 + - python: 3.6 + env: TOXENV=py27 + - python: 3.6 + env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 + - python: 3.7 + env: TOXENV=py37 + - python: 3.6 + env: TOXENV=flake8 allow_failures: - python: 3.6 env: TOXENV=flake8 @@ -25,11 +33,7 @@ matrix: before_install: # install conda - - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - wget -nv https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; - else - wget -nv https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - fi + - wget -nv https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - hash -r diff --git a/tox.ini b/tox.ini index a7ed23afc..e94c72689 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,10 @@ [tox] envlist = + py27-qt4 + py27 + py35 py36 + py37 flake8 [testenv:flake8] @@ -16,11 +20,15 @@ conda_channels= tango-controls conda_deps= - pyqt=5 + qt4: pyqt=4 + !qt4: pyqt=5 pytango pyepics guidata - guiqwt + py27: cython + py35: cython + py36: guiqwt + py37: guiqwt lxml future pillow @@ -30,13 +38,15 @@ conda_deps= pythonqwt numpy scipy - pymca + !py35: pymca click spyder pytest pytest-xvfb deps= + py27: guiqwt + py35: guiqwt commands= From e15e6d140d6fae46a88b686bf0ffdcf40527a575 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 28 Aug 2019 10:20:24 +0200 Subject: [PATCH 034/373] Simplify travis config: always use python 3.6 in host Since the tests are done in conda, the python version of the system is not relevant. Therefore, always use the same (python3.6) --- .travis.yml | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index cea57d7b7..a4bd34e72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,21 +14,14 @@ env: matrix: include: - - python: 3.6 - env: TOXENV=py27-qt4 - - python: 3.6 - env: TOXENV=py27 - - python: 3.6 - env: TOXENV=py35 - - python: 3.6 - env: TOXENV=py36 - - python: 3.7 - env: TOXENV=py37 - - python: 3.6 - env: TOXENV=flake8 + - env: TOXENV=py27-qt4 + - env: TOXENV=py27 + - env: TOXENV=py35 + - env: TOXENV=py36 + - env: TOXENV=py37 + - env: TOXENV=flake8 allow_failures: - - python: 3.6 - env: TOXENV=flake8 + - env: TOXENV=flake8 before_install: From 5aed665c4172cdb33a45cca64e4b971ad14ad6fc Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 28 Aug 2019 14:53:48 +0200 Subject: [PATCH 035/373] Re-add deployment of docs by travis Create the py37-docs tox env and use it in travis to deploy to gh pages --- .travis.yml | 24 +++++++++++++++++++++++- tox.ini | 25 ++++++++++++++----------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index a4bd34e72..523f664a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,12 +14,13 @@ env: matrix: include: + - env: TOXENV=flake8 + - env: TOXENV=py37-docs - env: TOXENV=py27-qt4 - env: TOXENV=py27 - env: TOXENV=py35 - env: TOXENV=py36 - env: TOXENV=py37 - - env: TOXENV=flake8 allow_failures: - env: TOXENV=flake8 @@ -48,6 +49,27 @@ install: script: - tox +after_success: + # Deploy docs to taurus-org/taurus-doc + - if [[ "$TOXENV" == "py37-docs" && "$TRAVIS_REPO_SLUG" == "taurus-org/taurus" ]]; then + pip install doctr ; + touch build/sphinx/html/.nojekyll; + if [[ "${TRAVIS_BRANCH}" == "develop" ]]; then + doctr deploy . ; + else + doctr deploy "v-$TRAVIS_BRANCH" ; + fi; + else + echo "Skip doc deployment for $TOXENV in $TRAVIS_REPO_SLUG"; + fi + +doctr: + key-path : ci/github_deploy_key.enc + deploy-repo : taurus-org/taurus-doc + require-master: false + sync: true + built-docs: build/sphinx/html/ + deploy: # deploy to pypi when a version tag is pushed to the official repo - provider: pypi diff --git a/tox.ini b/tox.ini index e94c72689..fa71c887d 100644 --- a/tox.ini +++ b/tox.ini @@ -5,20 +5,13 @@ envlist = py35 py36 py37 + py37-docs flake8 -[testenv:flake8] -basepython = python -conda_deps= -deps = flake8 -commands = flake8 --statistics - [testenv] - conda_channels= conda-forge tango-controls - conda_deps= qt4: pyqt=4 !qt4: pyqt=5 @@ -43,11 +36,21 @@ conda_deps= spyder pytest pytest-xvfb - + docs: sphinx + docs: sphinx_rtd_theme + docs: graphviz deps= py27: guiqwt py35: guiqwt - - commands= python -m pytest lib/taurus --show-capture=no + +[testenv:py37-docs] +commands= + sphinx-build -qW doc/source/ build/sphinx/html + +[testenv:flake8] +basepython = python +conda_deps= +deps = flake8 +commands = flake8 --statistics \ No newline at end of file From 4b40b4ba8c677136adedced3ce12809668996e07 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 28 Aug 2019 16:38:33 +0200 Subject: [PATCH 036/373] Add support for pyside2 environments - Add -ps2 environments (where Pyside2 is installed instead of PyQt). - Add py37-ps2 to the Travis build matrix list (and allow it to fail). - Rename the PyQt5 environments to have the "-qt5" suffix --- .travis.yml | 12 +++++++----- tox.ini | 13 ++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 523f664a1..584231dfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,14 +15,16 @@ env: matrix: include: - env: TOXENV=flake8 - - env: TOXENV=py37-docs + - env: TOXENV=py37-qt5-docs + - env: TOXENV=py37-ps2 + - env: TOXENV=py37-qt5 + - env: TOXENV=py36-qt5 + - env: TOXENV=py35-qt5 + - env: TOXENV=py27-qt5 - env: TOXENV=py27-qt4 - - env: TOXENV=py27 - - env: TOXENV=py35 - - env: TOXENV=py36 - - env: TOXENV=py37 allow_failures: - env: TOXENV=flake8 + - env: TOXENV=py37-ps2 before_install: diff --git a/tox.ini b/tox.ini index fa71c887d..19ff19925 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,9 @@ [tox] envlist = py27-qt4 - py27 - py35 - py36 - py37 - py37-docs + py{27,35,36,37}-qt5 + py37-ps2 + py37-qt5-docs flake8 [testenv] @@ -14,7 +12,8 @@ conda_channels= tango-controls conda_deps= qt4: pyqt=4 - !qt4: pyqt=5 + qt5: pyqt=5 + ps2: pyside2 pytango pyepics guidata @@ -45,7 +44,7 @@ deps= commands= python -m pytest lib/taurus --show-capture=no -[testenv:py37-docs] +[testenv:py37-qt5-docs] commands= sphinx-build -qW doc/source/ build/sphinx/html From 39f160c52a2105c230b18aced541fce90c3ad172 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 28 Aug 2019 17:20:52 +0200 Subject: [PATCH 037/373] Mark TimerTest as flaky TimerTest may spuriously fail. Mark it as flaky and require it to pass at least two out of 3 times. Also instal flaky dependency. --- lib/taurus/core/util/test/test_timer.py | 2 ++ tox.ini | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/taurus/core/util/test/test_timer.py b/lib/taurus/core/util/test/test_timer.py index f23cb0329..a6b178931 100644 --- a/lib/taurus/core/util/test/test_timer.py +++ b/lib/taurus/core/util/test/test_timer.py @@ -33,9 +33,11 @@ import threading import numpy import unittest +import pytest from taurus.core.util.timer import Timer +@pytest.mark.flaky(max_runs=3, min_passes=2) class TimerTest(unittest.TestCase): '''Test case for testing the taurus.core.util.timer.Timer class''' diff --git a/tox.ini b/tox.ini index 19ff19925..c90ac2a53 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,7 @@ conda_deps= spyder pytest pytest-xvfb + flaky docs: sphinx docs: sphinx_rtd_theme docs: graphviz From 8aaf69772d8c7e542ec99d45dc87ae1c3d88f441 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 28 Aug 2019 17:53:00 +0200 Subject: [PATCH 038/373] Remove pyside2 test from allow_failures in travis The py37-ps2 test is now passing. No need anymore to allowing it to fail. Also reorder tests in travis. --- .travis.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 584231dfa..602e43c0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,16 +15,15 @@ env: matrix: include: - env: TOXENV=flake8 - - env: TOXENV=py37-qt5-docs - - env: TOXENV=py37-ps2 - - env: TOXENV=py37-qt5 - - env: TOXENV=py36-qt5 - - env: TOXENV=py35-qt5 - - env: TOXENV=py27-qt5 - env: TOXENV=py27-qt4 + - env: TOXENV=py27-qt5 + - env: TOXENV=py35-qt5 + - env: TOXENV=py36-qt5 + - env: TOXENV=py37-qt5 + - env: TOXENV=py37-ps2 + - env: TOXENV=py37-qt5-docs allow_failures: - env: TOXENV=flake8 - - env: TOXENV=py37-ps2 before_install: From e4c7f390a8cf41dee0cf2834915bb26affeac726 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 28 Aug 2019 18:20:28 +0200 Subject: [PATCH 039/373] Mark TaurusValueTest tests as flaky --- lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py index d973560e6..3854a5bae 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py @@ -26,11 +26,13 @@ """Test for taurus.qt.qtgui.panel.taurusvalue""" import unittest +import pytest from taurus.test import insertTest from taurus.qt.qtgui.test import BaseWidgetTestCase from taurus.qt.qtgui.panel import TaurusValue from taurus.core.tango.test import TangoSchemeTestLauncher + DEV_NAME = TangoSchemeTestLauncher.DEV_NAME @@ -59,6 +61,7 @@ def test_bug126(self): self.assertEqual(label, shownLabel, msg) self.assertMaxDeprecations(1) + @pytest.mark.flaky def texts(self, model=None, expected=None, fgRole=None, maxdepr=0): """Checks the texts for scalar attributes""" self._widget.setModel(model) @@ -75,6 +78,7 @@ def texts(self, model=None, expected=None, fgRole=None, maxdepr=0): self.assertEqual(got, expected, msg) self.assertMaxDeprecations(maxdepr) + @pytest.mark.flaky def test_labelCaseSensitivity(self): """Verify that case is respected of in the label widget""" w = self._widget From d50734707d6a1f0f937c5ab2dab758b58450e7c6 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 28 Aug 2019 19:21:16 +0200 Subject: [PATCH 040/373] Avoid deprecation warning in TaurusValue TaurusValue uses an import of taurus.qt.qtgui.qwt5 to check if TaurusArrayEditorButton should be available. This yields a deprecation warning that breaks py2-qt4 tests. Replace the import by an import of PyQt4.Qwt5 which does not yield the warning. --- lib/taurus/qt/qtgui/panel/taurusvalue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index 2ef054ae8..42d1bf090 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -610,7 +610,7 @@ def getDefaultWriteWidgetClass(self, returnAll=False): result = [TaurusValuesTableButton_W, TaurusValueLineEdit] if modelType in (DataType.Float, DataType.Integer): try: - import taurus.qt.qtgui.qwt5 + import PyQt4.Qwt5 # noqa result.insert(0, TaurusArrayEditorButton) except: pass From 3f6856dd387385d74acbb597edb0e5b6e9cfcb25 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 29 Aug 2019 16:31:13 +0200 Subject: [PATCH 041/373] Add .tox dir to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c120963b4..d4425499f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ MANIFEST /setup.cfg doc/~thumbnails.zip /lib/taurus.egg-info +/.tox/ From dd94c68d495b2ebbb3bbaa422712398a6b060d7e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 29 Aug 2019 16:33:00 +0200 Subject: [PATCH 042/373] Fix docs build in tox (and travis) travis The sphinx build build was failing in py37-qt5-docs because of a missing QT_QPA_PLATFORM envvar in the environment variable (as a part of tox efforts to isolate environments, only a few variables are passed to the environment). Fix it by declaring QT_QPA_PLATFORM=offscreen in a setenv tox directive. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index c90ac2a53..e609c41f2 100644 --- a/tox.ini +++ b/tox.ini @@ -46,6 +46,8 @@ commands= python -m pytest lib/taurus --show-capture=no [testenv:py37-qt5-docs] +setenv= + QT_QPA_PLATFORM = offscreen commands= sphinx-build -qW doc/source/ build/sphinx/html From 69575de15ddc2eb36a278a88b929e394daa501c2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 29 Aug 2019 17:00:35 +0200 Subject: [PATCH 043/373] Fix typo in docs deployment condition --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 602e43c0d..e356dccfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ script: after_success: # Deploy docs to taurus-org/taurus-doc - - if [[ "$TOXENV" == "py37-docs" && "$TRAVIS_REPO_SLUG" == "taurus-org/taurus" ]]; then + - if [[ "$TOXENV" == "py37-qt5-docs" && "$TRAVIS_REPO_SLUG" == "taurus-org/taurus" ]]; then pip install doctr ; touch build/sphinx/html/.nojekyll; if [[ "${TRAVIS_BRANCH}" == "develop" ]]; then From 1a5eb2139105c8f2b7f67b96dcce5de6ed7e06b0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 2 Sep 2019 13:03:44 +0200 Subject: [PATCH 044/373] Delete .travis.yml-astropy Remove file that was added by mistake --- .travis.yml-astropy | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 .travis.yml-astropy diff --git a/.travis.yml-astropy b/.travis.yml-astropy deleted file mode 100644 index ca3b05ce9..000000000 --- a/.travis.yml-astropy +++ /dev/null @@ -1,39 +0,0 @@ -language: python - -python: - - 3.6 - -sudo: required - -services: - - docker - -env: - global: - - secure: "CCVtBQ8xIrOwUJGXxd81wyD1ng72Hf6d9y2U+5X88aVGTrOa8/hut10C+Jmnyf0NTZmGh/49eVvoWRvLDhjpECFMuO/bLkiNtVjz0VtWAHT2W98QJYmeymPzx86tGa+iAZCwlgXeRQFJCw1eqQvBYnjumMZWb9kj3fqgpqpSRH5SWnuRCmbxOoelmtTTUC8YKkzasAHYs03faR0DCq0oBmDy9nU2cfcRN7oE5wXUfEnDwNaoHbiQA4wiJbzNpBV432bIDtzD7gsFdiIT6ExJVFHi1gWB32bGZdbiDPpej7I2fW5qunbzUXS5doVhoBU67qqhI241RJ+AOBVb+sQnF8gwwi9/5/mcORiSBX7yI59YsOXYwo2YW+8PGr3OlF3t0+z92Q3uPytUXysdtVO4dExnLbV8OEzgmWCkv2M3GIajjE3isYAaBItqSBJHJnRClza4nNg2WLwmqLPBgM4AuSUZEpB/8kbz9kTecVEb13WrlTCNc8KVRGR+EGa4KmWADwOOxurxeb/NsTteOnzdyfrP2TXKeGOkN2uqBGYJaI7OoefsgLG7VF/+Sz4MTETMs/gZojwpO6igKBS1sJlcXujz/kt125b8gcSnrAiU1TjbZIBew/D40H64tpBcuk+dqF6i6HCoV2QmZ1QEpHOSoDk9FEaKMlgKhQj59/cWcI8=" - -before_install: - # start a docker-based tango system with tango-test - - docker-compose -f ci/tango_docker-compose.yml - -install: - # use astropy's ci-helpers to set up conda - - git clone --depth 1 git://github.com/astropy/ci-helpers.git - - source ci-helpers/travis/setup_conda.sh - -script: - - docker version - - docker-compose version - - pytest - -deploy: - # deploy to pypi when a version tag is pushed to the official repo - - provider: pypi - user: taurus_bot - skip_existing: true - password: - secure: "QjqutDroKg1ZcSXUAEGtaut9kwxHifSQKkisE+Pvd8UXElr6+inJqUbtLGkBRDDJsjVrSZi4TeLu/NfZyey/9kTQvwqrSHio80KgQ7HzuktgdmnFKfU4TWFEt4wd8LzYF4O5ljtYj4/k6txQ0zMVsLN5/SAQl44E0KSRBZBifrbeEXL3k9YI6nhw7cBiV+9XVFJBuBP5IVGhk4mOAFDT0UGyuCpMBKacfmtgDtZnYqb1SnRkFb9vT2kSPy8j3ZfZ3YPfZECwVWZtvG98/ujz1S6+mZKyErGNZc5RocBNdQFAG6AP8Epl05k+UmHO0mtHkSC+Mmh3J2grZXCKmojqcsrgJ/oP82WOQQtzZvLjYylIBC6tJ8GI0AJxUa7yukXS+x7ihkK3Xd9SoUuQri6dRlbE7iEJr7z6ZqwoiossY0feqN/v03fyJgze3KOsZ/sR1jQ2A8jdN62NzzM674w+UGhK7Q7hRRsiaODNzNrwcOrhYh4mjIXk9T8Iij223AjTixSJ29l2GUMyFgFU3KsgEUhgx8ZcL4G0olirokoMAn3wnqCbocQt7nWwUFGvQE384Br27iW598mka2njvAuww05xGCW6+/n3/aPXZYoE+DgMtYQLF8yHy7Ucvc+8mVfkrlNSkPzCF5W05JgkvVpNYznIMvnzjRcO5yoXdVUDcRM=" - on: - repo: taurus-org/taurus - tags: true - condition: "$TRAVIS_TAG =~ ^[0-9]+.[0-9]+.[0-9]+$" From 56476fe35970fa9b90d5505dbcfd6cc29d41d2d3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 4 Sep 2019 19:47:10 +0200 Subject: [PATCH 045/373] Avoid resolving dns aliases in tangohost fqdn expansion taurus.core.tango expands the tangohost names using socket.getfqdn() in order to normalize PQDN names into FQDN when generating the model full names. This has the unwanted side-effect that if the host name is defined as an alias in the ressolv chain, the target of the alias is returned. This causes problems with events tango events (the DB name in the client does not match that of the DB itself and the events are lost). It can also cause trouble if the expanded model names are stored in some persistent media and the target of the alias is changed in the meanwhile (e.g. if the Tango DB is served from a cloud provider that dynamically changes the actual machine running it). Fix these issues by replacing socket.getfqdn() by an alternative that behaves like it except in that it honors the aliases. --- lib/taurus/core/tango/tangodatabase.py | 4 ++-- lib/taurus/core/tango/tangovalidator.py | 10 +++++----- lib/taurus/core/tango/test/test_tangovalidator.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/taurus/core/tango/tangodatabase.py b/lib/taurus/core/tango/tangodatabase.py index cb138566e..0a46dd250 100644 --- a/lib/taurus/core/tango/tangodatabase.py +++ b/lib/taurus/core/tango/tangodatabase.py @@ -34,7 +34,6 @@ import collections import os -import socket import weakref from PyTango import (Database, DeviceProxy, DevFailed, ApiUtil) @@ -43,6 +42,7 @@ from taurus.core.taurusauthority import TaurusAuthority from taurus.core.util.containers import CaselessDict from taurus.core.util.log import taurus4_deprecation +from taurus.core.util.fqdn import fqdn_no_alias __all__ = ["TangoInfo", "TangoAttrInfo", "TangoDevInfo", "TangoServInfo", @@ -679,7 +679,7 @@ def __init__(self, host=None, port=None, parent=None): warning("Error getting default Tango host") # Set host to fqdn - host = socket.getfqdn(host) + host = fqdn_no_alias(host) self.dbObj = Database(host, port) self._dbProxy = None diff --git a/lib/taurus/core/tango/tangovalidator.py b/lib/taurus/core/tango/tangovalidator.py index 81d2eaece..0b7b4ce59 100644 --- a/lib/taurus/core/tango/tangovalidator.py +++ b/lib/taurus/core/tango/tangovalidator.py @@ -30,10 +30,10 @@ __docformat__ = "restructuredtext" -import socket from taurus.core.taurusvalidator import (TaurusAttributeNameValidator, TaurusDeviceNameValidator, TaurusAuthorityNameValidator) +from taurus.core.util.fqdn import fqdn_no_alias # todo: I do not understand the behaviour of getNames for Auth, Dev and Attr in @@ -63,7 +63,7 @@ def getUriGroups(self, name, strict=None): ''' ret = TaurusAuthorityNameValidator.getUriGroups(self, name, strict) if ret is not None: - fqdn = socket.getfqdn(ret["host"]) + fqdn = fqdn_no_alias(ret["host"]) ret["host"] = fqdn ret["authority"] = "//{host}:{port}".format(**ret) return ret @@ -97,7 +97,7 @@ def getUriGroups(self, name, strict=None): ''' ret = TaurusDeviceNameValidator.getUriGroups(self, name, strict) if ret is not None and ret.get("host", None) is not None: - fqdn = socket.getfqdn(ret["host"]) + fqdn = fqdn_no_alias(ret["host"]) ret["host"] = fqdn ret["authority"] = "//{host}:{port}".format(**ret) return ret @@ -122,7 +122,7 @@ def getNames(self, fullname, factory=None, queryAuth=True): import PyTango host, port = PyTango.ApiUtil.get_env_var('TANGO_HOST').split(":") # Get the fully qualified domain name - host = socket.getfqdn(host) + host = fqdn_no_alias(host) default_authority = "//{0}:{1}".format(host, port) authority = groups.get('authority') @@ -222,7 +222,7 @@ def getUriGroups(self, name, strict=None): ''' ret = TaurusAttributeNameValidator.getUriGroups(self, name, strict) if ret is not None and ret.get("host", None) is not None: - fqdn = socket.getfqdn(ret["host"]) + fqdn = fqdn_no_alias(ret["host"]) ret["host"] = fqdn ret["authority"] = "//{host}:{port}".format(**ret) return ret diff --git a/lib/taurus/core/tango/test/test_tangovalidator.py b/lib/taurus/core/tango/test/test_tangovalidator.py index 5eb677c96..17c4ea034 100644 --- a/lib/taurus/core/tango/test/test_tangovalidator.py +++ b/lib/taurus/core/tango/test/test_tangovalidator.py @@ -38,11 +38,11 @@ TangoAttributeNameValidator) import PyTango -import socket +from taurus.core.util.fqdn import fqdn_no_alias __PY_TANGO_HOST = PyTango.ApiUtil.get_env_var("TANGO_HOST") host, port = __PY_TANGO_HOST.split(':') -__TANGO_HOST = "{0}:{1}".format(socket.getfqdn(host), port) +__TANGO_HOST = "{0}:{1}".format(fqdn_no_alias(host), port) #========================================================================= # Tests for Tango Authority name validation From e31c96eba59a2ba9a7d2c98548591edf30369110 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 5 Sep 2019 12:11:05 +0200 Subject: [PATCH 046/373] Add missing fqdn.py implementation Last commit was incomplete. Add the missing file. --- lib/taurus/core/util/fqdn.py | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 lib/taurus/core/util/fqdn.py diff --git a/lib/taurus/core/util/fqdn.py b/lib/taurus/core/util/fqdn.py new file mode 100644 index 000000000..fbc0a5e05 --- /dev/null +++ b/lib/taurus/core/util/fqdn.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# +""" +This module contains provides the fqdn_no_alias function. +""" + +import socket +from taurus import warning + + +def fqdn_no_alias(hostname): + """ + Normalize hostname so that it is a full name (including domain name). + + .. note:: this is similar to socket.getfqdn, but it avoids translating + aliases into their "real" address. + """ + if hostname == "localhost" or "." in hostname: + # optimization: leave localhost or names including domain as they are + return hostname + + try: + real, aliases, _ = socket.gethostbyname_ex(hostname) + except (socket.gaierror, socket.herror) as e: + # return the given hostname in case of error + warning("fqdn_no_alias: problem resolving %s: %r", hostname, e) + return hostname + + if aliases: + # return alias if it exists + if len(aliases) > 1: + # warn in the (unusual) case that there is more than 1 alias + warning("fqdn_no_alias: %s has %d aliases. Using the first one", + hostname, + len(aliases) + ) + return aliases[0] + else: + # if there are no aliases, return real + return real + + +if __name__ == '__main__': + + # TODO: convert this into a pytest test + # (but need to figure out how to generalize the "www" test) + + # test nonexistent + assert fqdn_no_alias("NONEXISTENT") == "NONEXISTENT" + # test localhost + assert fqdn_no_alias("localhost") == "localhost" + # test with an existent host in your domain (with just host name) + # TODO: this is domain-specific! it needs to be generalized somehow + print(socket.gethostbyname_ex('www')) # use something in your domain + print("socket.getfqdn:", socket.getfqdn("www")) + assert fqdn_no_alias("www") == "www.cells.es" + # test with an aliased full hostname+domain (the alias is not resolved!) + print(socket.gethostbyname_ex('mail.google.com')) + print("socket.getfqdn:", socket.getfqdn("mail.google.com")) + assert fqdn_no_alias("mail.google.com") == "mail.google.com" + From 42da14937fa6d1a1cdbc0acfc63cc49bea3a4c77 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 5 Sep 2019 14:47:24 +0200 Subject: [PATCH 047/373] fqdn: Cover the case in which hostname mathes an alias --- lib/taurus/core/util/fqdn.py | 49 +++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/lib/taurus/core/util/fqdn.py b/lib/taurus/core/util/fqdn.py index fbc0a5e05..54489ced6 100644 --- a/lib/taurus/core/util/fqdn.py +++ b/lib/taurus/core/util/fqdn.py @@ -40,7 +40,6 @@ def fqdn_no_alias(hostname): if hostname == "localhost" or "." in hostname: # optimization: leave localhost or names including domain as they are return hostname - try: real, aliases, _ = socket.gethostbyname_ex(hostname) except (socket.gaierror, socket.herror) as e: @@ -49,6 +48,11 @@ def fqdn_no_alias(hostname): return hostname if aliases: + if hostname == real or hostname in aliases: + # in some corner cases (e.g. when defining several aliases in a + # hosts file), the hostname may be one of the aliases. + return hostname + # return alias if it exists if len(aliases) > 1: # warn in the (unusual) case that there is more than 1 alias @@ -64,20 +68,31 @@ def fqdn_no_alias(hostname): if __name__ == '__main__': - # TODO: convert this into a pytest test - # (but need to figure out how to generalize the "www" test) - - # test nonexistent - assert fqdn_no_alias("NONEXISTENT") == "NONEXISTENT" - # test localhost - assert fqdn_no_alias("localhost") == "localhost" - # test with an existent host in your domain (with just host name) - # TODO: this is domain-specific! it needs to be generalized somehow - print(socket.gethostbyname_ex('www')) # use something in your domain - print("socket.getfqdn:", socket.getfqdn("www")) - assert fqdn_no_alias("www") == "www.cells.es" - # test with an aliased full hostname+domain (the alias is not resolved!) - print(socket.gethostbyname_ex('mail.google.com')) - print("socket.getfqdn:", socket.getfqdn("mail.google.com")) - assert fqdn_no_alias("mail.google.com") == "mail.google.com" + # TODO: once we move to PyTest, test_fqdn_no_alias could be put in the + # official tests (test_fqdn_no_alias_local would require to be + # generalized) + + def test_fqdn_no_alias(): + """tests for test_fqdn_no_alias""" + assert fqdn_no_alias("NONEXISTENT") == "NONEXISTENT" + # test localhost + assert fqdn_no_alias("localhost") == "localhost" + # test with an aliased full hostname+domain (alias should not resolv!) + print(socket.gethostbyname_ex('mail.google.com')) + print("socket.getfqdn:", socket.getfqdn("mail.google.com")) + assert fqdn_no_alias("mail.google.com") == "mail.google.com" + + + def test_fqdn_no_alias_local(name="www"): + import re + # test with an existent host in your domain + print(socket.gethostbyname_ex(name)) + print("socket.getfqdn:", socket.getfqdn(name)) + assert re.match(name + "\.[a-zA-Z0-9\.-]+", fqdn_no_alias(name)) + + + test_fqdn_no_alias() + test_fqdn_no_alias_local() + + From 0c8c3e51cbb8d90650b75e18270bc38a9275a051 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 5 Sep 2019 16:08:28 +0200 Subject: [PATCH 048/373] Remove warning in fqdn_no_alias The fqdn_no_alias function logs warnings when the hostname is nonexistent. This in practice is likely to spam the logs with repeated messages. Remove it. --- lib/taurus/core/util/fqdn.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/taurus/core/util/fqdn.py b/lib/taurus/core/util/fqdn.py index 54489ced6..d628be5fc 100644 --- a/lib/taurus/core/util/fqdn.py +++ b/lib/taurus/core/util/fqdn.py @@ -44,7 +44,6 @@ def fqdn_no_alias(hostname): real, aliases, _ = socket.gethostbyname_ex(hostname) except (socket.gaierror, socket.herror) as e: # return the given hostname in case of error - warning("fqdn_no_alias: problem resolving %s: %r", hostname, e) return hostname if aliases: From e2c0cb8791d369b3742e1084b8cdc49d1fdc3bed Mon Sep 17 00:00:00 2001 From: cfalcon Date: Fri, 6 Sep 2019 15:24:33 +0200 Subject: [PATCH 049/373] Fix Xwindows issues in tox env Add QT_QPA_PLATFORM = offscreen variable to all tox envs --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e609c41f2..f9fb0f594 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,8 @@ envlist = flake8 [testenv] +setenv= + QT_QPA_PLATFORM = offscreen conda_channels= conda-forge tango-controls @@ -55,4 +57,4 @@ commands= basepython = python conda_deps= deps = flake8 -commands = flake8 --statistics \ No newline at end of file +commands = flake8 --statistics From 2ecb137f9aeee6eaff08bc2b6be957882b766741 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Fri, 6 Sep 2019 16:08:43 +0200 Subject: [PATCH 050/373] Remove redundant setenv directive in tox --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index f9fb0f594..619eda053 100644 --- a/tox.ini +++ b/tox.ini @@ -48,8 +48,6 @@ commands= python -m pytest lib/taurus --show-capture=no [testenv:py37-qt5-docs] -setenv= - QT_QPA_PLATFORM = offscreen commands= sphinx-build -qW doc/source/ build/sphinx/html From 47624af9e0fedc655886cfaea7630203a2efea71 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 21:28:33 +0200 Subject: [PATCH 051/373] Change getting widget properties from implicit dct to explicit. Also change name from extra_config to widget_properties. --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 8 ++++---- lib/taurus/qt/qtgui/taurusgui/utils.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 577935c9d..4e70f34a6 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1289,12 +1289,12 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): except AttributeError: pass w = p.getWidget(sdm=Qt.qApp.SDM, setModel=False) - extra_config = p.extra_config - if extra_config: - for key in extra_config: + widget_properties = p.widget_properties + if widget_properties: + for key in widget_properties: if hasattr(w, key): try: - value = extra_config[key] + value = widget_properties[key] setattr(w, key, value) except Exception as e: msg = "Cannot set attribute '%s' of widget '%s' " \ diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index 19e42156c..fa0ea8a5b 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -381,7 +381,7 @@ def __init__(self, *args, **kwargs): self.model_in_config = kwargs.pop("modelinconfig", False) self.modifiable_by_user = kwargs.pop("modifiablebyuser", False) self.formatter = kwargs.pop("formatter", 'taurus.qt.qtgui.base.taurusbase.defaultFormatter') - self.extra_config = kwargs + self.widget_properties = kwargs.pop("widget_properties", {}) TaurusGuiComponentDescription.__init__(self, *args, **kwargs) @staticmethod @@ -417,7 +417,7 @@ def fromPanel(panel): model_in_config = panel.model_in_config modifiable_by_user = panel.modifiablebyuser formatter = panel.formatter - extra_config = panel.extra_config + widget_properties = panel.widget_properties return PanelDescription(name, classname=classname, modulename=modulename, widgetname=widgetname, floating=floating, @@ -425,7 +425,7 @@ def fromPanel(panel): sharedDataRead=sharedDataRead, model=model, icon=icon, modelinconfig=model_in_config, modifiablebyuser=modifiable_by_user, - formatter=formatter, **extra_config) + formatter=formatter, widget_properties=widget_properties) class ToolBarDescription(TaurusGuiComponentDescription): From 3a19808c0319b542e9577ab8cec835e3ff83a9f3 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 21:39:53 +0200 Subject: [PATCH 052/373] Set widget properties (modifiablebyuser, modelincofnig, formatter) in _loadCustomPanels --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 24 ++++++++-------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 4e70f34a6..04d6e0696 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -629,8 +629,7 @@ def removePanel(self, name=None): self.debug('Panel "%s" removed' % name) def createPanel(self, widget, name, floating=False, registerconfig=True, custom=False, - permanent=False, icon=None, instrumentkey=None, modelinconfig=False, - modifiablebyuser=False, formatter='taurus.qt.qtgui.base.taurusbase.defaultFormatter'): + permanent=False, icon=None, instrumentkey=None): ''' Creates a panel containing the given widget. @@ -642,9 +641,6 @@ def createPanel(self, widget, name, floating=False, registerconfig=True, custom= :param permanent: (bool) set this to True for panels that need to be recreated when restoring the app :param icon: (QIcon) icon for the panel :param instrumentkey: (str) name of an instrument to which this panel is to be associated - :param modelinconfig: (bool) whether to store model in settings file or not - :param modifiablebyuser: (bool) whether user can modify widget or not - :param formatter: (str) formatter for widget :return: (DockWidgetPanel) the created panel @@ -669,11 +665,6 @@ def createPanel(self, widget, name, floating=False, registerconfig=True, custom= self.info('Panel with name "%s" already exists. Reusing.' % name) return self.__panels[name] - if isinstance(widget, TaurusBaseComponent): - widget.setModifiableByUser(modifiablebyuser) - widget.setModelInConfig(modelinconfig) - widget.setFormat(formatter) - # create a panel panel = DockWidgetPanel(None, widget, name, self) # we will only place panels in this area @@ -1312,18 +1303,19 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): w.setModel(p.model) if p.instrumentkey is None: instrumentkey = self.IMPLICIT_ASSOCIATION + + if isinstance(w, TaurusBaseComponent): + w.setModifiableByUser(p.modifiablebyuser) + w.setModelInConfig(p.modelinconfig) + w.setFormat(p.formatter) + icon = p.icon - model_in_config = p.model_in_config - modifiable_by_user = p.modifiable_by_user - formatter = p.formatter # the pool instruments may change when the pool config changes, # so we do not store their config registerconfig = p not in poolinstruments # create a panel self.createPanel(w, p.name, floating=p.floating, registerconfig=registerconfig, - instrumentkey=instrumentkey, permanent=True, icon=icon, - modelinconfig=model_in_config, modifiablebyuser=modifiable_by_user, - formatter=formatter) + instrumentkey=instrumentkey, permanent=True, icon=icon) except Exception as e: msg = "Cannot create panel %s" % getattr( p, "name", "__Unknown__") From 060a8b161f26b21c24f580598b440a2fe5f3e39e Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 21:43:19 +0200 Subject: [PATCH 053/373] Respect widget's default configuration if no configuration is passed in PanelDescription --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 9 ++++++--- lib/taurus/qt/qtgui/taurusgui/utils.py | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 04d6e0696..fe89f54ff 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1305,9 +1305,12 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): instrumentkey = self.IMPLICIT_ASSOCIATION if isinstance(w, TaurusBaseComponent): - w.setModifiableByUser(p.modifiablebyuser) - w.setModelInConfig(p.modelinconfig) - w.setFormat(p.formatter) + if p.modifiablebyuser is not None: + w.setModifiableByUser(p.modifiablebyuser) + if p.modelinconfig is not None: + w.setModelInConfig(p.modelinconfig) + if p.formatter is not None: + w.setFormat(p.formatter) icon = p.icon # the pool instruments may change when the pool config changes, diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index fa0ea8a5b..014d5314c 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -378,10 +378,10 @@ def __init__(self, *args, **kwargs): """ self.instrumentkey = kwargs.pop('instrumentkey', None) self.icon = kwargs.pop("icon", None) - self.model_in_config = kwargs.pop("modelinconfig", False) - self.modifiable_by_user = kwargs.pop("modifiablebyuser", False) - self.formatter = kwargs.pop("formatter", 'taurus.qt.qtgui.base.taurusbase.defaultFormatter') - self.widget_properties = kwargs.pop("widget_properties", {}) + self.model_in_config = kwargs.pop("modelinconfig", None) + self.modifiable_by_user = kwargs.pop("modifiablebyuser", None) + self.formatter = kwargs.pop("formatter", None) + self.widget_properties = kwargs.pop("widget_properties", None) TaurusGuiComponentDescription.__init__(self, *args, **kwargs) @staticmethod From 4d58a4256819cbd68e3cbd7ce3cd44ba149f4aa3 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:14:02 +0200 Subject: [PATCH 054/373] Use snake_case names --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 8 ++++---- lib/taurus/qt/qtgui/taurusgui/utils.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index fe89f54ff..1e4352a32 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1305,10 +1305,10 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): instrumentkey = self.IMPLICIT_ASSOCIATION if isinstance(w, TaurusBaseComponent): - if p.modifiablebyuser is not None: - w.setModifiableByUser(p.modifiablebyuser) - if p.modelinconfig is not None: - w.setModelInConfig(p.modelinconfig) + if p.modifiable_by_user is not None: + w.setModifiableByUser(p.modifiable_by_user) + if p.model_in_config is not None: + w.setModelInConfig(p.model_in_config) if p.formatter is not None: w.setFormat(p.formatter) diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index 014d5314c..c3101abaa 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -369,8 +369,8 @@ def __init__(self, *args, **kwargs): :param args: :param instrumentkey: (str) - :param modelinconfig: (bool) whther to store model in settigns file or not - :param modifiablebyuser: (bool) whether user can edit widget or not + :param model_in_config: (bool) whther to store model in settigns file or not + :param modifiable_by_user: (bool) whether user can edit widget or not :param formatter: (str) formatter used by this widget Additionally, extra configuration options can be passed in constructor as key word arguments. @@ -378,8 +378,8 @@ def __init__(self, *args, **kwargs): """ self.instrumentkey = kwargs.pop('instrumentkey', None) self.icon = kwargs.pop("icon", None) - self.model_in_config = kwargs.pop("modelinconfig", None) - self.modifiable_by_user = kwargs.pop("modifiablebyuser", None) + self.model_in_config = kwargs.pop("model_in_config", None) + self.modifiable_by_user = kwargs.pop("modifiable_by_user", None) self.formatter = kwargs.pop("formatter", None) self.widget_properties = kwargs.pop("widget_properties", None) TaurusGuiComponentDescription.__init__(self, *args, **kwargs) From 4be8c37d3d938d4b30db41420dac518eb86ef01b Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:17:41 +0200 Subject: [PATCH 055/373] Use snake_case names with the rest --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 2 +- lib/taurus/qt/qtgui/taurusgui/utils.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 1e4352a32..627fad034 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -836,7 +836,7 @@ def createCustomPanel(self, paneldesc=None): self.createPanel(w, paneldesc.name, floating=paneldesc.floating, custom=True, registerconfig=False, instrumentkey=paneldesc.instrumentkey, - permanent=False, modelinconfig=True, modifiablebyuser=True) + permanent=False) msg = 'Panel %s created. Drag items to it or use the context menu to customize it' % w.name self.newShortMessage.emit(msg) diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index c3101abaa..10c995a78 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -415,7 +415,7 @@ def fromPanel(panel): model = None icon = panel.icon model_in_config = panel.model_in_config - modifiable_by_user = panel.modifiablebyuser + modifiable_by_user = panel.modifiable_by_user formatter = panel.formatter widget_properties = panel.widget_properties return PanelDescription(name, classname=classname, @@ -423,8 +423,8 @@ def fromPanel(panel): floating=floating, sharedDataWrite=sharedDataWrite, sharedDataRead=sharedDataRead, model=model, - icon=icon, modelinconfig=model_in_config, - modifiablebyuser=modifiable_by_user, + icon=icon, model_in_config=model_in_config, + modifiable_by_user=modifiable_by_user, formatter=formatter, widget_properties=widget_properties) From dd93d87e74086c85831eb70a064eeee6329d1149 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:20:28 +0200 Subject: [PATCH 056/373] Change name to 'widget_formatter' to avoid clashing names --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 4 ++-- lib/taurus/qt/qtgui/taurusgui/utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 627fad034..c25716bce 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1309,8 +1309,8 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): w.setModifiableByUser(p.modifiable_by_user) if p.model_in_config is not None: w.setModelInConfig(p.model_in_config) - if p.formatter is not None: - w.setFormat(p.formatter) + if p.widget_formatter is not None: + w.setFormat(p.widget_formatter) icon = p.icon # the pool instruments may change when the pool config changes, diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index 10c995a78..335ef19a3 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -371,7 +371,7 @@ def __init__(self, *args, **kwargs): :param instrumentkey: (str) :param model_in_config: (bool) whther to store model in settigns file or not :param modifiable_by_user: (bool) whether user can edit widget or not - :param formatter: (str) formatter used by this widget + :param widget_formatter: (str) formatter used by this widget Additionally, extra configuration options can be passed in constructor as key word arguments. Proper widget attributes will be set to corresponding values @@ -380,7 +380,7 @@ def __init__(self, *args, **kwargs): self.icon = kwargs.pop("icon", None) self.model_in_config = kwargs.pop("model_in_config", None) self.modifiable_by_user = kwargs.pop("modifiable_by_user", None) - self.formatter = kwargs.pop("formatter", None) + self.widget_formatter = kwargs.pop("widget_formatter", None) self.widget_properties = kwargs.pop("widget_properties", None) TaurusGuiComponentDescription.__init__(self, *args, **kwargs) @@ -416,7 +416,7 @@ def fromPanel(panel): icon = panel.icon model_in_config = panel.model_in_config modifiable_by_user = panel.modifiable_by_user - formatter = panel.formatter + widget_formatter = panel.widget_formatter widget_properties = panel.widget_properties return PanelDescription(name, classname=classname, modulename=modulename, widgetname=widgetname, @@ -425,7 +425,7 @@ def fromPanel(panel): sharedDataRead=sharedDataRead, model=model, icon=icon, model_in_config=model_in_config, modifiable_by_user=modifiable_by_user, - formatter=formatter, widget_properties=widget_properties) + widget_formatter=widget_formatter, widget_properties=widget_properties) class ToolBarDescription(TaurusGuiComponentDescription): From f666198e712c7fcea62b2a29b19944c5a56956cb Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:36:40 +0200 Subject: [PATCH 057/373] Add module for common CLI options --- lib/taurus/cli/common.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 lib/taurus/cli/common.py diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py new file mode 100644 index 000000000..52e728e8d --- /dev/null +++ b/lib/taurus/cli/common.py @@ -0,0 +1,24 @@ +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +import click From 7fe12a7508f2d605b7d2f47b3a03c2028a339313 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:53:07 +0200 Subject: [PATCH 058/373] Add common CLI options --- lib/taurus/cli/common.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 52e728e8d..cd8833bed 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -22,3 +22,39 @@ ############################################################################# import click + + +log_port = click.option('--port', 'port', type=int, + default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, + show_default=True, + help='port where log server is running') + +log_name = click.option('--log-name', 'log_name', default=None, + help='filter specific log object') + +log_level = click.option('--log-level', 'log_level', + type=click.Choice(['critical', 'error', 'warning', 'info', + 'debug', 'trace']), + default='debug', show_default=True, + help='filter specific log level') + +config_file = click.option('--config', 'config_file', type=click.File('rb'), + help='configuration file for initialization') + +def window_name(help): + window_name = click.option('--window-name', 'window_name', + default='TaurusPlot ' + help, + help='Name of the window') + return window_name + +models = click.argument('models', nargs=-1) + +x_axis_mode = click.option("-x", "--x-axis-mode", "x_axis_mode", + type=click.Choice(['t', 'n']), + default='n', + show_default=True, + help=('X axis mode. "t" implies using a Date axis' + + '"n" uses the regular axis') + ) + +demo = click.option("--demo", is_flag=True, help="show a demo of the widget") From 84059b56e8cb0e8a3da4bc207c1120fbf8cbec4e Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:56:34 +0200 Subject: [PATCH 059/373] Use common CLI options for taurusform --- lib/taurus/qt/qtgui/panel/taurusform.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 3fa02aaf9..87687c096 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -44,6 +44,7 @@ from taurus.qt.qtgui.container import TaurusWidget, TaurusScrollArea from taurus.qt.qtgui.button import QButtonBox, TaurusCommandButton from .taurusmodelchooser import TaurusModelChooser +from taurus.cli import common as cli_common __all__ = ["TaurusAttrForm", "TaurusCommandsForm", "TaurusForm"] @@ -1048,12 +1049,9 @@ def setModel(self, model): @click.command('form') -@click.option('--window-name', 'window_name', - default='Taurus Form', - help='Name of the window') -@click.option('--config', 'config_file', type=click.File('rb'), - help='configuration file for initialization') -@click.argument('models', nargs=-1) +@cli_common.window_name("") +@cli_common.config_file +@cli_common.models def form_cmd(window_name, config_file, models): """Shows a Taurus form populated with the given model names""" from taurus.qt.qtgui.application import TaurusApplication From c922006625ccbf320f6e72aaf3dc501f957eff51 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:56:45 +0200 Subject: [PATCH 060/373] Use common CLI options for taurusdevicepanel --- lib/taurus/qt/qtgui/panel/taurusdevicepanel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py b/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py index 8fb0763e6..d051d8e9a 100644 --- a/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py +++ b/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py @@ -49,6 +49,7 @@ from taurus.qt.qtgui.panel.taurusform import TaurusCommandsForm from taurus.qt.qtgui.util.ui import UILoadable from taurus.qt.qtgui.icon import getCachedPixmap +from taurus.cli import common as cli_common __all__ = ["TaurusDevicePanel", "TaurusDevPanel"] @@ -611,8 +612,7 @@ def getQtDesignerPluginInfo(cls): + '(it can be passed more than once)'), multiple=True, ) -@click.option('--config', 'config_file', type=click.File('rb'), - help='configuration file for initialization') +@cli_common.config_file def device_cmd(dev, filter, config_file): """Show a Device Panel""" import sys From 2525ea1fec41d51c3eed657e3c88ef47b75288e6 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:58:49 +0200 Subject: [PATCH 061/373] Add proper window name --- lib/taurus/cli/common.py | 4 ++-- lib/taurus/qt/qtgui/panel/taurusform.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index cd8833bed..4e18d6e53 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -41,9 +41,9 @@ config_file = click.option('--config', 'config_file', type=click.File('rb'), help='configuration file for initialization') -def window_name(help): +def window_name(default): window_name = click.option('--window-name', 'window_name', - default='TaurusPlot ' + help, + default=default, help='Name of the window') return window_name diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 87687c096..bf14b9e67 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -1049,7 +1049,7 @@ def setModel(self, model): @click.command('form') -@cli_common.window_name("") +@cli_common.window_name("TaurusForm") @cli_common.config_file @cli_common.models def form_cmd(window_name, config_file, models): From adea3564aa28c2a0210a2ec11ebaa200fb2c44d1 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 22:59:59 +0200 Subject: [PATCH 062/373] Add common CLI options for Qwt5 --- lib/taurus/qt/qtgui/qwt5/cli.py | 39 ++++++++++----------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index 6f1818ade..96613607c 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -23,6 +23,7 @@ import click from .taurustrend import TaurusTrend +from taurus.cli import common as cli_common @click.group('qwt5') @@ -32,20 +33,11 @@ def qwt5(): @qwt5.command('plot') -@click.argument('models', nargs=-1) -@click.option('--config', 'config_file', type=click.File('rb'), - help='configuration file for initialization') -@click.option("-x", "--x-axis-mode", "x_axis_mode", - type=click.Choice(['t', 'n']), - default='n', - show_default=True, - help=('X axis mode. "t" implies using a Date axis' - + '"n" uses the regular axis') - ) -@click.option("--demo", is_flag=True, help="show a demo of the widget") -@click.option('--window-name', 'window_name', - default='TaurusPlot (qwt5)', - help='Name of the window') +@cli_common.models +@cli_common.config_file +@cli_common.x_axis_mode +@cli_common.demo +@cli_common.window_name("TaurusPlot (qwt5)") def plot_cmd(models, config_file, x_axis_mode, demo, window_name): """Shows a plot for the given models""" from .taurusplot import plot_main @@ -58,14 +50,11 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): @qwt5.command('trend') -@click.argument('models', nargs=-1) -@click.option("-x", "--x-axis-mode", "x_axis_mode", - type=click.Choice(['t', 'n']), - default='n', - show_default=True, - help=('X axis mode. "t" implies using a Date axis' - + '"n" uses the regular axis') - ) +@cli_common.models +@cli_common.x_axis_mode +@cli_common.config_file +@cli_common.demo +@cli_common.window_name @click.option('-a', '--use-archiving', 'use_archiving', is_flag=True, default=False, @@ -78,12 +67,6 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): default=-1, metavar="MILLISECONDS", help="force re-reading of the attributes every MILLISECONDS ms") -@click.option('--config', 'config_file', type=click.File('rb'), - help='configuration file for initialization') -@click.option("--demo", is_flag=True, help="show a demo of the widget") -@click.option('--window-name', 'window_name', - default='TaurusPlot (qwt5)', - help='Name of the window') def trend_cmd(models, x_axis_mode, use_archiving, max_buffer_size, forced_read_period, config_file, demo, window_name): """Shows a trend for the given models""" From 003b41681f2e8a9e975063252a4b0f0e0d7d2269 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 23:03:43 +0200 Subject: [PATCH 063/373] Add common CLI options for qtgui plot --- lib/taurus/qt/qtgui/extra_guiqwt/plot.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py index 8ba2579bf..24f46265a 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py @@ -43,6 +43,7 @@ from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_ATTR_MIME_TYPE from taurus.qt.qtgui.extra_guiqwt.builder import make from taurus.qt.qtgui.extra_guiqwt.curve import TaurusCurveItem, TaurusTrendParam, TaurusTrendItem +from taurus.cli import common as cli_common __all__ = ["TaurusCurveDialog", "TaurusTrendDialog", "TaurusImageDialog"] @@ -636,10 +637,8 @@ def taurusTrendDlgMain(): show_default=True, help=('Color mode expected from the attribute') ) -@click.option("--demo", is_flag=True, help="show a demo of the widget") -@click.option('--window-name', 'window_name', - default='TaurusPlot (qwt5)', - help='Name of the window') +@cli_common.demo +@cli_common.window_name('TaurusPlot (qwt5)') def image_cmd(model, color_mode, demo, window_name): from taurus.qt.qtgui.application import TaurusApplication import sys From abbdf96ad46ac8bcb6265d2b05d8d6a4f9a40b67 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 23:08:10 +0200 Subject: [PATCH 064/373] Add common 'model' as common CLI option --- lib/taurus/cli/common.py | 2 ++ lib/taurus/qt/qtgui/extra_guiqwt/plot.py | 2 +- lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py | 9 ++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 4e18d6e53..a160fab8d 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -58,3 +58,5 @@ def window_name(default): ) demo = click.option("--demo", is_flag=True, help="show a demo of the widget") + +model = click.argument('model', nargs=1, required=False) diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py index 24f46265a..f1af1ae27 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py @@ -630,13 +630,13 @@ def taurusTrendDlgMain(): @click.command('image') -@click.argument('model', nargs=1, required=False) @click.option('-c', '--color-mode', 'color_mode', type=click.Choice(['gray', 'rgb']), default='gray', show_default=True, help=('Color mode expected from the attribute') ) +@cli_common.model @cli_common.demo @cli_common.window_name('TaurusPlot (qwt5)') def image_cmd(model, color_mode, demo, window_name): diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 90090ff48..6dc58bc8d 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -40,6 +40,7 @@ TimeAxisTool, AutoScrollTool, AutoScaleXTool, AutoScaleYTool, AutoScaleZTool) +from taurus.cli import common as cli_common class TaurusTrend2DDialog(ImageDialog, TaurusBaseWidget): @@ -309,7 +310,6 @@ def setModifiableByUser(self, modifiable): @click.command('trend2d') -@click.argument('model', nargs=1, required=False) @click.option("-x", "--x-axis-mode", "x_axis_mode", type=click.Choice(['t', 'd', 'e']), default='d', @@ -324,10 +324,9 @@ def setModifiableByUser(self, modifiable): + "(when reached, the oldest values will be " + "discarded)") ) -@click.option("--demo", is_flag=True, help="show a demo of the widget") -@click.option('--window-name', 'window_name', - default='TaurusPlot (qwt5)', - help='Name of the window') +@cli_common.model +@cli_common.demo +@cli_common.window_name('TaurusPlot (qwt5)') def trend2d_cmd(model, x_axis_mode, max_buffer_size, demo, window_name): from taurus.qt.qtgui.application import TaurusApplication From 8e6642bdf9c31fbe28a998b0dcda0c1512af1768 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 23:10:05 +0200 Subject: [PATCH 065/373] Add common 'buffer' as common CLI option --- lib/taurus/cli/common.py | 10 ++++++++++ lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py | 8 +------- lib/taurus/qt/qtgui/qwt5/cli.py | 5 +---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index a160fab8d..97b79ae35 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -60,3 +60,13 @@ def window_name(default): demo = click.option("--demo", is_flag=True, help="show a demo of the widget") model = click.argument('model', nargs=1, required=False) + +def buffer(default): + o = click.option('-b', '--buffer', 'max_buffer_size', type=int, + default=default, + show_default=True, + help=("maximum number of values to be stacked " + + "(when reached, the oldest values will be " + + "discarded)") + ) + return o diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 6dc58bc8d..3f55b163d 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -317,13 +317,7 @@ def setModifiableByUser(self, modifiable): help=("interpret X values as timestamps (t), " + "time deltas (d) or event numbers (e). ") ) -@click.option('-b', '--buffer', 'max_buffer_size', type=int, - default=512, - show_default=True, - help=("maximum number of values to be stacked " - + "(when reached, the oldest values will be " - + "discarded)") - ) +@cli_common.buffer(512) @cli_common.model @cli_common.demo @cli_common.window_name('TaurusPlot (qwt5)') diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index 96613607c..5ff7238ae 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -55,14 +55,11 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): @cli_common.config_file @cli_common.demo @cli_common.window_name +@cli_common.buffer(TaurusTrend.DEFAULT_MAX_BUFFER_SIZE) @click.option('-a', '--use-archiving', 'use_archiving', is_flag=True, default=False, help='enable automatic archiving queries') -@click.option('-b', '--buffer', 'max_buffer_size', type=int, - default=TaurusTrend.DEFAULT_MAX_BUFFER_SIZE, - show_default=True, - help='maximum number of values per curve to be plotted') @click.option('-r', '--forced-read', 'forced_read_period', type=int, default=-1, metavar="MILLISECONDS", From d8fb5e2d1d612fbfd431d5052090adcaae008455 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 23:18:48 +0200 Subject: [PATCH 066/373] Improve formatting --- lib/taurus/cli/common.py | 91 +++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 97b79ae35..a13cf150e 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -24,49 +24,74 @@ import click -log_port = click.option('--port', 'port', type=int, - default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, - show_default=True, - help='port where log server is running') +log_port = click.option( + '--port', 'port', type=int, + default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, + show_default=True, + help='port where log server is running', +) -log_name = click.option('--log-name', 'log_name', default=None, - help='filter specific log object') +log_name = click.option( + '--log-name', 'log_name', + default=None, + help='filter specific log object', +) -log_level = click.option('--log-level', 'log_level', - type=click.Choice(['critical', 'error', 'warning', 'info', - 'debug', 'trace']), - default='debug', show_default=True, - help='filter specific log level') +log_level = click.option( + '--log-level', 'log_level', + type=click.Choice(['critical', 'error', 'warning', 'info', + 'debug', 'trace']), + default='debug', show_default=True, + help='filter specific log level', +) -config_file = click.option('--config', 'config_file', type=click.File('rb'), - help='configuration file for initialization') +config_file = click.option( + '--config', 'config_file', + type=click.File('rb'), + help='configuration file for initialization', +) def window_name(default): - window_name = click.option('--window-name', 'window_name', - default=default, - help='Name of the window') + window_name = click.option( + '--window-name', 'window_name', + default=default, + help='Name of the window', + ) return window_name -models = click.argument('models', nargs=-1) +models = click.argument( + 'models', nargs=-1, +) -x_axis_mode = click.option("-x", "--x-axis-mode", "x_axis_mode", - type=click.Choice(['t', 'n']), - default='n', - show_default=True, - help=('X axis mode. "t" implies using a Date axis' - + '"n" uses the regular axis') - ) +x_axis_mode = click.option( + "-x", "--x-axis-mode", "x_axis_mode", + type=click.Choice(['t', 'n']), + default='n', + show_default=True, + help=('X axis mode. "t" implies using a Date axis' + + '"n" uses the regular axis'), +) -demo = click.option("--demo", is_flag=True, help="show a demo of the widget") +demo = click.option( + "--demo", + is_flag=True, + help="show a demo of the widget", +) -model = click.argument('model', nargs=1, required=False) +model = click.argument( + 'model', + nargs=1, + required=False, +) def buffer(default): - o = click.option('-b', '--buffer', 'max_buffer_size', type=int, - default=default, - show_default=True, - help=("maximum number of values to be stacked " - + "(when reached, the oldest values will be " - + "discarded)") - ) + o = click.option( + '-b', '--buffer', 'max_buffer_size', + type=int, + default=default, + show_default=True, + help=("maximum number of values to be stacked " + + "(when reached, the oldest values will be " + + "discarded)"), + ) return o From f5d0cc76b54c93760542085b58d8b56c414414ad Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 23:21:05 +0200 Subject: [PATCH 067/373] Use common CLI options in log monitors --- lib/taurus/core/util/remotelogmonitor.py | 16 +++++----------- lib/taurus/qt/qtgui/table/qlogtable.py | 14 +++----------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/lib/taurus/core/util/remotelogmonitor.py b/lib/taurus/core/util/remotelogmonitor.py index 9fbf673f1..2264ca772 100644 --- a/lib/taurus/core/util/remotelogmonitor.py +++ b/lib/taurus/core/util/remotelogmonitor.py @@ -42,6 +42,8 @@ import socketserver +from taurus.cli import common as cli_common + _all__ = ["LogRecordStreamHandler", "LogRecordSocketReceiver", "log"] @@ -183,17 +185,9 @@ def log(host, port, name=None, level=None): @click.command('logmon') -@click.option('--port', 'port', type=int, - default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, - show_default=True, - help='port where log server is running') -@click.option('--log-name', 'log_name', default=None, - help='filter specific log object') -@click.option('--log-level', 'log_level', - type=click.Choice(['critical', 'error', 'warning', 'info', - 'debug', 'trace']), - default='debug', show_default=True, - help='filter specific log level') +@cli_common.log_port +@cli_common.log_name +@cli_common.log_level def logmon_cmd(port, log_name, log_level): """Show the console-based Taurus Remote Log Monitor""" import taurus diff --git a/lib/taurus/qt/qtgui/table/qlogtable.py b/lib/taurus/qt/qtgui/table/qlogtable.py index f0de1be3f..3deb27a1e 100644 --- a/lib/taurus/qt/qtgui/table/qlogtable.py +++ b/lib/taurus/qt/qtgui/table/qlogtable.py @@ -600,17 +600,9 @@ def main(): @click.command('qlogmon') -@click.option('--port', 'port', type=int, - default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, - show_default=True, - help='port where log server is running') -@click.option('--log-name', 'log_name', default=None, - help='filter specific log object') -@click.option('--log-level', 'log_level', - type=click.Choice(['critical', 'error', 'warning', 'info', - 'debug', 'trace']), - default='debug', show_default=True, - help='filter specific log level') +@cli_common.log_port +@cli_common.log_name +@cli_common.log_level def qlogmon_cmd(port, log_name, log_level): """Show the Taurus Remote Log Monitor""" import taurus From 95c1c38499b48cfa9aaeab0593155c8c66c33b75 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Wed, 11 Sep 2019 23:21:34 +0200 Subject: [PATCH 068/373] Add missing import --- lib/taurus/qt/qtgui/table/qlogtable.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/taurus/qt/qtgui/table/qlogtable.py b/lib/taurus/qt/qtgui/table/qlogtable.py index 3deb27a1e..eb1cc8175 100644 --- a/lib/taurus/qt/qtgui/table/qlogtable.py +++ b/lib/taurus/qt/qtgui/table/qlogtable.py @@ -47,6 +47,7 @@ from taurus.external.qt import Qt from taurus.qt.qtgui.model import FilterToolBar from taurus.qt.qtgui.util import ActionFactory +from taurus.cli import common as cli_common from .qtable import QBaseTableWidget From 2762ee1bd1a992ae8004a668eeddabaf4332b8e5 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 12 Sep 2019 12:39:35 +0200 Subject: [PATCH 069/373] Add missing argument --- lib/taurus/qt/qtgui/qwt5/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index 5ff7238ae..d8cd55444 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -54,7 +54,7 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): @cli_common.x_axis_mode @cli_common.config_file @cli_common.demo -@cli_common.window_name +@cli_common.window_name('TaurusPlot (qwt5)') @cli_common.buffer(TaurusTrend.DEFAULT_MAX_BUFFER_SIZE) @click.option('-a', '--use-archiving', 'use_archiving', is_flag=True, From e0c7666d2021350d498c1f7e2caf436bdad1ab56 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Thu, 12 Sep 2019 17:34:04 +0200 Subject: [PATCH 070/373] Fix removeAttributeFromPolling method This method assumes that an attribute only can be polled once. Modify implementation to remove the attribute from all polling timers. Fix docstring. --- lib/taurus/core/taurusfactory.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/taurus/core/taurusfactory.py b/lib/taurus/core/taurusfactory.py index 230c21516..f1d632eec 100644 --- a/lib/taurus/core/taurusfactory.py +++ b/lib/taurus/core/taurusfactory.py @@ -340,7 +340,7 @@ def addAttributeToPolling(self, attribute, period, unsubscribe_evts=False): """Activates the polling (client side) for the given attribute with the given period (seconds). - :param attribute: (taurus.core.tango.TangoAttribute) attribute name. + :param attribute: (taurus.core.tango.TaurusAttribute) attribute name. :param period: (float) polling period (in seconds) :param unsubscribe_evts: (bool) whether or not to unsubscribe from events """ @@ -354,15 +354,11 @@ def removeAttributeFromPolling(self, attribute): :param attribute: (str) attribute name. """ - p = None - for period, timer in self.polling_timers.items(): - if timer.containsAttribute(attribute): - timer.removeAttribute(attribute) - if timer.getAttributeCount() == 0: - p = period - break - if p: - del self.polling_timers[period] + timers = dict(self.polling_timers) + for period, timer in timers.items(): + timer.removeAttribute(attribute) + if not timer.dev_dict: + del self.polling_timers[period] def __str__(self): return '{0}()'.format(self.__class__.__name__) From 4c122ffe431ee51b9fe134296497d0f1f5b80f26 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Thu, 12 Sep 2019 17:38:43 +0200 Subject: [PATCH 071/373] Avoid circular reference with TaurusDevice The TaurusPollingTimer keeps device strong references for creating the weakref dictionary of polled attributes. The removeAttribute does not cleanup the structures and many of TaurusPollingTimer methods are not thread-safe. Fix removeAttribute implementation to delete unreferenced elements. Protect the methods using the lock or making copy of the iterated containers. Fix #999 --- lib/taurus/core/tauruspollingtimer.py | 59 +++++++++++++++------------ 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/lib/taurus/core/tauruspollingtimer.py b/lib/taurus/core/tauruspollingtimer.py index 31f787d06..0abf035ce 100644 --- a/lib/taurus/core/tauruspollingtimer.py +++ b/lib/taurus/core/tauruspollingtimer.py @@ -93,21 +93,22 @@ def addAttribute(self, attribute, auto_start=True): that it should startup as soon as there is at least one attribute registered. """ - dev, attr_name = attribute.getParentObj(), attribute.getSimpleName() - attr_dict = self.dev_dict.get(dev) - if attr_dict is None: - if attribute.factory().caseSensitive: - self.dev_dict[dev] = attr_dict = weakref.WeakValueDictionary() + with self.lock: + dev, attr_name = attribute.getParentObj(), attribute.getSimpleName() + attr_dict = self.dev_dict.get(dev) + if attr_dict is None: + if attribute.factory().caseSensitive: + self.dev_dict[dev] = attr_dict = weakref.WeakValueDictionary() + else: + self.dev_dict[dev] = attr_dict = CaselessWeakValueDict() + if attr_name not in attr_dict: + attr_dict[attr_name] = attribute + self.attr_nb += 1 + if self.attr_nb == 1 and auto_start: + self.start() else: - self.dev_dict[dev] = attr_dict = CaselessWeakValueDict() - if attr_name not in attr_dict: - attr_dict[attr_name] = attribute - self.attr_nb += 1 - if self.attr_nb == 1 and auto_start: - self.start() - else: - import taurus - taurus.Manager().enqueueJob(attribute.poll) + import taurus + taurus.Manager().enqueueJob(attribute.poll) def removeAttribute(self, attribute): """Unregisters the attribute from this polling. If the number of registered @@ -116,32 +117,40 @@ def removeAttribute(self, attribute): :param attribute: (taurus.core.taurusattribute.TaurusAttribute) the attribute to be added """ - dev, attr_name = attribute.getParentObj(), attribute.getSimpleName() - attr_dict = self.dev_dict.get(dev) - if attr_dict is None: - return - if attr_name in attr_dict: - del attr_dict[attr_name] + with self.lock: + dev, attr_name = attribute.getParentObj(), attribute.getSimpleName() + attr_dict = self.dev_dict.get(dev) + if attr_dict is None: + return + if attr_name in attr_dict: + del attr_dict[attr_name] + self.attr_nb -= 1 if not attr_dict: del self.dev_dict[dev] - self.attr_nb -= 1 - if self.attr_nb < 1: - self.stop() + if not self.dev_dict: + self.stop() def _pollAttributes(self): """Polls the registered attributes. This method is called by the timer when it is time to poll. Do not call this method directly """ req_ids = {} - for dev, attrs in self.dev_dict.items(): + dev_dict = {} + with self.lock: + for dev, attrs in self.dev_dict.items(): + dev_dict[dev] = dict(attrs) + + for dev, attrs in dev_dict.items(): try: req_id = dev.poll(attrs, asynch=True) req_ids[dev] = attrs, req_id except Exception as e: self.error("poll_asynch error") self.debug("Details:", exc_info=1) + for dev, (attrs, req_id) in req_ids.items(): try: dev.poll(attrs, req_id=req_id) except Exception as e: - self.error("poll_reply error") + self.error("poll_reply error %r", e) + From e199c7848193713a9ab6dd5a2c59fbf827f54aeb Mon Sep 17 00:00:00 2001 From: cfalcon Date: Fri, 13 Sep 2019 09:32:28 +0200 Subject: [PATCH 072/373] Add cleanup after polling unittests Add pytest for testing the Taurus Factory cleanup chain. --- .../core/tango/test/test_tangofactory.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/taurus/core/tango/test/test_tangofactory.py b/lib/taurus/core/tango/test/test_tangofactory.py index 7c7c3a8ae..ab87ce0fc 100644 --- a/lib/taurus/core/tango/test/test_tangofactory.py +++ b/lib/taurus/core/tango/test/test_tangofactory.py @@ -26,6 +26,7 @@ """Tests for taurus.core.tango.tangofactory""" +import time import taurus from taurus.external.unittest import TestCase @@ -74,3 +75,38 @@ def create(): def tearDown(self): self.factory = None + +def test_cleanup_after_polling_1(): + """ + Ensure that polling a Tango attribute does not keep device alive + """ + polling_period = .1 # seconds + a = taurus.Attribute('sys/tg_test/1/float_scalar') + f = a.factory() + a.activatePolling(polling_period * 1000, force=True) + assert len(f.tango_attrs.keys()) == 1 + assert len(f.tango_devs.keys()) == 1 + a.disablePolling() + a = None + assert len(f.tango_attrs.keys()) == 0 + assert len(f.tango_devs.keys()) == 0 + + +def test_cleanup_after_polling_2(): + """ + Ensure that polling a Tango attribute does not keep device alive + See Bug #999 + """ + polling_period = .1 # seconds + a = taurus.Attribute('sys/tg_test/1/float_scalar') + f = a.factory() + a.activatePolling(polling_period * 1000, force=True) + assert len(f.tango_attrs.keys()) == 1 + assert len(f.tango_devs.keys()) == 1 + a = None + assert len(f.tango_attrs.keys()) == 0 + assert len(f.tango_devs.keys()) == 1 + time.sleep(polling_period) + assert len(f.tango_attrs.keys()) == 0 + assert len(f.tango_devs.keys()) == 0 + From b5a3395f1ea15097b3e0d84f894c415e0ceb43f7 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Fri, 13 Sep 2019 15:44:08 +0200 Subject: [PATCH 073/373] Relax test conditions Avoid false positive by relaxing the conditions --- lib/taurus/core/tango/test/test_tangofactory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/test/test_tangofactory.py b/lib/taurus/core/tango/test/test_tangofactory.py index ab87ce0fc..8eb0161e9 100644 --- a/lib/taurus/core/tango/test/test_tangofactory.py +++ b/lib/taurus/core/tango/test/test_tangofactory.py @@ -88,6 +88,7 @@ def test_cleanup_after_polling_1(): assert len(f.tango_devs.keys()) == 1 a.disablePolling() a = None + time.sleep(polling_period) assert len(f.tango_attrs.keys()) == 0 assert len(f.tango_devs.keys()) == 0 @@ -105,7 +106,6 @@ def test_cleanup_after_polling_2(): assert len(f.tango_devs.keys()) == 1 a = None assert len(f.tango_attrs.keys()) == 0 - assert len(f.tango_devs.keys()) == 1 time.sleep(polling_period) assert len(f.tango_attrs.keys()) == 0 assert len(f.tango_devs.keys()) == 0 From bd10005c9c3c0b2c7e53c2fb3b7d9f709aa169de Mon Sep 17 00:00:00 2001 From: cfalcon Date: Fri, 13 Sep 2019 16:20:40 +0200 Subject: [PATCH 074/373] Adapt test_cleanup_after_polling to python3. Remove unuseful test. --- .../core/tango/test/test_tangofactory.py | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/lib/taurus/core/tango/test/test_tangofactory.py b/lib/taurus/core/tango/test/test_tangofactory.py index 8eb0161e9..a2dc762ed 100644 --- a/lib/taurus/core/tango/test/test_tangofactory.py +++ b/lib/taurus/core/tango/test/test_tangofactory.py @@ -76,24 +76,8 @@ def create(): def tearDown(self): self.factory = None -def test_cleanup_after_polling_1(): - """ - Ensure that polling a Tango attribute does not keep device alive - """ - polling_period = .1 # seconds - a = taurus.Attribute('sys/tg_test/1/float_scalar') - f = a.factory() - a.activatePolling(polling_period * 1000, force=True) - assert len(f.tango_attrs.keys()) == 1 - assert len(f.tango_devs.keys()) == 1 - a.disablePolling() - a = None - time.sleep(polling_period) - assert len(f.tango_attrs.keys()) == 0 - assert len(f.tango_devs.keys()) == 0 - -def test_cleanup_after_polling_2(): +def test_cleanup_after_polling(): """ Ensure that polling a Tango attribute does not keep device alive See Bug #999 @@ -102,11 +86,11 @@ def test_cleanup_after_polling_2(): a = taurus.Attribute('sys/tg_test/1/float_scalar') f = a.factory() a.activatePolling(polling_period * 1000, force=True) - assert len(f.tango_attrs.keys()) == 1 - assert len(f.tango_devs.keys()) == 1 + assert len(list(f.tango_attrs.keys())) == 1 + assert len(list(f.tango_devs.keys())) == 1 a = None - assert len(f.tango_attrs.keys()) == 0 + assert len(list(f.tango_attrs.keys())) == 0 time.sleep(polling_period) - assert len(f.tango_attrs.keys()) == 0 - assert len(f.tango_devs.keys()) == 0 + assert len(list(f.tango_attrs.keys())) == 0 + assert len(list(f.tango_devs.keys())) == 0 From 25034be250410cd2d60cfa5313fff1408f6becb1 Mon Sep 17 00:00:00 2001 From: zreszela Date: Wed, 18 Sep 2019 17:43:10 +0200 Subject: [PATCH 075/373] Fix jdraw_view to work with JD element names with "/" and but not being Tango Use Taurus validators to check if JD element names are valid Tango device or attribute names. --- lib/taurus/qt/qtgui/graphic/taurusgraphic.py | 33 +++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py index e27fa7b12..93c318597 100644 --- a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py +++ b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py @@ -1530,14 +1530,31 @@ def getGraphicsItem(self, type_, params): name = str(name).replace(k, v) params[self.getNameParam()] = name cls = None - if '/' in name: - # replacing Taco identifiers in %s'%name - if name.lower().startswith('tango:') and (name.count('/') == 2 or not 'tango:/' in name.lower()): - nname = name.split(':', 1)[-1] - params[self.getNameParam()] = name = nname - if name.lower().endswith('/state'): - name = name.rsplit('/', 1)[0] - cls = Manager().findObjectClass(name) + # TODO: starting slashes are allowed while we support parent model + # feature (taurus-org/taurus#734) + if not name.startswith("/") and "/" in name: + try: + from taurus.core.tango.tangovalidator import ( + TangoDeviceNameValidator, + TangoAttributeNameValidator, + ) + except ImportError: + pass + else: + if (TangoDeviceNameValidator().isValid(name) + or TangoAttributeNameValidator().isValid(name)): + # replacing Taco identifiers in %s'%name + if name.lower().startswith("tango:"): + if name.count('/') == 2 or 'tango:/' not in name.lower(): # noqa + nname = name.split(':', 1)[-1] + params[self.getNameParam()] = name = nname + else: + from taurus import warning + warning("if you use a tango name as JD name it must " + "with tango:") + if name.lower().endswith('/state'): + name = name.rsplit('/', 1)[0] + cls = Manager().findObjectClass(name) else: if name: self.debug('%s does not match a tango name' % name) From ba7bfcbfb809ac06c65c0c8568f73e961ed6a1f2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 23 Sep 2019 10:41:48 +0200 Subject: [PATCH 076/373] Fix exception in TaurusModelChooser.getListedModels() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TaurusModelChooser.getListedModels raises an exception under python3 due to awrong usage of str instead of bytes. This is an oversight from #886. Thanks to Pierre HĂ©ricourt for spotting and suggesting the fix. Fixes #997 --- lib/taurus/qt/qtgui/panel/taurusmodelchooser.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py b/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py index 552a168bd..5b82a14e4 100644 --- a/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py +++ b/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py @@ -355,12 +355,16 @@ def getListedModels(self, asMimeData=False): models = models[:1] if asMimeData: md = Qt.QMimeData() - md.setData(taurus.qt.qtcore.mimetypes.TAURUS_MODEL_LIST_MIME_TYPE, str( - "\r\n".join(models))) + md.setData( + taurus.qt.qtcore.mimetypes.TAURUS_MODEL_LIST_MIME_TYPE, + bytes("\r\n".join(models), encoding="utf8") + ) md.setText(", ".join(models)) if len(models) == 1: md.setData( - taurus.qt.qtcore.mimetypes.TAURUS_MODEL_MIME_TYPE, str(models[0])) + taurus.qt.qtcore.mimetypes.TAURUS_MODEL_MIME_TYPE, + bytes(models[0], encoding="utf8") + ) return md return models From e95d0335265287d5c000c2b9bc6e7018c3f29058 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 23 Sep 2019 16:07:58 +0200 Subject: [PATCH 077/373] Remove tango-centric event handling in TaurusWheelEdit TaurusWheelEdit.handleEvent() is tango-centric. Make it scheme-agnostic. --- lib/taurus/qt/qtgui/input/tauruswheel.py | 44 +++++------------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/lib/taurus/qt/qtgui/input/tauruswheel.py b/lib/taurus/qt/qtgui/input/tauruswheel.py index 69901fd7e..3f371093e 100644 --- a/lib/taurus/qt/qtgui/input/tauruswheel.py +++ b/lib/taurus/qt/qtgui/input/tauruswheel.py @@ -56,42 +56,16 @@ def __init__(self, qt_parent=None, designMode=False): def handleEvent(self, evt_src, evt_type, evt_value): if evt_type == TaurusEventType.Config and evt_value is not None: - import re - # match the format string to "%[width][.precision][f_type]" obj = self.getModelObj() - m = re.match(r'%([0-9]+)?(\.([0-9]+))?([df])', obj.format) - if m is None: - raise ValueError("'%s' format unsupported" % obj.format) - - width, _, precision, f_type = m.groups() - - if width is None: - width = self.DefaultIntDigitCount + \ - self.DefaultDecDigitCount + 1 - else: - width = int(width) - - if precision is None: - precision = self.DefaultDecDigitCount - else: - precision = int(precision) - - dec_nb = precision - - if dec_nb == 0 or f_type == 'd': - int_nb = width - else: - int_nb = width - dec_nb - 1 # account for decimal sep - - self.setDigitCount(int_nb=int_nb, dec_nb=dec_nb) - try: - self.setMinValue(float(obj.min_value)) - except: - pass - try: - self.setMaxValue(float(obj.max_value)) - except: - pass + # set decimal digits + self.setDigitCount(int_nb=None, dec_nb=obj.precision) + # set min and max values + min_, max_ = obj.getRange() + if min_ is not None: + self.setMinValue(min_.magnitude) + if max_ is not None: + self.setMaxValue(max_.magnitude) + TaurusBaseWritableWidget.handleEvent( self, evt_src, evt_type, evt_value) From 213ad6a779101463b9ebe822aab1d42c7935386b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 25 Sep 2019 15:50:49 +0200 Subject: [PATCH 078/373] Allow changing digit count of QWheelEdit from GUI I Add "Change Digits" context menu option to QwheelEdit --- lib/taurus/qt/qtgui/input/qwheel.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/qwheel.py b/lib/taurus/qt/qtgui/input/qwheel.py index 8b3ac6f0d..dfe1d672a 100644 --- a/lib/taurus/qt/qtgui/input/qwheel.py +++ b/lib/taurus/qt/qtgui/input/qwheel.py @@ -220,6 +220,10 @@ def __init__(self, parent=None): self._setValue(0) self._build() + self.setDigitCountAction = Qt.QAction("Change digits", self) + self.setDigitCountAction.triggered.connect(self._onSetDigitCount) + self.addAction(self.setDigitCountAction) + self.setContextMenuPolicy(Qt.Qt.ActionsContextMenu) def _getMinPossibleValue(self): """_getMinPossibleValue(self) -> None @@ -547,6 +551,28 @@ def buttonPressed(self, b): self._setValue(self.getValue() + b._inc) self._updateValue() + def _onSetDigitCount(self): + text, ok = Qt.QInputDialog.getText( + self, + "Change digits", + "Enter digits as .", + text="{:d}.{:d}".format( + self.getIntDigitCount(), + self.getDecDigitCount() + ) + ) + if ok: + try: + dc = list(map(int, text.split('.'))) + self.setDigitCount(*dc) + except Exception as e: + Qt.QMessageBox.warning( + self, + "Invalid digit count specification", + 'Invalid specification: "{}"\nReason:{}'.format(text, e) + ) + self._onSetDigitCount() + def setDigitCount(self, int_nb, dec_nb): """setDigitCount(self, int_nb, dec_nb) -> None @@ -775,7 +801,6 @@ def showEditWidget(self): l.columnCount() - 1).bottomRight()) ed.setGeometry(rect) ed.setAlignment(Qt.Qt.AlignRight) - ed.setMaxLength(self.getDigitCount() + 2) ed.setText(self.getValueStr()) ed.selectAll() ed.setFocus() From 8b74a4c15ee3741120f5e6515ec4d54e39d50aea Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 25 Sep 2019 16:12:38 +0200 Subject: [PATCH 079/373] Protect QWheelEdit.setValue(v) against v=None --- lib/taurus/qt/qtgui/input/qwheel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/taurus/qt/qtgui/input/qwheel.py b/lib/taurus/qt/qtgui/input/qwheel.py index dfe1d672a..29ea44d02 100644 --- a/lib/taurus/qt/qtgui/input/qwheel.py +++ b/lib/taurus/qt/qtgui/input/qwheel.py @@ -504,6 +504,8 @@ def _setValue(self, v): Sets value of this widget. If the given value exceeds any limit, the value is NOT set. """ + if v is None: + return if self._roundFunc: v = self._roundFunc(v) if v > self._maxValue or v < self._minValue: From a82c2da2a5e3e5bc1db95d3cf8e6dc2a2af404b4 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 26 Sep 2019 09:52:47 +0200 Subject: [PATCH 080/373] Force validator of qwheel edit to standard notation QWheelEdit can only represent values in standard notation. Make the editor's validator enforce the same for the editor. --- lib/taurus/qt/qtgui/input/qwheel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/qwheel.py b/lib/taurus/qt/qtgui/input/qwheel.py index 29ea44d02..62177c920 100644 --- a/lib/taurus/qt/qtgui/input/qwheel.py +++ b/lib/taurus/qt/qtgui/input/qwheel.py @@ -179,7 +179,9 @@ class _NumericEditor(Qt.QLineEdit): def __init__(self, parent=None): Qt.QLineEdit.__init__(self, parent) - self.setValidator(Qt.QDoubleValidator(self)) + validator = Qt.QDoubleValidator(self) + validator.setNotation(validator.StandardNotation) + self.setValidator(validator) self.setFrame(False) From 48edd90bbed46c650595266c082f838bc6cc254e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 26 Sep 2019 09:56:35 +0200 Subject: [PATCH 081/373] Warn the user if a value outside allowed ranks is set If _setValue is called with a value exceeding representation limits of the widget, the va lue is silently discarded. This may be confusing. Show a warning instead message dialog instead. --- lib/taurus/qt/qtgui/input/qwheel.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/taurus/qt/qtgui/input/qwheel.py b/lib/taurus/qt/qtgui/input/qwheel.py index 62177c920..397e946a6 100644 --- a/lib/taurus/qt/qtgui/input/qwheel.py +++ b/lib/taurus/qt/qtgui/input/qwheel.py @@ -511,6 +511,13 @@ def _setValue(self, v): if self._roundFunc: v = self._roundFunc(v) if v > self._maxValue or v < self._minValue: + Qt.QMessageBox.warning( + self, + "Invalid Value", + ("'{}' cannot be represented in current widget. \n" + + "Tip: try changing the number of digits from context menu" + ).format(v) + ) return self._previous_value = self._value self._value = v From b3c0a16077491206792cbb75b8d626f9ff6097d0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 26 Sep 2019 10:52:40 +0200 Subject: [PATCH 082/373] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c50d8762..a81c49430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ develop branch) won't be reflected in this file. ### Changed ### Deprecated ### Fixed +- Several issues in TaurusWheelEdit (#1010) ## [4.6.1] - 2019-08-19 Hotfix for auto-deployment in PyPI with Travis. No other difference from 4.6.0. From 7ae53e3766b7c8e3176542df49e6df6ab25fee8f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 30 Sep 2019 11:01:59 +0200 Subject: [PATCH 083/373] Improve handling of limits in QWheelEdit QWheelEdit's current implementation does not properly distinguish between the value limits imposed by the setMinValue / setMaxValue methods and those imposed by the amount of digits available in the wheel widget. Fix this by using a single validator (which is now shared by the wheel and the editor) that properly combines both types of limits. --- lib/taurus/qt/qtgui/input/qwheel.py | 68 +++++++++++------------------ 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/lib/taurus/qt/qtgui/input/qwheel.py b/lib/taurus/qt/qtgui/input/qwheel.py index 397e946a6..3b9b187af 100644 --- a/lib/taurus/qt/qtgui/input/qwheel.py +++ b/lib/taurus/qt/qtgui/input/qwheel.py @@ -174,17 +174,6 @@ def setText(self, t): skin = Qt.pyqtProperty('QString', getSkin, setSkin, resetSkin) -class _NumericEditor(Qt.QLineEdit): - """A private editor to be used by QWheelEdit widget""" - - def __init__(self, parent=None): - Qt.QLineEdit.__init__(self, parent) - validator = Qt.QDoubleValidator(self) - validator.setNotation(validator.StandardNotation) - self.setValidator(validator) - self.setFrame(False) - - class QWheelEdit(Qt.QFrame): """A widget designed to handle numeric scalar values. It allows interaction based on single digit as well as normal value edition.""" @@ -217,6 +206,8 @@ def __init__(self, parent=None): self._hideEditWidget = True self._showArrowButtons = True self._forwardReturn = False + self._validator = Qt.QDoubleValidator(self) + self._validator.setNotation(self._validator.StandardNotation) self._setDigits(QWheelEdit.DefaultIntDigitCount, QWheelEdit.DefaultDecDigitCount) self._setValue(0) @@ -327,18 +318,20 @@ def _build(self): self._upButtons.buttonClicked.connect(self.buttonPressed) self._downButtons.buttonClicked.connect(self.buttonPressed) - ed = _NumericEditor(self) + ed = Qt.QLineEdit(self) + ed.setFrame(False) ed.returnPressed.connect(self.editingFinished) ed.editingFinished.connect(self.hideEditWidget) rect = Qt.QRect(l.cellRect(1, 0).topLeft(), l.cellRect(1, l.columnCount() - 1).bottomRight()) ed.setGeometry(rect) ed.setAlignment(Qt.Qt.AlignRight) - ed.validator().setRange(self.getMinValue(), self.getMaxValue(), - self.getDecDigitCount()) + ed.setValidator(self._validator) ed.setVisible(False) self._editor = ed + self._updateValidator() + # set the minimum height for the widget # (otherwise the hints seem to be ignored by the layouts) min_height = max(ed.minimumSizeHint().height(), @@ -377,17 +370,6 @@ def _clear(self): self._editor.destroy() self._editor = None - def _setMinMax(self, min, max): - """_setMinMax(self, min, max) -> None - - Sets the minimum and maximum values for this widget - - @param[in] min(float) minimum value - @param[in] max(float) maximum value - """ - self._minValue = min - self._maxValue = max - def _setDigits(self, int_nb=None, dec_nb=None): """_setDigits(self, int_nb=None, dec_nb=None) -> None @@ -415,8 +397,7 @@ def _setDigits(self, int_nb=None, dec_nb=None): if self._decDigitCount > 0: total_chars += 1 # for dot self._valueFormat = '%%+0%d.%df' % (total_chars, self._decDigitCount) - self._setMinMax(self._getMinPossibleValue(), - self._getMaxPossibleValue()) + self._updateValidator() # we call setValue to update the self._value_str self._setValue(self.getValue()) @@ -510,13 +491,15 @@ def _setValue(self, v): return if self._roundFunc: v = self._roundFunc(v) - if v > self._maxValue or v < self._minValue: + if v > self._validator.top() or v < self._validator.bottom(): Qt.QMessageBox.warning( self, "Invalid Value", - ("'{}' cannot be represented in current widget. \n" - + "Tip: try changing the number of digits from context menu" - ).format(v) + "'{}' is out of the allowed range ({}, {})".format( + v, + self._validator.bottom(), + self._validator.top(), + ) ) return self._previous_value = self._value @@ -712,17 +695,19 @@ def setMinValue(self, v): @param[in] v (float) the new minimum allowed value """ self._minValue = v - w = self.getEditWidget() - if not w is None: - w.validator().setRange(self._minValue, self._maxValue, self._decDigitCount) + self._updateValidator() + + def _updateValidator(self): + min_ = max(self._minValue, self._getMinPossibleValue()) + max_ = min(self._maxValue, self._getMaxPossibleValue()) + self._validator.setRange(min_, max_, self._decDigitCount) def resetMinValue(self): """resetMinValue(self) -> None - Resets the minimum allowed value to the minimum possible according to - the current total number of digits + Resets the minimum allowed value to -inf """ - self.setMinValue(self._getMinPossibleValue()) + self.setMinValue(numpy.finfo('d').min) def getMaxValue(self): """getMaxValue(self) -> float @@ -741,17 +726,14 @@ def setMaxValue(self, v): @param[in] v (float) the new maximum allowed value """ self._maxValue = v - w = self.getEditWidget() - if not w is None: - w.validator().setRange(self._minValue, self._maxValue, self._decDigitCount) + self._updateValidator() def resetMaxValue(self): """resetMaxValue(self) -> None - Resets the maximum allowed value to the maximum possible according to - the current total number of digits + Resets the maximum allowed value to +inf """ - self.setMaxValue(self._getMaxPossibleValue()) + self.setMaxValue(numpy.finfo('d').max) def getAutoRepeat(self): return self._upButtons.buttons()[0].autoRepeat() From 3cc95c1659e55f81c469a87bb2065f3f29fad0e9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 30 Sep 2019 13:38:43 +0200 Subject: [PATCH 084/373] Set QT_THEME_FORCE_ON_LINUX=False The value of QT_THEME_FORCE_ON_LINUX is set to True by default in `tauruscustomsettings.py`, which is inconsistent with what is documented in that same module. Set it to False by default. --- lib/taurus/tauruscustomsettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index e5fa03ae0..8b4728248 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -134,7 +134,7 @@ #: In Linux the QT_THEME_NAME is not applied (to respect the system theme) #: setting QT_THEME_FORCE_ON_LINUX=True overrides this. -QT_THEME_FORCE_ON_LINUX = True +QT_THEME_FORCE_ON_LINUX = False #: Full Qt designer path (including filename. Default is None, meaning: #: - linux: look for the system designer following Qt.QLibraryInfo.BinariesPath From d92f5ff2d104d9722b0b36a1d61e1c15b075ec95 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 4 Oct 2019 15:50:10 +0200 Subject: [PATCH 085/373] Bump version 4.6.2-alpha to 4.6.2 --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index cb448e3f1..067a1125d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.6.2-alpha +current_version = 4.6.2 parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index 887edff7f..665f7e6f9 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.6.2-alpha' +version = '4.6.2' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: From 83a494b07769fa8ff63b8d3fcf215913f1cebea3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 4 Oct 2019 15:51:01 +0200 Subject: [PATCH 086/373] Bump version 4.6.2 to 4.6.3-alpha --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 067a1125d..f9284f107 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.6.2 +current_version = 4.6.3-alpha parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index 665f7e6f9..ef58bc3cd 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.6.2' +version = '4.6.3-alpha' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: From 11f5bd9d5b0032fabdbc6ce423623f6f3a35317a Mon Sep 17 00:00:00 2001 From: cfalcon Date: Mon, 14 Oct 2019 10:49:28 +0200 Subject: [PATCH 087/373] Fix TaurusWheel does not respect attribute precision and limits The configuration event does not arrive and the attribute precision and limits are not initialized. Force an initialization of them the first time that a change event arrives before a configuration event. Fix #1019 --- lib/taurus/qt/qtgui/input/tauruswheel.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/taurus/qt/qtgui/input/tauruswheel.py b/lib/taurus/qt/qtgui/input/tauruswheel.py index 3f371093e..3f3d8e0ba 100644 --- a/lib/taurus/qt/qtgui/input/tauruswheel.py +++ b/lib/taurus/qt/qtgui/input/tauruswheel.py @@ -49,22 +49,25 @@ def __init__(self, qt_parent=None, designMode=False): self.numberChanged.connect(self.notifyValueChanged) self.returnPressed.connect(self.writeValue) self.valueChangedSignal.connect(self.updatePendingOperations) + self._init_range = False #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # TaurusBaseWidget overwriting #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- def handleEvent(self, evt_src, evt_type, evt_value): - if evt_type == TaurusEventType.Config and evt_value is not None: - obj = self.getModelObj() - # set decimal digits - self.setDigitCount(int_nb=None, dec_nb=obj.precision) - # set min and max values - min_, max_ = obj.getRange() - if min_ is not None: - self.setMinValue(min_.magnitude) - if max_ is not None: - self.setMaxValue(max_.magnitude) + if evt_type == TaurusEventType.Config or not self._init_range: + if evt_value is not None: + obj = self.getModelObj() + # set decimal digits + self.setDigitCount(int_nb=None, dec_nb=obj.precision) + # set min and max values + min_, max_ = obj.getRange() + if min_ is not None: + self.setMinValue(min_.magnitude) + if max_ is not None: + self.setMaxValue(max_.magnitude) + self._init_range = True TaurusBaseWritableWidget.handleEvent( self, evt_src, evt_type, evt_value) From 06530c655b1758a7d23f6f5d089a661dd3612e02 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 14 Oct 2019 11:30:02 +0200 Subject: [PATCH 088/373] (m) private variable rename --- lib/taurus/qt/qtgui/input/tauruswheel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/taurus/qt/qtgui/input/tauruswheel.py b/lib/taurus/qt/qtgui/input/tauruswheel.py index 3f3d8e0ba..cbcae37f5 100644 --- a/lib/taurus/qt/qtgui/input/tauruswheel.py +++ b/lib/taurus/qt/qtgui/input/tauruswheel.py @@ -49,14 +49,14 @@ def __init__(self, qt_parent=None, designMode=False): self.numberChanged.connect(self.notifyValueChanged) self.returnPressed.connect(self.writeValue) self.valueChangedSignal.connect(self.updatePendingOperations) - self._init_range = False + self._configured = False #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # TaurusBaseWidget overwriting #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- def handleEvent(self, evt_src, evt_type, evt_value): - if evt_type == TaurusEventType.Config or not self._init_range: + if evt_type == TaurusEventType.Config or not self._configured: if evt_value is not None: obj = self.getModelObj() # set decimal digits @@ -67,7 +67,7 @@ def handleEvent(self, evt_src, evt_type, evt_value): self.setMinValue(min_.magnitude) if max_ is not None: self.setMaxValue(max_.magnitude) - self._init_range = True + self._configured = True TaurusBaseWritableWidget.handleEvent( self, evt_src, evt_type, evt_value) From 36d58315cb8dab243002f73060bffdbf45e6e749 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 7 Nov 2019 11:38:46 +0100 Subject: [PATCH 089/373] Fix TaurusImageDialog.setModel Qt slot in py2 The setModel slot of TaurusImageDialog is not exposed in py2 due to a wrong py2-py3 compatibility definition. Use "QString" instead of str for defining the slot in a py2+3 compatible way. Fixes #1023 --- lib/taurus/qt/qtgui/extra_guiqwt/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py index 8ba2579bf..58d15ea79 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py @@ -474,7 +474,7 @@ def getModelClass(self): '''reimplemented from :class:`TaurusBaseWidget`''' return taurus.core.taurusattribute.TaurusAttribute - @Qt.pyqtSlot(str) + @Qt.pyqtSlot("QString") def setModel(self, model): '''reimplemented from :class:`TaurusBaseWidget`''' if self.getUseParentModel(): From 8e26872ce70542725faa2754c7b4358de69a6f4d Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 11 Nov 2019 17:46:38 +0100 Subject: [PATCH 090/373] Fix 909: expose more modules from qwt5 in ..qtgui.plot The taurus.qt.qtgui.plot module is a backwards-compatibility implementation that exposes all public API from taurus.qt.qtgui.qwt5. Unfortunately, in some cases, the non-public API is also needed for backwards compat. One example is when loading old settings files (created with v<4.5) that are based on pickles that end up calling `import taurus.qt.qtgui.plot.curvesAppearanceChooserDlg`. In order to provide better backwards compatibility, patch sys.modules by injecting the submodules of taurus.qt.qtgui.qwt5 as submodules of taurus.qt.qtgui.plot. --- lib/taurus/qt/qtgui/plot/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/taurus/qt/qtgui/plot/__init__.py b/lib/taurus/qt/qtgui/plot/__init__.py index 1d4326cb6..a265297c4 100644 --- a/lib/taurus/qt/qtgui/plot/__init__.py +++ b/lib/taurus/qt/qtgui/plot/__init__.py @@ -52,7 +52,17 @@ alt='taurus.qt.qtgui.tpg or taurus.qt.qtgui.qwt5') try: + # Import all from qwt5 from taurus.qt.qtgui.qwt5 import * + # ...and patch sys.modules to expose all qwt5 submodules here + # (even those not in the public API). This fixes + # https://github.com/taurus-org/taurus/issues/909) + import sys + d = {} + for k, v in sys.modules.items(): + if 'taurus.qt.qtgui.qwt5.' in k: + d[k.replace('taurus.qt.qtgui.qwt5.', 'taurus.qt.qtgui.plot.')] = v + sys.modules.update(d) except: try: from taurus.qt.qtgui.tpg import TaurusPlot, TaurusTrend From 2c7ce36233d6ca8827413d5dbe383fea893b7581 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Fri, 8 Nov 2019 14:28:09 +0100 Subject: [PATCH 091/373] Fix constructor call to up-to-date Qt API --- lib/taurus/qt/qtgui/graphic/taurusgraphic.py | 26 ++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) mode change 100644 => 100755 lib/taurus/qt/qtgui/graphic/taurusgraphic.py diff --git a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py old mode 100644 new mode 100755 index 93c318597..62a91bf8b --- a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py +++ b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py @@ -1250,7 +1250,9 @@ class TaurusEllipseStateItem(Qt.QGraphicsEllipseItem, TaurusGraphicsStateItem): def __init__(self, name=None, parent=None, scene=None): name = name or self.__class__.__name__ - Qt.QGraphicsEllipseItem.__init__(self, parent, scene) + Qt.QGraphicsEllipseItem.__init__(self, parent) + if scene is not None: + scene.addItem(self) self.call__init__(TaurusGraphicsStateItem, name, parent) def paint(self, painter, option, widget=None): @@ -1264,7 +1266,9 @@ class TaurusRectStateItem(Qt.QGraphicsRectItem, TaurusGraphicsStateItem): def __init__(self, name=None, parent=None, scene=None): name = name or self.__class__.__name__ - Qt.QGraphicsRectItem.__init__(self, parent, scene) + Qt.QGraphicsRectItem.__init__(self, parent) + if scene is not None: + scene.addItem(self) self.call__init__(TaurusGraphicsStateItem, name, parent) def paint(self, painter, option, widget): @@ -1278,7 +1282,9 @@ class TaurusSplineStateItem(QSpline, TaurusGraphicsStateItem): def __init__(self, name=None, parent=None, scene=None): name = name or self.__class__.__name__ - QSpline.__init__(self, parent, scene) + QSpline.__init__(self, parent) + if scene is not None: + scene.addItem(self) self.call__init__(TaurusGraphicsStateItem, name, parent) def paint(self, painter, option, widget): @@ -1341,7 +1347,9 @@ def paint(self, painter, option, widget): class TaurusGroupItem(Qt.QGraphicsItemGroup): def __init__(self, name=None, parent=None, scene=None): - Qt.QGraphicsItemGroup.__init__(self, parent, scene) + Qt.QGraphicsItemGroup.__init__(self, parent) + if scene is not None: + scene.addItem(self) class TaurusGroupStateItem(TaurusGroupItem, TaurusGraphicsStateItem): @@ -1359,8 +1367,10 @@ class TaurusPolygonStateItem(Qt.QGraphicsPolygonItem, TaurusGraphicsStateItem): def __init__(self, name=None, parent=None, scene=None): name = name or self.__class__.__name__ - #Qt.QGraphicsRectItem.__init__(self, parent, scene) - Qt.QGraphicsPolygonItem.__init__(self, parent, scene) + #Qt.QGraphicsRectItem.__init__(self, parent) + Qt.QGraphicsPolygonItem.__init__(self, parent) + if scene is not None: + scene.addItem(self) self.call__init__(TaurusGraphicsStateItem, name, parent) def paint(self, painter, option, widget): @@ -1374,7 +1384,9 @@ class TaurusLineStateItem(Qt.QGraphicsLineItem, TaurusGraphicsStateItem): def __init__(self, name=None, parent=None, scene=None): name = name or self.__class__.__name__ - Qt.QGraphicsLineItem.__init__(self, parent, scene) + Qt.QGraphicsLineItem.__init__(self, parent) + if scene is not None: + scene.addItem(self) self.call__init__(TaurusGraphicsStateItem, name, parent) def paint(self, painter, option, widget): From 8c1822821e077e54b5ec230de7cd6f4c5dfc6af9 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Fri, 8 Nov 2019 15:12:49 +0100 Subject: [PATCH 092/373] Avoid to access to a None scene --- lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) mode change 100644 => 100755 lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py diff --git a/lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py b/lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py old mode 100644 new mode 100755 index 0e476a587..3f5a9cd3e --- a/lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py +++ b/lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py @@ -167,7 +167,8 @@ def get_item_colors(self, emit=False): @Qt.pyqtSlot(object) @Qt.pyqtSlot('QString') def selectGraphicItem(self, item_name): - self.scene().selectGraphicItem(item_name) + if self.scene() is not None: + self.scene().selectGraphicItem(item_name) return False def _graphicItemSelected(self, item_name): @@ -389,7 +390,7 @@ def setModel(self, model, alias=None, delayed=False, trace=False): self.path = os.path.dirname(filename) factory = self.getGraphicsFactory(delayed=delayed) scene = parse(filename, factory) - scene.setSelectionStyle(self._selectionStyle) + self.setSelectionStyle(self._selectionStyle) self.debug("Obtained %s(%s)", type(scene).__name__, filename) if not scene: self.warning("TaurusJDrawSynopticsView.setModel(%s): Unable to parse %s!!!" % ( @@ -450,7 +451,7 @@ def setSelectionStyle(self, selectionStyle): self.debug('invalid selectionStyle "%s"', selectionStyle) return if self.scene() is not None: - self.scene().setSelectionStyle(selectionStyle) + self.setSelectionStyle(selectionStyle) self._selectionStyle = selectionStyle def getSelectionStyle(self): From 48bb4e2f50fc14315815feb6af3d0f88595599d2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 12 Nov 2019 15:24:53 +0100 Subject: [PATCH 093/373] Remove unneeded execution permissions --- lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py | 0 lib/taurus/qt/qtgui/graphic/taurusgraphic.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py mode change 100755 => 100644 lib/taurus/qt/qtgui/graphic/taurusgraphic.py diff --git a/lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py b/lib/taurus/qt/qtgui/graphic/jdraw/jdraw_view.py old mode 100755 new mode 100644 diff --git a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py old mode 100755 new mode 100644 From c69bd8030307cce8f93f2ba69c5cfdb406944340 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Wed, 13 Nov 2019 10:43:11 +0100 Subject: [PATCH 094/373] Fix doSomething signal calls doSomething signal is used with a wrong signature. It is called with tuples instead of list. Adapt the calls, to pass list instead of tuples. --- lib/taurus/qt/qtcore/util/emitter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtcore/util/emitter.py b/lib/taurus/qt/qtcore/util/emitter.py index 205e1351f..7ec2f64f7 100644 --- a/lib/taurus/qt/qtcore/util/emitter.py +++ b/lib/taurus/qt/qtcore/util/emitter.py @@ -361,7 +361,7 @@ def __init__(self, schema, parent=None, sleep=10000, pause=5, period=0): sleep=sleep, loopwait=pause, polling=period) - self._modelsQueue.put((self.addUnsubscribedAttributes,)) + self._modelsQueue.put([self.addUnsubscribedAttributes]) self._modelsThread.start() def _modelSubscriber(self, method, args=[]): @@ -402,7 +402,7 @@ def _addModelObj(self, modelObj): self.debug('addModelObj(%s), proxy not available' % modelObj) return - self._modelsQueue.put((modelObj.subscribePendingEvents,)) + self._modelsQueue.put([modelObj.subscribePendingEvents]) self.debug('addModelObj(%s)' % str(modelObj)) def cleanUp(self): From cdcaa0b0c95cc26184174de312a1310466ff5ff8 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 13 Nov 2019 11:32:27 +0100 Subject: [PATCH 095/373] Add CI/CD debian packaging configuration (.gitlab-ci-alba.yml) The official debian packaging of tarus is currently done in salsa.debian.org , but at ALBA we are experimenting with a more integrated CI/CD approach that auto-creates (unofficial) packages for stretch, buster and sid on each upstream change. This requires to add a configuration file to upstream (.gitlab-ci-alba.yml). This file does nothing unless you explicitly configure your gitlab instance to run on it. TODO: generalise this file so that it is not dependent on ALBA's infrastructure so that anyone can make use of it. --- .gitlab-ci-alba.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .gitlab-ci-alba.yml diff --git a/.gitlab-ci-alba.yml b/.gitlab-ci-alba.yml new file mode 100644 index 000000000..4e7e56f71 --- /dev/null +++ b/.gitlab-ci-alba.yml @@ -0,0 +1,8 @@ +# --------------------------------------------------------- +# Use this as the ".gitlab-ci.yml" in your project root dir +# to activate the ctpipeline +# --------------------------------------------------------- + +include: +- https://git.cells.es/ctpkg/ci/ctpipeline/raw/master/ctjobdefs-ci.yml +- https://git.cells.es/ctpkg/ci/ctpipeline/raw/master/ctpipeline.yml From 93fa7a932166563d49f14ae1c1a0c31a16251e05 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 13 Nov 2019 12:11:06 +0100 Subject: [PATCH 096/373] Update CHANGELOG Updated till 2019-11-13 --- CHANGELOG.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a81c49430..e707fc760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,27 @@ Note: changes in the [support-3.x] branch (which was split from the master branch after [3.7.1] and maintained in parallel to the develop branch) won't be reflected in this file. -## Unreleased +## [Unreleased] ### Added - check-deps subcommand (#988) +- `taurus.cli.register_subcommands()` and `taurus.cli.taurus_cmd()` (#991) ### Removed ### Changed +- Qt theme no longer set to TangoIcons by default (for coherence with docs) (#1012) +- (for developers) Support of tox and change to pytest. More platforms + being now automatically tested by travis (#994) + ### Deprecated ### Fixed -- Several issues in TaurusWheelEdit (#1010) +- Several issues in TaurusWheelEdit (#1010, #1021) +- Several issues affecting synoptics (#1005, #1029) +- Support dns aliases for the authority name in tango model names (#998) +- Py3 exception in `TaurusModelChooser.getListedModels()` (#1008) +- (for py2) Improved backwards compatibility of `taurus.qt.qtgui.plot` (#1027) +- Exception in DelayedSubscriber (#1030) + ## [4.6.1] - 2019-08-19 Hotfix for auto-deployment in PyPI with Travis. No other difference from 4.6.0. From e4e4205ab5829ce86d23d6b3ba1839b275c1aed5 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 13 Nov 2019 12:12:18 +0100 Subject: [PATCH 097/373] Bump version 4.6.3-alpha to 4.6.4-alpha --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f9284f107..2deadd771 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.6.3-alpha +current_version = 4.6.4-alpha parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index ef58bc3cd..520e0dbff 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.6.3-alpha' +version = '4.6.4-alpha' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: From e28130992401d2c2d125f4b491ecdbdea596c84a Mon Sep 17 00:00:00 2001 From: cfalcon Date: Mon, 18 Nov 2019 17:06:28 +0100 Subject: [PATCH 098/373] Fix TaurusValueComboBox does not show set value When a TaurusValueComboBox widget is initialized it does not show the set value. The setValue method does not found the given value since it saves internally as string. Fix it doing a cast. --- lib/taurus/qt/qtgui/input/tauruscombobox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/tauruscombobox.py b/lib/taurus/qt/qtgui/input/tauruscombobox.py index 7b21557f9..bf29c9166 100644 --- a/lib/taurus/qt/qtgui/input/tauruscombobox.py +++ b/lib/taurus/qt/qtgui/input/tauruscombobox.py @@ -115,7 +115,7 @@ def setValue(self, value): Set the value for the widget to display, not the value of the attribute. """ - index = self.findData(value) + index = self.findData(str(value)) self._setCurrentIndex(index) def updateStyle(self): From b0c32106d14cb6ef52d80ee62ae7a7e1f0303529 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 19 Nov 2019 17:34:27 +0100 Subject: [PATCH 099/373] Add unit test for #1032 (TaurusValueComboBox initialization) Check that we are not affected by https://github.com/taurus-org/taurus/pull/1032 --- lib/taurus/qt/qtgui/input/test/__init__.py | 24 +++++++ .../qtgui/input/test/test_tauruscombobox.py | 63 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 lib/taurus/qt/qtgui/input/test/__init__.py create mode 100644 lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py diff --git a/lib/taurus/qt/qtgui/input/test/__init__.py b/lib/taurus/qt/qtgui/input/test/__init__.py new file mode 100644 index 000000000..b5a567265 --- /dev/null +++ b/lib/taurus/qt/qtgui/input/test/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# diff --git a/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py b/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py new file mode 100644 index 000000000..94167ca2b --- /dev/null +++ b/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py @@ -0,0 +1,63 @@ +from taurus.qt.qtgui.application import TaurusApplication +from taurus.qt.qtgui.input import TaurusValueComboBox +import taurus + +def test_TaurusValueCombobox(): + """Check that the TaurusValueComboBox is started with the right display + See https://github.com/taurus-org/taurus/pull/1032 + """ + # TODO: Parameterize this test + app = TaurusApplication.instance() + if app is None: + app = TaurusApplication(cmd_line_parser=None) + + # test with a pint quantity + model = 'sys/tg_test/1/short_scalar' + a = taurus.Attribute(model) + units = a.write(123).wvalue.units + w = TaurusValueComboBox() + names = [("A", 1234), ("B", "123"), ("C", 123 * units), ("E", -123)] + w.addValueNames(names) + w.setModel(model) + assert(w.currentText() == "C") + + # test with a boolean (using quantities) + model = 'sys/tg_test/1/boolean_scalar' + a = taurus.Attribute(model) + a.write(False) + w = TaurusValueComboBox() + w.addValueNames([("N", None), ("F", False), ("T", True)]) + w.setModel(model) + assert (w.currentText() == "F") + + # test with a spectrum + model = 'sys/tg_test/1/boolean_spectrum' + a = taurus.Attribute(model) + a.write([False, True, False]) + w = TaurusValueComboBox() + w.addValueNames([ + ("A", False), + ("B", [False, False, False]), + ("C", [False, True, False]), + ]) + w.setModel(model) + assert (w.currentText() == "C") + + # test with strings + model = 'sys/tg_test/1/string_scalar' + a = taurus.Attribute(model) + a.write("foobar") + w = TaurusValueComboBox() + w.addValueNames([("A", "foobarbaz"), ("B", "FOOBAR"), ("C", "foobar")]) + w.setModel(model) + assert (w.currentText() == "C") + + # test non-match + model = 'sys/tg_test/1/string_scalar' + a = taurus.Attribute(model) + a.write("foo") + w = TaurusValueComboBox() + w.addValueNames([("A", "foobarbaz"), ("B", "FOOBAR"), ("C", "foobar")]) + w.setModel(model) + assert (w.currentText() == "") + w.setModel(None) \ No newline at end of file From 6736f012b81305a721b46540a98becb5d397e7d2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 19 Nov 2019 18:25:22 +0100 Subject: [PATCH 100/373] Make TaurusValueCombobox.findData() more robust QComboBox.findData() fails when data is of some non-basic types (e.g. for Quantities, numpy arrays,...). Reimplement it in TaurusValueCombobox to support all these types. This fixes the issue reported in #1032 --- lib/taurus/qt/qtgui/input/tauruscombobox.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/tauruscombobox.py b/lib/taurus/qt/qtgui/input/tauruscombobox.py index bf29c9166..cfe188a91 100644 --- a/lib/taurus/qt/qtgui/input/tauruscombobox.py +++ b/lib/taurus/qt/qtgui/input/tauruscombobox.py @@ -36,6 +36,7 @@ from taurus.core.taurusattribute import TaurusAttribute from taurus.qt.qtgui.base import TaurusBaseWidget, TaurusBaseWritableWidget from taurus.core.util import eventfilters +import numpy class TaurusValueComboBox(Qt.QComboBox, TaurusBaseWritableWidget): @@ -115,9 +116,27 @@ def setValue(self, value): Set the value for the widget to display, not the value of the attribute. """ - index = self.findData(str(value)) + index = self.findData(value, pymatch=True) self._setCurrentIndex(index) + def findData(self, data, **kwargs): + """ + Reimplemented from :meth:`Qt.QComboBox.findData` to accept + the extra `pymatch` keyword arg. If `pymatch` is True, the + match will be attempted using python's `==` operator. + This is required to bypass some limitations imposed by C++'s QVariant . + By default, pymatch is False and behaves just as + :meth:`Qt.QComboBox.findData` + """ + pymatch = kwargs.pop("pymatch", False) + index = Qt.QComboBox.findData(self, data, **kwargs) + if pymatch and index == -1: + for i in range(self.count()): + if numpy.all(self.itemData(i) == data): + index = i + break + return index + def updateStyle(self): '''reimplemented from :class:`TaurusBaseWritableWidget`''' if self.hasPendingOperations(): From 0227552fb487f570ccc78fa387736f828385ddc9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 Nov 2019 15:36:34 +0100 Subject: [PATCH 101/373] =?UTF-8?q?Register=20degreeC=20(=3D=20=C2=B0C=20?= =?UTF-8?q?=3D=20=C2=BAC=20=3D=20degC=20=3Dcelsius)=20in=20the=20units=20r?= =?UTF-8?q?egistry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make celsius display as "°C" in taurus. Do it by registering a new unit (degreeC) equivalent to degC in taurus.core.units.UR This is a workaround until pint properly supports this. See: https://github.com/hgrecco/pint/issues/546 --- lib/taurus/core/test/test_units.py | 64 ++++++++++++++++++++++++++++++ lib/taurus/core/units.py | 3 ++ 2 files changed, 67 insertions(+) create mode 100644 lib/taurus/core/test/test_units.py diff --git a/lib/taurus/core/test/test_units.py b/lib/taurus/core/test/test_units.py new file mode 100644 index 000000000..d778e4e80 --- /dev/null +++ b/lib/taurus/core/test/test_units.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +from taurus.core.units import UR, Q_ + +# from pint import UnitRegistry +# UR = UnitRegistry() +# Q_= UR.Quantity +# UR.define(u'degreeC = kelvin; offset: 273.15 = °C = ÂșC = degC = celsius') + +def test_celsius(): + """ + Check that the definition of degreeC works + Note: see https://github.com/hgrecco/pint/issues/546 + """ + a = Q_(3, u"°C") + b = Q_(3, u"ÂșC") + c = Q_(3, "degC") + d = Q_(3, "celsius") + e = Q_(276.15, "kelvin") + assert(a == b) + assert(a == c) + assert(a == d) + assert(a == e) + assert(a.to("kelvin") == e) + assert(a == e.to("°C")) + assert(a == e.to("ÂșC")) + assert(a == e.to("celsius")) + assert(u"{}".format(a) == u"3 °C") + assert(u"{}".format(b) == u"3 °C") + assert(u"{}".format(c) == u"3 °C") + assert(u"{}".format(d) == u"3 °C") + + # # I would have expected the following to be formatted as "3 celsius"... + # a.default_format = u"" + # b.default_format = u"" + # c.default_format = u"" + # d.default_format = u"" + # assert(u"{}".format(a) == u"3 celsius") + # assert(u"{}".format(b) == u"3 celsius") + # assert(u"{}".format(c) == u"3 celsius") + # assert(u"{}".format(d) == u"3 celsius") diff --git a/lib/taurus/core/units.py b/lib/taurus/core/units.py index 64a1250e6..0e4b1f709 100644 --- a/lib/taurus/core/units.py +++ b/lib/taurus/core/units.py @@ -35,3 +35,6 @@ UR = UnitRegistry() UR.default_format = '~' # use abbreviated units Q_ = Quantity = UR.Quantity + +# Support degreeC +UR.define(u'degreeC = kelvin; offset: 273.15 = °C = ÂșC = degC = celsius') From 2955841faea1ced2ca469f3dae369eae7527c555 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 Nov 2019 16:10:44 +0100 Subject: [PATCH 102/373] Fix unicode issues in py2 The last commit introduced problems in py2 related to unicode support. Fix them. --- lib/taurus/core/test/test_units.py | 18 ++++++++++-------- lib/taurus/core/units.py | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/taurus/core/test/test_units.py b/lib/taurus/core/test/test_units.py index d778e4e80..f0ec1bea7 100644 --- a/lib/taurus/core/test/test_units.py +++ b/lib/taurus/core/test/test_units.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- ############################################################################# ## @@ -23,13 +24,14 @@ ## ############################################################################# -from taurus.core.units import UR, Q_ +from taurus.core.units import Q_ # from pint import UnitRegistry # UR = UnitRegistry() # Q_= UR.Quantity # UR.define(u'degreeC = kelvin; offset: 273.15 = °C = ÂșC = degC = celsius') + def test_celsius(): """ Check that the definition of degreeC works @@ -37,17 +39,17 @@ def test_celsius(): """ a = Q_(3, u"°C") b = Q_(3, u"ÂșC") - c = Q_(3, "degC") - d = Q_(3, "celsius") - e = Q_(276.15, "kelvin") + c = Q_(3, u"degC") + d = Q_(3, u"celsius") + e = Q_(276.15, u"kelvin") assert(a == b) assert(a == c) assert(a == d) assert(a == e) - assert(a.to("kelvin") == e) - assert(a == e.to("°C")) - assert(a == e.to("ÂșC")) - assert(a == e.to("celsius")) + assert(a.to(u"kelvin") == e) + assert(a == e.to(u"°C")) + assert(a == e.to(u"ÂșC")) + assert(a == e.to(u"celsius")) assert(u"{}".format(a) == u"3 °C") assert(u"{}".format(b) == u"3 °C") assert(u"{}".format(c) == u"3 °C") diff --git a/lib/taurus/core/units.py b/lib/taurus/core/units.py index 0e4b1f709..9ce74b787 100644 --- a/lib/taurus/core/units.py +++ b/lib/taurus/core/units.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- + ############################################################################# ## # This file is part of Taurus From b17df86c0796ae5bd811805054aa61317730422b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 21 Nov 2019 12:53:30 +0100 Subject: [PATCH 103/373] Support unicode in tooltip messages in Python2 Attribute values (or units) containing non-ascii characters trigger an exception when generating a widget's tooltip in Python2. Fix it by using unicode literals in the methods involved. --- lib/taurus/core/tango/tangodevice.py | 4 ++-- lib/taurus/core/taurusattribute.py | 14 +++++++------- lib/taurus/core/taurusauthority.py | 4 ++-- lib/taurus/core/taurusdevice.py | 6 +++--- lib/taurus/qt/qtgui/base/taurusbase.py | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/taurus/core/tango/tangodevice.py b/lib/taurus/core/tango/tangodevice.py index 60e979c85..78d3c4af8 100644 --- a/lib/taurus/core/tango/tangodevice.py +++ b/lib/taurus/core/tango/tangodevice.py @@ -184,9 +184,9 @@ def getDisplayDescrObj(self, cache=True): # extend the info on dev state ret = [] for name, value in desc_obj: - if name.lower() == 'device state' and self.stateObj is not None: + if name.lower() == u'device state' and self.stateObj is not None: tg_state = self.stateObj.read(cache).rvalue.name - value = "%s (%s)" % (value, tg_state) + value = u"%s (%s)" % (value, tg_state) ret.append((name, value)) return ret diff --git a/lib/taurus/core/taurusattribute.py b/lib/taurus/core/taurusattribute.py index eba0365a4..bebb30efd 100644 --- a/lib/taurus/core/taurusattribute.py +++ b/lib/taurus/core/taurusattribute.py @@ -286,12 +286,12 @@ def getDisplayDescription(self, cache=True): def getDisplayDescrObj(self, cache=True): name = self.getLabel(cache=cache) - obj = [('name', name), - ('model', self.getFullName() or '')] + obj = [(u'name', name), + (u'model', self.getFullName() or u'')] descr = self.description if descr: - _descr = descr.replace("<", "<").replace(">", ">") - obj.append(('description', _descr)) + _descr = descr.replace(u"<", u"<").replace(u">", u">") + obj.append((u'description', _descr)) if isinstance(self.rvalue, Quantity): _unitless = self.rvalue.unitless @@ -305,7 +305,7 @@ def getDisplayDescrObj(self, cache=True): else: low = range[0].magnitude high = range[1].magnitude - obj.append(('range', "[%s, %s]" % (low, high))) + obj.append((u'range', u"[%s, %s]" % (low, high))) if alarm != [None, None]: if not _unitless: low = alarm[0] @@ -313,7 +313,7 @@ def getDisplayDescrObj(self, cache=True): else: low = alarm[0].magnitude high = alarm[1].magnitude - obj.append(('alarm', "[%s, %s]" % (low, high))) + obj.append((u'alarm', u"[%s, %s]" % (low, high))) if warning != [None, None]: if not _unitless: low = warning[0] @@ -321,7 +321,7 @@ def getDisplayDescrObj(self, cache=True): else: low = warning[0].magnitude high = warning[1].magnitude - obj.append(('warning', "[%s, %s]" % (low, high))) + obj.append((u'warning', u"[%s, %s]" % (low, high))) return obj def isWritable(self, cache=True): diff --git a/lib/taurus/core/taurusauthority.py b/lib/taurus/core/taurusauthority.py index c5ffdb7c8..e9396a684 100644 --- a/lib/taurus/core/taurusauthority.py +++ b/lib/taurus/core/taurusauthority.py @@ -73,8 +73,8 @@ def getDisplayDescription(self, cache=True): def getDisplayDescrObj(self, cache=True): obj = [] - obj.append(('name', self.getDisplayName(cache=cache))) - obj.append(('description', self.description)) + obj.append((u'name', self.getDisplayName(cache=cache))) + obj.append((u'description', self.description)) return obj def getChildObj(self, child_name): diff --git a/lib/taurus/core/taurusdevice.py b/lib/taurus/core/taurusdevice.py index ae605e1f6..9052155b5 100644 --- a/lib/taurus/core/taurusdevice.py +++ b/lib/taurus/core/taurusdevice.py @@ -106,9 +106,9 @@ def getNameValidator(cls): def getDisplayDescrObj(self, cache=True): obj = [] - obj.append(('name', self.getDisplayName(cache=cache))) - obj.append(('description', self.description)) - obj.append(('device state', self.state.name)) + obj.append((u'name', self.getDisplayName(cache=cache))) + obj.append((u'description', self.description)) + obj.append((u'device state', self.state.name)) return obj def getChildObj(self, child_name): diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index 8ffd62067..cb13a06f2 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -707,12 +707,12 @@ def toolTipObjToStr(self, toolTipObj): """ if toolTipObj is None: return self.getNoneValue() - ret = '' + ret = u'
' for id, value in toolTipObj: - ret += '' % ( + ret += u'' % ( id.capitalize(), value) - ret += '
%s:%s
%s:%s
' + ret += u'' return ret def displayValue(self, v): @@ -783,7 +783,7 @@ def baseFormatter(dtype=None, basecomponent=None, **kwargs): fmt_v = self._format.format(v, bc=self) except Exception: self.debug("Invalid format %r for %r. Using '{0}'", self._format, v) - fmt_v = "{0}".format(v) + fmt_v = u"{0}".format(v) return fmt_v From b8caebf29986303f50ff539af7bb259a36908874 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Tue, 26 Nov 2019 11:32:35 +0100 Subject: [PATCH 104/373] Allow to stop pool timer thread sync Add API to stop the pool timer thread synchronous and use it to mitigate polling issues. --- lib/taurus/core/tauruspollingtimer.py | 6 +++--- lib/taurus/core/util/timer.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/taurus/core/tauruspollingtimer.py b/lib/taurus/core/tauruspollingtimer.py index 0abf035ce..b1d183b5a 100644 --- a/lib/taurus/core/tauruspollingtimer.py +++ b/lib/taurus/core/tauruspollingtimer.py @@ -58,9 +58,9 @@ def start(self): """ Starts the polling timer """ self.timer.start() - def stop(self): + def stop(self, sync=False): """ Stop the polling timer""" - self.timer.stop() + self.timer.stop(sync=sync) def containsAttribute(self, attribute): """Determines if the polling timer already contains this attribute @@ -128,7 +128,7 @@ def removeAttribute(self, attribute): if not attr_dict: del self.dev_dict[dev] if not self.dev_dict: - self.stop() + self.stop(sync=True) def _pollAttributes(self): """Polls the registered attributes. This method is called by the timer diff --git a/lib/taurus/core/util/timer.py b/lib/taurus/core/util/timer.py index c7fc7d1d5..163ab867e 100644 --- a/lib/taurus/core/util/timer.py +++ b/lib/taurus/core/util/timer.py @@ -76,12 +76,14 @@ def start(self): finally: self.__lock.release() - def stop(self): + def stop(self, sync=False): """ Stop Timer Object """ self.debug("Timer::stop()") self.__lock.acquire() self.__loop = False self.__lock.release() + if sync and self.__thread is not None: + self.__thread.join() def __run(self): """ Private Thread Function """ From 995f02102b7febae078e22ce2a917ef6cb6b820b Mon Sep 17 00:00:00 2001 From: zreszela Date: Mon, 2 Dec 2019 10:45:48 +0100 Subject: [PATCH 105/373] Add cache to TaurusAttribute isScalar and isSpectrum (back. compat.) This methods were in Taurus 3 implemented on the configuration class level and exposed on the attribute level with __getattr__ used to have cache argument. Add a dummy argument to maintain the backwards compat. --- lib/taurus/core/tango/tangoattribute.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index b06208816..8eb6955b8 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -1169,11 +1169,13 @@ def getWritable(self, cache=True): return self.isWritable(cache) @taurus4_deprecation(alt='self.data_format') - def isScalar(self): + def isScalar(self, cache=True): + # cache is ignored, it is only for back. compat. return self.data_format == DataFormat._0D @taurus4_deprecation(alt='self.data_format') - def isSpectrum(self): + def isSpectrum(self, cache=True): + # cache is ignored, it is only for back. compat. return self.data_format == DataFormat._1D @taurus4_deprecation(alt='self.data_format') From e4166f5a75f5e1c712ebf4dff5f9bff288b33a1f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 2 Dec 2019 13:09:34 +0100 Subject: [PATCH 106/373] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e707fc760..8e06c6eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ develop branch) won't be reflected in this file. - Py3 exception in `TaurusModelChooser.getListedModels()` (#1008) - (for py2) Improved backwards compatibility of `taurus.qt.qtgui.plot` (#1027) - Exception in DelayedSubscriber (#1030) +- Compatibility issue in deprecated TangoAttribute's `isScalar()` and `isSpectrum()` (#1034) ## [4.6.1] - 2019-08-19 From eb829b5664e77ad8b437edcc980056ec6a9278ca Mon Sep 17 00:00:00 2001 From: cfalcon Date: Wed, 4 Dec 2019 16:41:40 +0100 Subject: [PATCH 107/373] Fix TangoDevicepollResult method This method revives a caseless dict as argument and is trying to get attributes using names with some letter in uppercase. --- lib/taurus/core/tango/tangodevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/tangodevice.py b/lib/taurus/core/tango/tangodevice.py index 60e979c85..e1e40d859 100644 --- a/lib/taurus/core/tango/tangodevice.py +++ b/lib/taurus/core/tango/tangodevice.py @@ -340,7 +340,7 @@ def __pollResult(self, attrs, ts, result, error=False): v, err = None, DevFailed(*da.get_err_stack()) else: v, err = da, None - attr = attrs[da.name] + attr = attrs[da.name.lower()] attr.poll(single=False, value=v, error=err, time=ts) def __pollAsynch(self, attrs): From 470da8812dc1a5025ee31d56835de57daa14e041 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 10 Dec 2019 10:24:04 +0100 Subject: [PATCH 108/373] Revert "Fix TangoDevicepollResult method" This reverts commit eb829b5664e77ad8b437edcc980056ec6a9278ca. --- lib/taurus/core/tango/tangodevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/tangodevice.py b/lib/taurus/core/tango/tangodevice.py index e1e40d859..60e979c85 100644 --- a/lib/taurus/core/tango/tangodevice.py +++ b/lib/taurus/core/tango/tangodevice.py @@ -340,7 +340,7 @@ def __pollResult(self, attrs, ts, result, error=False): v, err = None, DevFailed(*da.get_err_stack()) else: v, err = da, None - attr = attrs[da.name.lower()] + attr = attrs[da.name] attr.poll(single=False, value=v, error=err, time=ts) def __pollAsynch(self, attrs): From 0b7bbc3e398b699bba5dce07f4f19c59e1b81eda Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 10 Dec 2019 11:06:39 +0100 Subject: [PATCH 109/373] support dict-like argument to CaselessDict Caselessdict fails when initialized with a Mapping type that is not an instance of dict (e.g. a weakref.WeakValueDict). Fix it by using duck-typing instead of strict inheritance check. Note: when py2 support is dropped, we can change this to interface checking using collections.abc --- lib/taurus/core/util/containers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/taurus/core/util/containers.py b/lib/taurus/core/util/containers.py index 8fbd5ce61..61aca30ef 100644 --- a/lib/taurus/core/util/containers.py +++ b/lib/taurus/core/util/containers.py @@ -254,9 +254,15 @@ class CaselessDict(dict): def __init__(self, other=None): if other: # Doesn't do keyword args - if isinstance(other, dict): + + # ------------------------------------------------------- + # TODO: when we drop py2 support, change this to + # `if isinstance(other, collections.abc.Mapping)` + # + if hasattr(other, 'items'): for k, v in other.items(): dict.__setitem__(self, k.lower(), v) + # ------------------------------------------------------- else: for k, v in other: dict.__setitem__(self, k.lower(), v) From fbe4993f19a2e1f58309f65c8096416c97b1941c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 10 Dec 2019 11:09:49 +0100 Subject: [PATCH 110/373] Use caseless dict for case-insensitive schemes TaurusPollingTimer._pollAttributes uses an internal cache that breaks case insensitive schemes. Fix it by switching to CaselessDict when appropriate. --- lib/taurus/core/tauruspollingtimer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/taurus/core/tauruspollingtimer.py b/lib/taurus/core/tauruspollingtimer.py index b1d183b5a..ed1efd7e0 100644 --- a/lib/taurus/core/tauruspollingtimer.py +++ b/lib/taurus/core/tauruspollingtimer.py @@ -29,7 +29,7 @@ import threading from .util.log import Logger -from .util.containers import CaselessWeakValueDict +from .util.containers import CaselessWeakValueDict, CaselessDict from .util.timer import Timer __all__ = ["TaurusPollingTimer"] @@ -138,7 +138,10 @@ def _pollAttributes(self): dev_dict = {} with self.lock: for dev, attrs in self.dev_dict.items(): - dev_dict[dev] = dict(attrs) + if dev.factory().caseSensitive: + dev_dict[dev] = dict(attrs) + else: + dev_dict[dev] = CaselessDict(attrs) for dev, attrs in dev_dict.items(): try: From 367f08283bbb7b719e64bd47f3d793567c477051 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 10 Dec 2019 15:25:23 +0100 Subject: [PATCH 111/373] Support spyder v4 The taurus.qt.qtgui.editor module fails to import with spyder 4. Adapt its imports to support spyder 3 and 4 simultaneously. --- lib/taurus/qt/qtgui/editor/tauruseditor.py | 32 ++++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/taurus/qt/qtgui/editor/tauruseditor.py b/lib/taurus/qt/qtgui/editor/tauruseditor.py index bc472974b..06c27bcf3 100644 --- a/lib/taurus/qt/qtgui/editor/tauruseditor.py +++ b/lib/taurus/qt/qtgui/editor/tauruseditor.py @@ -33,10 +33,21 @@ from spyder.utils.qthelpers import create_toolbutton from spyder.widgets.findreplace import FindReplace -from spyder.widgets.editortools import OutlineExplorerWidget -from spyder.widgets.editor import EditorMainWindow, EditorSplitter +try: + from spyder.plugins.outlineexplorer.widgets import OutlineExplorerWidget +except ImportError: + # spyder v3 + from spyder.widgets.editortools import OutlineExplorerWidget +try: + from spyder.plugins.editor.widgets.editor import ( + EditorMainWindow, + EditorSplitter, + ) +except: + # spyder v3 + from spyder.widgets.editor import EditorMainWindow, EditorSplitter from spyder.py3compat import to_text_string -from spyder.utils.introspection.manager import IntrospectionManager + class TaurusBaseEditor(Qt.QSplitter): def __init__(self, parent=None): @@ -72,11 +83,16 @@ def __init__(self, parent=None): self.menu_list = None self.setup_window([], []) - # Set introspector - introspector = IntrospectionManager() - editorstack = self.editor_splitter.editorstack - editorstack.set_introspector(introspector) - introspector.set_editor_widget(editorstack) + try: + # spyder v3 + from spyder.utils.introspection.manager import IntrospectionManager + # Set introspector + introspector = IntrospectionManager() + editorstack = self.editor_splitter.editorstack + editorstack.set_introspector(introspector) + introspector.set_editor_widget(editorstack) + except ImportError: + pass # TODO: support introspection with spyder v4 def createMenuActions(self): """Returns a list of menu actions and a list of IO actions. From db66557f00229b8b768e1bd5324b6d9cfb3afa29 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 10 Dec 2019 16:04:57 +0100 Subject: [PATCH 112/373] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e06c6eeb..87523f689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ develop branch) won't be reflected in this file. ### Added - check-deps subcommand (#988) - `taurus.cli.register_subcommands()` and `taurus.cli.taurus_cmd()` (#991) +- Support for spyder v4 in `taurus.qt.qtgui.editor` (#1038) ### Removed ### Changed From a4d9da1f265a047c533b7a23450c8fad64e898f0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 11 Dec 2019 09:21:37 +0100 Subject: [PATCH 113/373] Avoid duplicated (and optimistic) assert The same check is done before and after waiting for a polling period. Remove the one before the wait because the cleanup is only guaranteed after one extra iteration of the polling timer. --- lib/taurus/core/tango/test/test_tangofactory.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/taurus/core/tango/test/test_tangofactory.py b/lib/taurus/core/tango/test/test_tangofactory.py index a2dc762ed..114febc8f 100644 --- a/lib/taurus/core/tango/test/test_tangofactory.py +++ b/lib/taurus/core/tango/test/test_tangofactory.py @@ -89,7 +89,6 @@ def test_cleanup_after_polling(): assert len(list(f.tango_attrs.keys())) == 1 assert len(list(f.tango_devs.keys())) == 1 a = None - assert len(list(f.tango_attrs.keys())) == 0 time.sleep(polling_period) assert len(list(f.tango_attrs.keys())) == 0 assert len(list(f.tango_devs.keys())) == 0 From 285adb4965b0abe1f085ae175c752b5fd1d5c0b5 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 11 Dec 2019 09:29:34 +0100 Subject: [PATCH 114/373] (doc) Fix param descriptions --- lib/taurus/core/taurusfactory.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/taurus/core/taurusfactory.py b/lib/taurus/core/taurusfactory.py index f1d632eec..44cdd96b1 100644 --- a/lib/taurus/core/taurusfactory.py +++ b/lib/taurus/core/taurusfactory.py @@ -340,7 +340,8 @@ def addAttributeToPolling(self, attribute, period, unsubscribe_evts=False): """Activates the polling (client side) for the given attribute with the given period (seconds). - :param attribute: (taurus.core.tango.TaurusAttribute) attribute name. + :param attribute: (taurus.core.taurusattribute.TaurusAttribute) + the attribute to be added :param period: (float) polling period (in seconds) :param unsubscribe_evts: (bool) whether or not to unsubscribe from events """ @@ -352,7 +353,8 @@ def removeAttributeFromPolling(self, attribute): """Deactivate the polling (client side) for the given attribute. If the polling of the attribute was not previously enabled, nothing happens. - :param attribute: (str) attribute name. + :param attribute: (taurus.core.taurusattribute.TaurusAttribute) + the attribute to be removed """ timers = dict(self.polling_timers) for period, timer in timers.items(): From c0d7743c77d9a7d4d00d6901dfde5c8c50fef325 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Tue, 10 Dec 2019 14:24:23 +0100 Subject: [PATCH 115/373] Add entrypoint for registering formaters Create entrypoint for the taurus formater. Register default taurus formater and tango formater. --- lib/taurus/qt/qtgui/base/taurusbase.py | 19 +++++++++++++++++++ setup.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index cb13a06f2..33e901136 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -29,6 +29,7 @@ import sys import threading +import pkg_resources from types import MethodType from future.builtins import str from future.utils import string_types @@ -37,6 +38,7 @@ from enum import Enum import taurus +from taurus import warning from taurus.core.util import eventfilters from taurus.core.util.timer import Timer from taurus.core.taurusbasetypes import TaurusElementType, TaurusEventType @@ -66,6 +68,23 @@ DefaultNoneValue = "-----" + +# ------------------------------------------------------------------------ +# Note: this is an experimental feature introduced in v 4.6.4a +# It may be removed or changed in future releases + +scheme_formatters = {} +for __p in pkg_resources.iter_entry_points('taurus.qt.qtgui.base.formatter'): + try: + scheme_formatters[__p.name] = '{}.{}'.format(__p.module_name, + __p.attrs[0]) + except Exception as e: + warning('Could not load formatter plugin "%s". Reason: %s', + __p.module_name, e) + +# ------------------------------------------------------------------------ + + def defaultFormatter(dtype=None, basecomponent=None, **kwargs): """ Default formatter callable. Returns a format string based on dtype diff --git a/setup.py b/setup.py index e68f6c32a..9448fb768 100644 --- a/setup.py +++ b/setup.py @@ -118,10 +118,16 @@ def get_release_info(): 'Tango = taurus.qt.qtgui.panel.taurusmodelchooser:TangoModelSelectorItem', ] +formatters = [ + 'default = taurus.qt.qtgui.base.taurusbase:defaultFormatter', + 'tango = taurus.core.tango.util.formatter:tangoFormatter', +] + entry_points = { 'console_scripts': console_scripts, 'taurus.cli.subcommands': taurus_subcommands, 'taurus.qt.qtgui.panel.TaurusModelSelector.items': model_selectors, + 'taurus.qt.qtgui.base.formatter': formatters, } classifiers = [ From d634bd7a827f29ea3c9186e4ebe0fae199c44df2 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Tue, 10 Dec 2019 15:25:18 +0100 Subject: [PATCH 116/373] Improve set formatter dialog Use an editable combobox instead lineedit. Fill the combobox with the registered formaters. Add fromat example Fix #1035 --- lib/taurus/qt/qtgui/base/taurusbase.py | 33 ++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index 33e901136..13d587ea9 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -1361,6 +1361,8 @@ def __init__(self, name='', parent=None, designMode=False, **kwargs): self.call__init__(TaurusBaseComponent, name, parent=parent, designMode=designMode) self._setText = self._findSetTextMethod() + self._formaters = scheme_formatters + self._formaters['{:2.3e}'] = '{:2.3e}' def showFormatterDlg(self): """ @@ -1369,13 +1371,27 @@ def showFormatterDlg(self): (in string version) or None """ current_format = self.getFormat() + ind = self._formaters.values().index(current_format) + + formatter, ok = Qt.QInputDialog.getItem(self, "Set formatter", + "Choose/Enter a formatter:", + list(self._formaters.keys()), + current=ind, + editable=True) + + try: + moduleName, formatterName = formatter.rsplit('.', 1) + __import__(moduleName) + k, v = formatterName, formatter + except: + # Python format string + k = v = formatter + + if not self._formaters.has_key(k): + self._formaters[k] = v - formatter, ok = Qt.QInputDialog.getText(self, "Set formatter", - "Enter a formatter:", - Qt.QLineEdit.Normal, - current_format) if ok and formatter: - return formatter + return self._formaters[k] return None @@ -1390,13 +1406,6 @@ def onSetFormatter(self): if format is not None: self.debug( 'Default format has been changed to: {0}'.format(format)) - # ----------------------------------------------------------------- - # TODO: Tango-centric (replace by agnostic entry point solution) - # shortcut to setup the tango formatter - if format.strip() == "tangoFormatter": - from taurus.core.tango.util.formatter import tangoFormatter - format = tangoFormatter - # ----------------------------------------------------------------- self.setFormat(format) return format From b3f9118e489c621db11a4bb3cc17a17da0087500 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 11 Dec 2019 11:40:40 +0100 Subject: [PATCH 117/373] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e06c6eeb..fa3dc4917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ develop branch) won't be reflected in this file. - Several issues affecting synoptics (#1005, #1029) - Support dns aliases for the authority name in tango model names (#998) - Py3 exception in `TaurusModelChooser.getListedModels()` (#1008) +- Thread safety issues in `TaurusPollingTimer`'s add/remove attributes API (#1022, #999) - (for py2) Improved backwards compatibility of `taurus.qt.qtgui.plot` (#1027) - Exception in DelayedSubscriber (#1030) - Compatibility issue in deprecated TangoAttribute's `isScalar()` and `isSpectrum()` (#1034) From a6b95f43633533c3489fecf0bcf7de1c78de3415 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 11 Dec 2019 13:17:20 +0100 Subject: [PATCH 118/373] Improve tests for bug-999 - including the (special) state attribute - use mixed-case to check case handling --- .../core/tango/test/test_tangofactory.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/test/test_tangofactory.py b/lib/taurus/core/tango/test/test_tangofactory.py index 114febc8f..56acfbcc8 100644 --- a/lib/taurus/core/tango/test/test_tangofactory.py +++ b/lib/taurus/core/tango/test/test_tangofactory.py @@ -81,9 +81,28 @@ def test_cleanup_after_polling(): """ Ensure that polling a Tango attribute does not keep device alive See Bug #999 + (Also check case insensitivity) """ polling_period = .1 # seconds - a = taurus.Attribute('sys/tg_test/1/float_scalar') + a = taurus.Attribute('sys/TG_test/1/FLOAT_scalar') + f = a.factory() + a.activatePolling(polling_period * 1000, force=True) + assert len(list(f.tango_attrs.keys())) == 1 + assert len(list(f.tango_devs.keys())) == 1 + a = None + time.sleep(polling_period) + assert len(list(f.tango_attrs.keys())) == 0 + assert len(list(f.tango_devs.keys())) == 0 + + +def test_cleanup_state_after_polling(): + """ + Ensure that polling the state Tango attribute does not keep device alive + See Bug #999 + (Also check case insensitivity) + """ + polling_period = .1 # seconds + a = taurus.Attribute('sys/TG_TEST/1/STate') f = a.factory() a.activatePolling(polling_period * 1000, force=True) assert len(list(f.tango_attrs.keys())) == 1 From 0ac758599a27975e63c2abceae8d95f7ac074bee Mon Sep 17 00:00:00 2001 From: Philippe GAURON Date: Wed, 11 Dec 2019 13:40:34 +0100 Subject: [PATCH 119/373] fix some missing breaklines in RST doc --- lib/taurus/qt/qtgui/base/taurusbase.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index cb13a06f2..970ad0c00 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -735,6 +735,7 @@ def displayValue(self, v): that returns a python format string. If a callable is used, it will be called with the following keyword arguments: + - dtype: the data type of the value to be formatted - basecomponent: the affected widget @@ -1019,6 +1020,7 @@ def setModelInConfig(self, yesno): restored when calling :meth:`applyConfig`). By default this is not enabled. The following properties are affected by this: + - "model" :param yesno: (bool) If True, the model-related properties will be @@ -1914,6 +1916,7 @@ def getQtDesignerPluginInfo(cls): The dictionary returned by this method should contain *at least* the following keys and values: + - 'module' : a string representing the full python module name (ex.: 'taurus.qt.qtgui.base') - 'icon' : a string representing valid resource icon (ex.: 'designer:combobox.png') - 'container' : a bool telling if this widget is a container widget or not. From 1f1e35e34f0a0f13ee5bffbc0a6647e8a38225cd Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 11 Dec 2019 13:52:42 +0100 Subject: [PATCH 120/373] Bump version 4.6.4-alpha to 4.6.5-alpha --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2deadd771..cd5bc215a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.6.4-alpha +current_version = 4.6.5-alpha parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index 520e0dbff..52f80175c 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.6.4-alpha' +version = '4.6.5-alpha' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: From c37ee6d1ab0966e418035110845bf9be7a161445 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Thu, 12 Dec 2019 16:59:30 +0100 Subject: [PATCH 121/373] Refactoring formatter register Move ep register to a static method in TaurusBaseComponent. Fix typos --- lib/taurus/qt/qtgui/base/taurusbase.py | 57 +++++++++++++++----------- setup.py | 6 ++- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index 13d587ea9..6c83f05a3 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -69,22 +69,6 @@ -# ------------------------------------------------------------------------ -# Note: this is an experimental feature introduced in v 4.6.4a -# It may be removed or changed in future releases - -scheme_formatters = {} -for __p in pkg_resources.iter_entry_points('taurus.qt.qtgui.base.formatter'): - try: - scheme_formatters[__p.name] = '{}.{}'.format(__p.module_name, - __p.attrs[0]) - except Exception as e: - warning('Could not load formatter plugin "%s". Reason: %s', - __p.module_name, e) - -# ------------------------------------------------------------------------ - - def defaultFormatter(dtype=None, basecomponent=None, **kwargs): """ Default formatter callable. Returns a format string based on dtype @@ -101,6 +85,10 @@ def defaultFormatter(dtype=None, basecomponent=None, **kwargs): return basecomponent.defaultFormatDict.get(dtype, "{0}") +expFormatter = '{:2.3e}' +floatFormatter = '{:.5f}' + + class TaurusBaseComponent(TaurusListener, BaseConfigurableClass): """A generic Taurus component. @@ -194,6 +182,23 @@ def __init__(self, name='', parent=None, designMode=False, **kwargs): except Exception as e: self.warning('Could not connect taurusEvent signal: %r', e) + @staticmethod + def get_registered_formatters(): + """ Static method to get the registered formatters + """ + # Note: this is an experimental feature introduced in v 4.6.4a + # It may be removed or changed in future releases + + formatters = {} + for ep in pkg_resources.iter_entry_points('taurus.qt.formatters'): + try: + formatters[ep.name] = '{}.{}'.format(ep.module_name, + ep.attrs[0]) + except Exception as e: + warning('Could not load formatter plugin "%s". Reason: %s', + ep.module_name, e) + return formatters + @deprecation_decorator(rel='4.0') def getSignaller(self): return self @@ -1361,8 +1366,7 @@ def __init__(self, name='', parent=None, designMode=False, **kwargs): self.call__init__(TaurusBaseComponent, name, parent=parent, designMode=designMode) self._setText = self._findSetTextMethod() - self._formaters = scheme_formatters - self._formaters['{:2.3e}'] = '{:2.3e}' + self._formatters = None def showFormatterDlg(self): """ @@ -1370,12 +1374,19 @@ def showFormatterDlg(self): :return: formatter: python fromat string or formatter callable (in string version) or None """ + if self._formatters is None: + # initialize cache + self._formatters = TaurusBaseWidget.get_registered_formatters() + current_format = self.getFormat() - ind = self._formaters.values().index(current_format) + try: + ind = self._formatters.values().index(current_format) + except ValueError: + ind = 0 formatter, ok = Qt.QInputDialog.getItem(self, "Set formatter", "Choose/Enter a formatter:", - list(self._formaters.keys()), + list(self._formatters.keys()), current=ind, editable=True) @@ -1387,11 +1398,11 @@ def showFormatterDlg(self): # Python format string k = v = formatter - if not self._formaters.has_key(k): - self._formaters[k] = v + if not self._formatters.has_key(k): + self._formatters[k] = v if ok and formatter: - return self._formaters[k] + return self._formatters[k] return None diff --git a/setup.py b/setup.py index 9448fb768..34d777f71 100644 --- a/setup.py +++ b/setup.py @@ -119,15 +119,17 @@ def get_release_info(): ] formatters = [ - 'default = taurus.qt.qtgui.base.taurusbase:defaultFormatter', + 'taurus = taurus.qt.qtgui.base.taurusbase:defaultFormatter', 'tango = taurus.core.tango.util.formatter:tangoFormatter', + '{:2.3e} = taurus.qt.qtgui.base.taurusbase:expFormatter', + '{:.5f} = taurus.qt.qtgui.base.taurusbase:floatFormatter', ] entry_points = { 'console_scripts': console_scripts, 'taurus.cli.subcommands': taurus_subcommands, 'taurus.qt.qtgui.panel.TaurusModelSelector.items': model_selectors, - 'taurus.qt.qtgui.base.formatter': formatters, + 'taurus.qt.formatters': formatters, } classifiers = [ From 03f0831eac0fa4f1f93652e3c714caa7f1a0489b Mon Sep 17 00:00:00 2001 From: cfalcon Date: Thu, 12 Dec 2019 17:44:10 +0100 Subject: [PATCH 122/373] Improve the management of formatters EP Load the entry point instead of keeping its string representation in the discovery. Register new formatter EP when a formatter is a callable (e.g. "full.module.callable") --- lib/taurus/qt/qtgui/base/taurusbase.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index 6c83f05a3..1ccd8f4fc 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -192,11 +192,11 @@ def get_registered_formatters(): formatters = {} for ep in pkg_resources.iter_entry_points('taurus.qt.formatters'): try: - formatters[ep.name] = '{}.{}'.format(ep.module_name, - ep.attrs[0]) + formatters[ep.name] = ep.load() except Exception as e: - warning('Could not load formatter plugin "%s". Reason: %s', - ep.module_name, e) + taurus.warning('Cannot load "%s" formatter. Reason: %r', + ep.name, e) + return formatters @deprecation_decorator(rel='4.0') @@ -1378,9 +1378,9 @@ def showFormatterDlg(self): # initialize cache self._formatters = TaurusBaseWidget.get_registered_formatters() - current_format = self.getFormat() + # Find index of the current formatter try: - ind = self._formatters.values().index(current_format) + ind = self._formatters.values().index(self.FORMAT) except ValueError: ind = 0 @@ -1391,9 +1391,14 @@ def showFormatterDlg(self): editable=True) try: + # Try to register new formatter moduleName, formatterName = formatter.rsplit('.', 1) - __import__(moduleName) - k, v = formatterName, formatter + f = pkg_resources.get_entry_map('taurus')['taurus.qt.formatters'] + ep = pkg_resources.EntryPoint.parse('{} = {}:{}'.format( + formatterName, moduleName, formatterName), + dist=pkg_resources.get_distribution('taurus')) + f[formatterName] = ep + k, v = formatterName, ep.load() except: # Python format string k = v = formatter From e668af518f2f279eac4c30bd3c96fd800bd212ca Mon Sep 17 00:00:00 2001 From: cfalcon Date: Thu, 12 Dec 2019 18:24:49 +0100 Subject: [PATCH 123/373] Add link to the documentation Add link to the formatter documentation in the formatter dialog --- lib/taurus/qt/qtgui/base/taurusbase.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index 1ccd8f4fc..c3a23be16 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -1384,8 +1384,12 @@ def showFormatterDlg(self): except ValueError: ind = 0 + uri = "http://taurus-scada.org/devel/api/taurus/qt/qtgui/base/_TaurusBaseComponent.html" + help = " Find more info in doc".format(uri) + + msg = "Choose/Enter a formatter: {}".format(help) formatter, ok = Qt.QInputDialog.getItem(self, "Set formatter", - "Choose/Enter a formatter:", + msg, list(self._formatters.keys()), current=ind, editable=True) From 7d2d21fee4b4ad86d39b29810a4f7ca7d32f63fd Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 8 Jan 2020 11:13:31 +0100 Subject: [PATCH 124/373] Add debugging info on model handling exceptions Log the exception message with level=debug) if getting an exception while loading a model in TaurusForm. Fixes #1046 --- lib/taurus/qt/qtgui/panel/taurusform.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 3fa02aaf9..204dd56de 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -331,9 +331,10 @@ def getFormWidget(self, model=None): except: try: obj = taurus.Device(model) - except: + except Exception as e: self.warning( - 'Cannot handle model "%s". Using default widget.' % (model)) + 'Cannot handle model "%s". Using default widget.', model) + self.debug('Model error: %s', e) return self._defaultFormWidget, (), {} try: key = obj.getDeviceProxy().info().dev_class # TODO: Tango-centric From 00d049cf3941723de4a888f4cf5b30753f56357a Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 8 Jan 2020 11:21:34 +0100 Subject: [PATCH 125/373] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ab2fd354..c425490b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ develop branch) won't be reflected in this file. - Qt theme no longer set to TangoIcons by default (for coherence with docs) (#1012) - (for developers) Support of tox and change to pytest. More platforms being now automatically tested by travis (#994) +- TaurusForm provides more debugging info when failing to handle a model (#1049) ### Deprecated ### Fixed From cf997d9a4b5b1cb97714029efe1edc52b40d68f9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 9 Jan 2020 11:41:49 +0100 Subject: [PATCH 126/373] [CI] Pin tango-test docker version to 9.3.3-rc1 Many tests that depend on the TangoTest service provided by the tangocs/tango-test docker image started to fail at about the time when the tangocs/tango-test:latest was updated to point to tangocs/tango-test:2.1 (december 2019). Previously it pointed to tangocs/tango-test:9.3.3-rc1 and tests were ok. A manual check confirms that using tangocs/tango-test:latest (=2.1) causes the TangoTest service not being available (the container exits immediately). Fix by pinning the version of the docker image to tangocs/tango-test:9.3.3-rc1 --- ci/tango_docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/tango_docker-compose.yml b/ci/tango_docker-compose.yml index 8367e352c..313924f5c 100644 --- a/ci/tango_docker-compose.yml +++ b/ci/tango_docker-compose.yml @@ -21,7 +21,7 @@ services: depends_on: - tango-db tango-test: - image: tangocs/tango-test:latest + image: tangocs/tango-test:9.3.3-rc1 environment: - TANGO_HOST=tango-cs:10000 links: From 1125fe98d93539bdc56be1d6744af00e7c59db82 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 28 Jan 2020 16:49:24 +0100 Subject: [PATCH 127/373] (doc) Add plotting guide Add docs about plotting options (content copied from https://github.com/taurus-org/taurus/wiki/Best-Practices-for-Taurus-4 ) --- doc/source/devel/index.rst | 1 + doc/source/devel/plotting_guide.rst | 54 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 doc/source/devel/plotting_guide.rst diff --git a/doc/source/devel/index.rst b/doc/source/devel/index.rst index e917c7ba9..009fed192 100644 --- a/doc/source/devel/index.rst +++ b/doc/source/devel/index.rst @@ -13,6 +13,7 @@ Developer's Guide Designer tutorial Icon guide Core tutorial + Plotting guide Taurus Custom Settings Examples API diff --git a/doc/source/devel/plotting_guide.rst b/doc/source/devel/plotting_guide.rst new file mode 100644 index 000000000..33daff628 --- /dev/null +++ b/doc/source/devel/plotting_guide.rst @@ -0,0 +1,54 @@ +.. _plotting-guide: + +===================== +Taurus plotting guide +===================== + +*TL;DR*: Use taurus-pyqtgraph_ + +In taurus, the following dependencies are used for its various plotting widgets: + +- PyQwt5_: for the `TaurusPlot` and `TaurusTrend` implemented in :mod:`taurus.qt.qtgui.qwt5` + (former :mod:`taurus.qt.qtgui.plot`). PyQwt5 is unmaintained, removed from debian10, and only works with py2+Qt4, + so it needs to be replaced urgently. **DO NOT BASE ANY NEW DEVELOPMENT ON THIS** +- guiqwt_: for `TaurusImageDialog` and `TaurusTrend2DDialog` implemented in :mod:`taurus.qt.qtgui.extra_guiqwt`. + It does not depend at all of PyQwt5, and supports py2, py3 and Qt4, Qt5 so replacing it is not urgent + (but still it should be eventually replaced by pyqtgraph-based widgets) +- pyqtgraph_: for `TaurusPlot` and `TaurusTrend` implementions in :mod:`taurus.qt.qtgui.qtgui.tpg`] provided by the + taurus_pyqtgraph_ plugin. It supports py2, py3 and Qt4, Qt5 and is the intended replacement for all taurus plotting + widgets (TEP17_). + +Even if the TEP17_ is not yet accepted, in practice the taurus_pyqtgraph_ widgets are the best (in some cases the only) +option for new developments involving plotting. + +A lot of `features are already implemented in taurus_pyqtgraph `_ +and some features are already better than in the PyQwt5 implementations. + +Still, some features available in the old PyQwt5-based implementations of `TaurusPlot` and `TaurusTrend` are still +missing in the pyqtgraph-based implementations (any help with this is welcome). The main ones are: + +- persistence of user changes related to models (but this is not needed for use cases where the models are set +programmatically and not altered by the user) +- full support of tango archiving in TaurusTrend (but basic support already available with the taurus_tangoarchiving_ plugin) +- maturity (certainly these classes are less tested than the qwt5 ones!) + +Tips for Getting started with taurus_pyqtgraph_ +------------------------------------------------ + +1. install the plugin (the module will be installed as :mod:`taurus_pyqtgraph` **and** at the same time will be available as + :mod:`taurus.qt.qtgui.tpg`) +2. The philosophy is that you should use `tpg` as an extension to regular pyqtgraph_ widgets. Therefore you should read + `the official pyqtgraph docs `_ , and also run the official demo with + `python -m pyqtgraph.examples` +3. :mod:`taurus_pyqtgraph` also has `some examples `_. + Have a look at them. Also have a look at the `__main__` sections of the files in the :mod:`taurus_pyqtgraph` module +4. See `this tutorial `_. + + + +.. _PyQwt5: https://github.com/PyQwt/PyQwt5 +.. _guiqwt: https://pythonhosted.org/guiqwt/ +.. _pyqtgraph: http://www.pyqtgraph.org/ +.. _taurus_pyqtgraph: https://github.com/taurus-org/taurus_pyqtgraph +.. _taurus_tangoarchiving: https://github.com/taurus-org/tangoarchiving-scheme +.. _TEP17: https://github.com/cpascual/taurus/blob/tep17/doc/source/tep/TEP17.md From 59ca066f1fc05669ad758b68ece7b0b01cd359db Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 29 Jan 2020 13:25:50 +0100 Subject: [PATCH 128/373] (doc) Fix sphinx warnings --- doc/source/devel/plotting_guide.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/devel/plotting_guide.rst b/doc/source/devel/plotting_guide.rst index 33daff628..e2690c7de 100644 --- a/doc/source/devel/plotting_guide.rst +++ b/doc/source/devel/plotting_guide.rst @@ -4,7 +4,7 @@ Taurus plotting guide ===================== -*TL;DR*: Use taurus-pyqtgraph_ +*TL;DR*: Use taurus_pyqtgraph_ In taurus, the following dependencies are used for its various plotting widgets: @@ -28,10 +28,11 @@ Still, some features available in the old PyQwt5-based implementations of `Tauru missing in the pyqtgraph-based implementations (any help with this is welcome). The main ones are: - persistence of user changes related to models (but this is not needed for use cases where the models are set -programmatically and not altered by the user) + programmatically and not altered by the user) - full support of tango archiving in TaurusTrend (but basic support already available with the taurus_tangoarchiving_ plugin) - maturity (certainly these classes are less tested than the qwt5 ones!) + Tips for Getting started with taurus_pyqtgraph_ ------------------------------------------------ From 65b422872a0ab0fa17c127640b6eab28500a919a Mon Sep 17 00:00:00 2001 From: cfalcon Date: Fri, 31 Jan 2020 08:43:17 +0100 Subject: [PATCH 129/373] Fix missing precision initialization Some widgets report 'Invalid format' warning messages for TangoAttributes URIS of Type.Integer. The precision attribute is not initialized right for integer types. Fix it. --- lib/taurus/core/tango/tangoattribute.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 8eb6955b8..043334b0f 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -1078,6 +1078,8 @@ def _decodeAttrInfoEx(self, pytango_attrinfoex=None): match = re.search("[^\.]*\.(?P[0-9]+)[eEfFgG%]", fmt) if match: self.precision = int(match.group(1)) + elif re.match("%[0-9]*d", '%d'): + self.precision = 0 # self._units and self._display_format is to be used by # TangoAttrValue for performance reasons. Do not rely on it in other # code From a7be3c7f89059852b6904d086ccc45780e3ba276 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 3 Feb 2020 09:39:17 +0100 Subject: [PATCH 130/373] (doc) 3 to 4 guide: replacement for tango's .state() In taurus 3.x, The tango.DeviceProxy.state() method was accessible from the TangoDevice object, but in taurus 4 it is shadowed by the TaurusDevice.state attribute. Suggest a replacement in the 3 to 4 migration guide. Note that cache should **not** be used. --- doc/source/devel/taurus3to4.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/devel/taurus3to4.rst b/doc/source/devel/taurus3to4.rst index 21fbe7042..275acb917 100644 --- a/doc/source/devel/taurus3to4.rst +++ b/doc/source/devel/taurus3to4.rst @@ -143,6 +143,9 @@ This list is a best-effort to document changes, but it may not be 100% complete | TangoDevice.getState | TangoDevice.stateObj.read().rvalue *tango* or | | | .state *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ +| TangoDevice.state() *PyTango.DeviceProxy API* | TangoDevice.stateObj.read(cache=False).rvalue *tango* or | +| | .state *agnostic* | ++-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getStateObj | TangoDevice.stateObj *tango* or | | | .factory.getAttribute(state_full_name) *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ From fd53336b69db0f36869d7a3f5ac0a155c91f4472 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 3 Feb 2020 09:47:59 +0100 Subject: [PATCH 131/373] (doc) minor formatting changes --- doc/source/devel/taurus3to4.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/source/devel/taurus3to4.rst b/doc/source/devel/taurus3to4.rst index 275acb917..16c81169b 100644 --- a/doc/source/devel/taurus3to4.rst +++ b/doc/source/devel/taurus3to4.rst @@ -141,17 +141,18 @@ This list is a best-effort to document changes, but it may not be 100% complete +-----------------------------------------------+---------------------------------------------------------------+ +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getState | TangoDevice.stateObj.read().rvalue *tango* or | -| | .state *agnostic* | +| | .state *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.state() *PyTango.DeviceProxy API* | TangoDevice.stateObj.read(cache=False).rvalue *tango* or | | | .state *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getStateObj | TangoDevice.stateObj *tango* or | -| | .factory.getAttribute(state_full_name) *agnostic* | +| | .factory.getAttribute(state_full_name) *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getSWState | TangoDevice.state | +-----------------------------------------------+---------------------------------------------------------------+ -| TangoDevice.getValueObj | TangoDevice.state *agnostic or* stateObj.read *tango* | +| TangoDevice.getValueObj | TangoDevice.stateObj.read *tango* or | +| | state *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getDisplayValue | TangoDevice.state().name | +-----------------------------------------------+---------------------------------------------------------------+ From 529e95064a6429d5997b4e764a6866a022da8fd3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 3 Feb 2020 09:52:30 +0100 Subject: [PATCH 132/373] Fix exception in TangoDevice.getDisplayValue The deprecated TangoDevice.getDisplayValue method is buggy (and its suggested replacement in the migration guide as well). Fix both. --- doc/source/devel/taurus3to4.rst | 3 ++- lib/taurus/core/tango/tangodevice.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/source/devel/taurus3to4.rst b/doc/source/devel/taurus3to4.rst index 16c81169b..8214e7691 100644 --- a/doc/source/devel/taurus3to4.rst +++ b/doc/source/devel/taurus3to4.rst @@ -154,7 +154,8 @@ This list is a best-effort to document changes, but it may not be 100% complete | TangoDevice.getValueObj | TangoDevice.stateObj.read *tango* or | | | state *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ -| TangoDevice.getDisplayValue | TangoDevice.state().name | +| TangoDevice.getDisplayValue | TangoDevice.stateObj.read().rvalue.name *tango* or | +| | state.name *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getHWObj | TangoDevice.getDeviceProxy | +-----------------------------------------------+---------------------------------------------------------------+ diff --git a/lib/taurus/core/tango/tangodevice.py b/lib/taurus/core/tango/tangodevice.py index 78d3c4af8..91731b745 100644 --- a/lib/taurus/core/tango/tangodevice.py +++ b/lib/taurus/core/tango/tangodevice.py @@ -200,9 +200,9 @@ def cleanUp(self): self._deviceObj = None TaurusDevice.cleanUp(self) - @taurus4_deprecation(alt='.state().name') + @taurus4_deprecation(alt='.state.name') def getDisplayValue(self, cache=True): - return self.state(cache).name + return self.stateObj.read(cache=cache).rvalue.name def _createHWObject(self): try: From f0f1cec5e2da24937527bc8a66307c136bba80f6 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 3 Feb 2020 10:04:36 +0100 Subject: [PATCH 133/373] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ab2fd354..9d2d1d6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ develop branch) won't be reflected in this file. - (for py2) Improved backwards compatibility of `taurus.qt.qtgui.plot` (#1027) - Exception in DelayedSubscriber (#1030) - Compatibility issue in deprecated TangoAttribute's `isScalar()` and `isSpectrum()` (#1034) +- Some issues in taurus v3 to v4 migration support (#1059) ## [4.6.1] - 2019-08-19 From 5a52e811adc9aeaa80770a6ae3b318a505dc7b12 Mon Sep 17 00:00:00 2001 From: zreszela Date: Mon, 3 Feb 2020 12:58:38 +0100 Subject: [PATCH 134/373] Minor correction --- doc/source/devel/plotting_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/devel/plotting_guide.rst b/doc/source/devel/plotting_guide.rst index e2690c7de..fed898619 100644 --- a/doc/source/devel/plotting_guide.rst +++ b/doc/source/devel/plotting_guide.rst @@ -14,7 +14,7 @@ In taurus, the following dependencies are used for its various plotting widgets: - guiqwt_: for `TaurusImageDialog` and `TaurusTrend2DDialog` implemented in :mod:`taurus.qt.qtgui.extra_guiqwt`. It does not depend at all of PyQwt5, and supports py2, py3 and Qt4, Qt5 so replacing it is not urgent (but still it should be eventually replaced by pyqtgraph-based widgets) -- pyqtgraph_: for `TaurusPlot` and `TaurusTrend` implementions in :mod:`taurus.qt.qtgui.qtgui.tpg`] provided by the +- pyqtgraph_: for `TaurusPlot` and `TaurusTrend` implementions in :mod:`taurus.qt.qtgui.qtgui.tpg` provided by the taurus_pyqtgraph_ plugin. It supports py2, py3 and Qt4, Qt5 and is the intended replacement for all taurus plotting widgets (TEP17_). From 469968bf7a211d18f77172c72873b40f96c4780d Mon Sep 17 00:00:00 2001 From: reszelaz Date: Mon, 3 Feb 2020 14:19:32 +0100 Subject: [PATCH 135/373] Minor corrections --- doc/source/devel/taurus3to4.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/devel/taurus3to4.rst b/doc/source/devel/taurus3to4.rst index 8214e7691..d66bf023a 100644 --- a/doc/source/devel/taurus3to4.rst +++ b/doc/source/devel/taurus3to4.rst @@ -151,11 +151,11 @@ This list is a best-effort to document changes, but it may not be 100% complete +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getSWState | TangoDevice.state | +-----------------------------------------------+---------------------------------------------------------------+ -| TangoDevice.getValueObj | TangoDevice.stateObj.read *tango* or | -| | state *agnostic* | +| TangoDevice.getValueObj | TangoDevice.stateObj.read() *tango* or | +| | .state *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getDisplayValue | TangoDevice.stateObj.read().rvalue.name *tango* or | -| | state.name *agnostic* | +| | .state.name *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getHWObj | TangoDevice.getDeviceProxy | +-----------------------------------------------+---------------------------------------------------------------+ From 08b9dbeec31f8b6f7cba1c1c060b85d44285a3c0 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Mon, 3 Feb 2020 14:41:33 +0100 Subject: [PATCH 136/373] Revert wrong correction --- doc/source/devel/taurus3to4.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/devel/taurus3to4.rst b/doc/source/devel/taurus3to4.rst index d66bf023a..e0ab7b687 100644 --- a/doc/source/devel/taurus3to4.rst +++ b/doc/source/devel/taurus3to4.rst @@ -151,7 +151,7 @@ This list is a best-effort to document changes, but it may not be 100% complete +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getSWState | TangoDevice.state | +-----------------------------------------------+---------------------------------------------------------------+ -| TangoDevice.getValueObj | TangoDevice.stateObj.read() *tango* or | +| TangoDevice.getValueObj | TangoDevice.stateObj.read *tango* or | | | .state *agnostic* | +-----------------------------------------------+---------------------------------------------------------------+ | TangoDevice.getDisplayValue | TangoDevice.stateObj.read().rvalue.name *tango* or | From 483edc035608c34737850f15070ac521d27fb385 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Thu, 6 Feb 2020 16:17:24 +0100 Subject: [PATCH 137/373] Avoid deadlock between push_event and poll Tango attribute has a deadlock when it can not connect to a device (e.g. device is down). Use disablePolling instead of _deactivatePolling to update internal variables. Move disablePolling out of the critical region in case of PyTango.DevFailed. --- lib/taurus/core/tango/tangoattribute.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 8eb6955b8..371c1d698 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -295,6 +295,7 @@ def __init__(self, name='', parent=None, **kwargs): self.__already_warned_unit = None self.call__init__(TaurusAttribute, name, parent, **kwargs) + self.__deactivate_polling = False attr_info = None if parent: @@ -485,6 +486,7 @@ def poll(self, single=True, value=None, time=None, error=None): value = self.decode(value) self.__attr_err = error if self.__attr_err: + raise self.__attr_err # Avoid "valid-but-outdated" notifications # if FILTER_OLD_TANGO_EVENTS is enabled @@ -727,7 +729,7 @@ def _unsubscribeChangeEvents(self): else: self.debug("Failed: %s", df.args[0].desc) self.trace(str(df)) - self._deactivatePolling() + self.disablePolling() self.__subscription_state = SubscriptionState.Unsubscribed def _subscribeConfEvents(self): @@ -803,6 +805,7 @@ def push_event(self, event): specific handlers for different event types. """ with self.__read_lock: + # if it is a configuration event if isinstance(event, PyTango.AttrConfEventData): etype, evalue = self._pushConfEvent(event) @@ -824,6 +827,12 @@ def push_event(self, event): job_kwargs={'listeners': listeners}, serialization_mode=sm) + # Deactivate polling in case of PyTango.DevFailed + # it must be managed out of the critical region to avoid deadlock + if self.__deactivate_polling: + self.__deactivate_polling = False + self.disablePolling() + def _pushAttrEvent(self, event): """Handler of (non-configuration) events from the PyTango layer. It handles the subscription and the (de)activation of polling @@ -856,7 +865,7 @@ def _pushAttrEvent(self, event): self.__subscription_state = SubscriptionState.Subscribed self.__subscription_event.set() if not self.isPollingForced(): - self._deactivatePolling() + self.disablePolling() return TaurusEventType.Change, self.__attr_value elif event.errors[0].reason in EVENT_TO_POLLING_EXCEPTIONS: @@ -872,7 +881,7 @@ def _pushAttrEvent(self, event): *event.errors) self.__subscription_state = SubscriptionState.Subscribed self.__subscription_event.set() - self._deactivatePolling() + self.__deactivate_polling = True return TaurusEventType.Error, self.__attr_err def _pushConfEvent(self, event): From 3ef3f50f33fa03177417f87fe2d18331027ca4a6 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Thu, 6 Feb 2020 16:34:58 +0100 Subject: [PATCH 138/373] Fix cp "static" line --- lib/taurus/core/tango/tangoattribute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 043334b0f..5a3e984fd 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -1078,7 +1078,7 @@ def _decodeAttrInfoEx(self, pytango_attrinfoex=None): match = re.search("[^\.]*\.(?P[0-9]+)[eEfFgG%]", fmt) if match: self.precision = int(match.group(1)) - elif re.match("%[0-9]*d", '%d'): + elif re.match("%[0-9]*d", fmt): self.precision = 0 # self._units and self._display_format is to be used by # TangoAttrValue for performance reasons. Do not rely on it in other From 8bc98a65bb15fbfe9c17d41c95f7a11f4eb0d365 Mon Sep 17 00:00:00 2001 From: aalonso Date: Mon, 10 Feb 2020 12:06:34 +0100 Subject: [PATCH 139/373] Solved issue where taurus was saving Sardana's motor panels as non pickable --- lib/taurus/core/tauruspollingtimer.py | 2 +- lib/taurus/qt/qtgui/panel/taurusvalue.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/taurus/core/tauruspollingtimer.py b/lib/taurus/core/tauruspollingtimer.py index ed1efd7e0..b2629d97d 100644 --- a/lib/taurus/core/tauruspollingtimer.py +++ b/lib/taurus/core/tauruspollingtimer.py @@ -128,7 +128,7 @@ def removeAttribute(self, attribute): if not attr_dict: del self.dev_dict[dev] if not self.dev_dict: - self.stop(sync=True) + self.stop(sync=False) def _pollAttributes(self): """Polls the registered attributes. This method is called by the timer diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index 42d1bf090..b0d2cf980 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -1165,13 +1165,14 @@ def createConfig(self, allowUnpickable=False): or allowUnpickable): #configdict[key] = classID configdict[key] = {'classid': classID} - widget = getattr(self, key[0].lower() + key[1:])() - if isinstance(widget, BaseConfigurableClass): - configdict[key]['delegate'] = widget.createConfig() else: - self.info('createConfig: %s not saved because it is not Pickable (%s)' % ( + self.debug('createConfig: classid of %s not saved because it is not Pickable (%s)' % ( key, str(classID))) - + widget = getattr(self, key[0].lower() + key[1:])() + if isinstance(widget, BaseConfigurableClass): + widget_configdict = configdict.get(key, {}) + widget_configdict['delegate'] = widget.createConfig() + configdict[key] = widget_configdict return configdict def applyConfig(self, configdict, **kwargs): @@ -1187,8 +1188,9 @@ def applyConfig(self, configdict, **kwargs): for key in ('LabelWidget', 'ReadWidget', 'WriteWidget', 'UnitsWidget', 'CustomWidget', 'ExtraWidget'): if key in configdict: widget_configdict = configdict[key] - getattr(self, 'set%sClass' % key)( - widget_configdict.get('classid', None)) + classid = widget_configdict.get('classid', None) + if classid: + getattr(self, 'set%sClass' % key)(classid) if 'delegate' in widget_configdict: widget = getattr(self, key[0].lower() + key[1:])() if isinstance(widget, BaseConfigurableClass): From dcecfcf9a822dd5eaa04ad48a3707aebc0cddcd0 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Mon, 10 Feb 2020 17:40:56 +0100 Subject: [PATCH 140/373] Use _deactivatePolling instead In case of subscription error is necessary to not reset the variables: __enable_polling and __forced_polling for attributes without events. Revert change for this case. --- lib/taurus/core/tango/tangoattribute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 371c1d698..0b81a2f25 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -831,7 +831,7 @@ def push_event(self, event): # it must be managed out of the critical region to avoid deadlock if self.__deactivate_polling: self.__deactivate_polling = False - self.disablePolling() + self._deactivatePolling() def _pushAttrEvent(self, event): """Handler of (non-configuration) events from the PyTango layer. From 2e392ec5d0e1a7a069c75176936fa8b3be5f061e Mon Sep 17 00:00:00 2001 From: aalonso Date: Tue, 11 Feb 2020 10:53:56 +0100 Subject: [PATCH 141/373] revert bad commit --- lib/taurus/core/tauruspollingtimer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/tauruspollingtimer.py b/lib/taurus/core/tauruspollingtimer.py index b2629d97d..ed1efd7e0 100644 --- a/lib/taurus/core/tauruspollingtimer.py +++ b/lib/taurus/core/tauruspollingtimer.py @@ -128,7 +128,7 @@ def removeAttribute(self, attribute): if not attr_dict: del self.dev_dict[dev] if not self.dev_dict: - self.stop(sync=False) + self.stop(sync=True) def _pollAttributes(self): """Polls the registered attributes. This method is called by the timer From 63a65b503ff236ea9d7ae7f97c9bd7ff8673ac07 Mon Sep 17 00:00:00 2001 From: aalonso Date: Tue, 11 Feb 2020 16:52:10 +0100 Subject: [PATCH 142/373] Allow taurus config to store subwidgets that are not from taurus. This requires to create a classid from the class name and restore the class on apply config. --- lib/taurus/qt/qtgui/panel/taurusvalue.py | 30 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index b0d2cf980..f457ee0e3 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -1161,18 +1161,20 @@ def createConfig(self, allowUnpickable=False): for key in ('LabelWidget', 'ReadWidget', 'WriteWidget', 'UnitsWidget', 'CustomWidget', 'ExtraWidget'): # calls self.getLabelWidgetClass, self.getReadWidgetClass,... classID = getattr(self, 'get%sClass' % key)() + if isinstance(classID, type): + # If the class is not from taurus, it doesn't have classid, so we generate it. + classID = "{0.__module__}:{0.__name__}".format(classID) + if (isinstance(classID, string_types) or allowUnpickable): #configdict[key] = classID configdict[key] = {'classid': classID} + widget = getattr(self, key[0].lower() + key[1:])() + if isinstance(widget, BaseConfigurableClass): + configdict[key]['delegate'] = widget.createConfig() else: - self.debug('createConfig: classid of %s not saved because it is not Pickable (%s)' % ( - key, str(classID))) - widget = getattr(self, key[0].lower() + key[1:])() - if isinstance(widget, BaseConfigurableClass): - widget_configdict = configdict.get(key, {}) - widget_configdict['delegate'] = widget.createConfig() - configdict[key] = widget_configdict + self.info('createConfig: %s not saved because it is not Pickable (%s)' % ( + key, str(classID))) return configdict def applyConfig(self, configdict, **kwargs): @@ -1188,9 +1190,17 @@ def applyConfig(self, configdict, **kwargs): for key in ('LabelWidget', 'ReadWidget', 'WriteWidget', 'UnitsWidget', 'CustomWidget', 'ExtraWidget'): if key in configdict: widget_configdict = configdict[key] - classid = widget_configdict.get('classid', None) - if classid: - getattr(self, 'set%sClass' % key)(classid) + if ":" in widget_configdict.get('classid', None): + # Check if classid is not a taurus class but has a classid generated from a type class. + import importlib + classID = widget_configdict.get('classid') + module, name = classID.split(":") + module = importlib.import_module(module) + getattr(self, 'set%sClass' % key)(getattr(module, name)) + else: + getattr(self, 'set%sClass' % key)( + widget_configdict.get('classid', None)) + if 'delegate' in widget_configdict: widget = getattr(self, key[0].lower() + key[1:])() if isinstance(widget, BaseConfigurableClass): From 420e1cca3543f2ea76172a12b1e8549a0112e860 Mon Sep 17 00:00:00 2001 From: aalonso Date: Tue, 11 Feb 2020 17:54:14 +0100 Subject: [PATCH 143/373] Fix pep8 problem and treat None case correctly --- lib/taurus/qt/qtgui/panel/taurusvalue.py | 22 +++++++++++++--------- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 1 + 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index f457ee0e3..305a54725 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -1175,6 +1175,7 @@ def createConfig(self, allowUnpickable=False): else: self.info('createConfig: %s not saved because it is not Pickable (%s)' % ( key, str(classID))) + return configdict def applyConfig(self, configdict, **kwargs): @@ -1190,16 +1191,19 @@ def applyConfig(self, configdict, **kwargs): for key in ('LabelWidget', 'ReadWidget', 'WriteWidget', 'UnitsWidget', 'CustomWidget', 'ExtraWidget'): if key in configdict: widget_configdict = configdict[key] - if ":" in widget_configdict.get('classid', None): - # Check if classid is not a taurus class but has a classid generated from a type class. - import importlib - classID = widget_configdict.get('classid') - module, name = classID.split(":") - module = importlib.import_module(module) - getattr(self, 'set%sClass' % key)(getattr(module, name)) + classID = widget_configdict.get('classid', None) + if classID is not None: + if ":" in classID: + # Check if classid is not a taurus class but has a classid generated from a type class. + import importlib + classID = widget_configdict.get('classid') + module, name = classID.split(":") + module = importlib.import_module(module) + getattr(self, 'set%sClass' % key)(getattr(module, name)) + else: + getattr(self, 'set%sClass' % key)(classID) else: - getattr(self, 'set%sClass' % key)( - widget_configdict.get('classid', None)) + getattr(self, 'set%sClass' % key)(None) if 'delegate' in widget_configdict: widget = getattr(self, key[0].lower() + key[1:])() diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index f46e5c865..496214b7c 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -207,6 +207,7 @@ def createConfig(self, *args, **kwargs): configdict['widgetModuleName'] = self.getWidgetModuleName() if isinstance(self.widget(), BaseConfigurableClass): configdict['widget'] = self.widget().createConfig() + print("CREATING CONFIG DICT ") return configdict def closeEvent(self, event): From 4cf0ef0fd5e1f559f4a945333cb43480e70187d0 Mon Sep 17 00:00:00 2001 From: aalonso Date: Tue, 11 Feb 2020 17:57:05 +0100 Subject: [PATCH 144/373] remove bad commited debug print --- lib/taurus/qt/qtgui/panel/taurusvalue.py | 2 +- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index 305a54725..966a6ba42 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -1174,7 +1174,7 @@ def createConfig(self, allowUnpickable=False): configdict[key]['delegate'] = widget.createConfig() else: self.info('createConfig: %s not saved because it is not Pickable (%s)' % ( - key, str(classID))) + key, str(classID))) return configdict diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 496214b7c..f46e5c865 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -207,7 +207,6 @@ def createConfig(self, *args, **kwargs): configdict['widgetModuleName'] = self.getWidgetModuleName() if isinstance(self.widget(), BaseConfigurableClass): configdict['widget'] = self.widget().createConfig() - print("CREATING CONFIG DICT ") return configdict def closeEvent(self, event): From c1510fad5c34119b672006af537b645332f9e48e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 12 Feb 2020 11:14:36 +0100 Subject: [PATCH 145/373] Simplify implementation of TV.applyConfig --- lib/taurus/qt/qtgui/panel/taurusvalue.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index 966a6ba42..0067fff45 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -1167,7 +1167,6 @@ def createConfig(self, allowUnpickable=False): if (isinstance(classID, string_types) or allowUnpickable): - #configdict[key] = classID configdict[key] = {'classid': classID} widget = getattr(self, key[0].lower() + key[1:])() if isinstance(widget, BaseConfigurableClass): @@ -1192,18 +1191,13 @@ def applyConfig(self, configdict, **kwargs): if key in configdict: widget_configdict = configdict[key] classID = widget_configdict.get('classid', None) - if classID is not None: - if ":" in classID: - # Check if classid is not a taurus class but has a classid generated from a type class. - import importlib - classID = widget_configdict.get('classid') - module, name = classID.split(":") - module = importlib.import_module(module) - getattr(self, 'set%sClass' % key)(getattr(module, name)) - else: - getattr(self, 'set%sClass' % key)(classID) - else: - getattr(self, 'set%sClass' % key)(None) + if classID is not None and ":" in classID: + # classid is of type "module:type" instead of a taurus class name + import importlib + module, name = classID.split(":") + module = importlib.import_module(module) + classID = getattr(module, name) + getattr(self, 'set%sClass' % key)(classID) if 'delegate' in widget_configdict: widget = getattr(self, key[0].lower() + key[1:])() From 1bd545ae9e2a22b832b937e6102f0f5c37159701 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 12 Feb 2020 12:44:12 +0100 Subject: [PATCH 146/373] Avoid using --show-capture opt for pytest in py27-qt5 For some reason, the travis tests for the py27-qt5 target started complaining that pytest does not recognize the --show-capture option. Remove the option (only for py27-qt5) to allow the tests to work. This fixes #1068 --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 619eda053..d8a59b631 100644 --- a/tox.ini +++ b/tox.ini @@ -47,6 +47,10 @@ deps= commands= python -m pytest lib/taurus --show-capture=no +[testenv:py27-qt5] +commands= + python -m pytest lib/taurus + [testenv:py37-qt5-docs] commands= sphinx-build -qW doc/source/ build/sphinx/html From 126427f4ea5db9a256cad334b070d4d1346b9556 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Tue, 18 Feb 2020 18:19:53 +0100 Subject: [PATCH 147/373] Protect removeAttributeFromPolling Check if exist before delete a polling_timer. Avoid possible KeyError --- lib/taurus/core/taurusfactory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/taurusfactory.py b/lib/taurus/core/taurusfactory.py index 44cdd96b1..5318d3675 100644 --- a/lib/taurus/core/taurusfactory.py +++ b/lib/taurus/core/taurusfactory.py @@ -359,7 +359,7 @@ def removeAttributeFromPolling(self, attribute): timers = dict(self.polling_timers) for period, timer in timers.items(): timer.removeAttribute(attribute) - if not timer.dev_dict: + if not timer.dev_dict and period in self.polling_timers: del self.polling_timers[period] def __str__(self): From c2efb6f49a11f028f7c2a1f328a6c91949137a57 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Tue, 18 Feb 2020 18:25:52 +0100 Subject: [PATCH 148/373] Avoid NoneType AttributeError TaurusValueLineEdit._updateValidator method reads the range from the model object and this can be None (not initialized). Use the range of the given taurus value. Fix #1071 --- lib/taurus/qt/qtgui/input/tauruslineedit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/input/tauruslineedit.py b/lib/taurus/qt/qtgui/input/tauruslineedit.py index 4abeaf75c..5054c402d 100644 --- a/lib/taurus/qt/qtgui/input/tauruslineedit.py +++ b/lib/taurus/qt/qtgui/input/tauruslineedit.py @@ -94,8 +94,7 @@ def _updateValidator(self, value): if not isinstance(val, PintValidator): val = PintValidator(self) self.setValidator(val) - attr = self.getModelObj() - bottom, top = attr.range + bottom, top = value.range if bottom != val.bottom: val.setBottom(bottom) if top != val.top: From 78e1620df2630c518996d663bfe96b5037290c2a Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 09:54:10 +0100 Subject: [PATCH 149/373] Protect against exception without using deprecated API The previous commit protects against an exception in the case the attribute is not set, but it uses a deprecated API. Do an equivalent fix without resorting to the deprecated API. --- lib/taurus/qt/qtgui/input/tauruslineedit.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/tauruslineedit.py b/lib/taurus/qt/qtgui/input/tauruslineedit.py index 5054c402d..3279c9293 100644 --- a/lib/taurus/qt/qtgui/input/tauruslineedit.py +++ b/lib/taurus/qt/qtgui/input/tauruslineedit.py @@ -94,7 +94,11 @@ def _updateValidator(self, value): if not isinstance(val, PintValidator): val = PintValidator(self) self.setValidator(val) - bottom, top = value.range + attr = self.getModelObj() + if attr is None: + bottom, top = -numpy.inf, numpy.inf + else: + bottom, top= value.range if bottom != val.bottom: val.setBottom(bottom) if top != val.top: From f90f9457b9f1a481cf6b6864f13cd5f88e7fb878 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 09:54:56 +0100 Subject: [PATCH 150/373] m, PEP8 --- lib/taurus/qt/qtgui/input/tauruslineedit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/tauruslineedit.py b/lib/taurus/qt/qtgui/input/tauruslineedit.py index 3279c9293..fd56427eb 100644 --- a/lib/taurus/qt/qtgui/input/tauruslineedit.py +++ b/lib/taurus/qt/qtgui/input/tauruslineedit.py @@ -98,7 +98,7 @@ def _updateValidator(self, value): if attr is None: bottom, top = -numpy.inf, numpy.inf else: - bottom, top= value.range + bottom, top = value.range if bottom != val.bottom: val.setBottom(bottom) if top != val.top: From b17fa7bc4ee3a5192e9c117a8c885071561c3d31 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 10:07:50 +0100 Subject: [PATCH 151/373] Fix copy-paste error The previous commits introduced a copy-paste error. Fix it and, as a further refinement , avoid setting the range at all if the attribute object is unavailable. --- lib/taurus/qt/qtgui/input/tauruslineedit.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/taurus/qt/qtgui/input/tauruslineedit.py b/lib/taurus/qt/qtgui/input/tauruslineedit.py index fd56427eb..931a30408 100644 --- a/lib/taurus/qt/qtgui/input/tauruslineedit.py +++ b/lib/taurus/qt/qtgui/input/tauruslineedit.py @@ -95,14 +95,12 @@ def _updateValidator(self, value): val = PintValidator(self) self.setValidator(val) attr = self.getModelObj() - if attr is None: - bottom, top = -numpy.inf, numpy.inf - else: - bottom, top = value.range - if bottom != val.bottom: - val.setBottom(bottom) - if top != val.top: - val.setTop(top) + if attr is not None: + bottom, top = attr.range + if bottom != val.bottom: + val.setBottom(bottom) + if top != val.top: + val.setTop(top) units = value.wvalue.units if units != val.units: val.setUnits(units) From b5fe3450c6633fff5181455f148f45839503efeb Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 11:43:29 +0100 Subject: [PATCH 152/373] [Temporary workaround] allow failures in py27qt5 tests Allow failures in py27qt5 tests until #1073 is properly fixed. Revert this when #1073 is closed. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e356dccfc..5d36ff228 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ matrix: - env: TOXENV=py37-qt5-docs allow_failures: - env: TOXENV=flake8 + - env: TOXENV=py27-qt4 before_install: From 5dc6e7e49f078f88c4bc960df548d8a42bac360d Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 13:06:38 +0100 Subject: [PATCH 153/373] Fix previous commit The previous commit allowed failures in the wrong environment (py2qt4) Changing to py2qt5. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5d36ff228..dc10fa332 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ matrix: - env: TOXENV=py37-qt5-docs allow_failures: - env: TOXENV=flake8 - - env: TOXENV=py27-qt4 + - env: TOXENV=py27-qt5 before_install: From d46fdd7cbc8df7089f55b37de455f68a36c3ae9c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 15:01:18 +0100 Subject: [PATCH 154/373] Require pytest>= 3.6.3 to avoid #1073 Fixes #1073 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d8a59b631..83aece833 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,7 @@ conda_deps= !py35: pymca click spyder - pytest + pytest >= 3.6.3 pytest-xvfb flaky docs: sphinx From baa21d14259b5913116401fa8352d44c15e674f5 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 15:09:00 +0100 Subject: [PATCH 155/373] Revert temporary workaround The CI tests for py27qt5 were allowed to fail due to #1073. This is no longer needed --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dc10fa332..3fdd4a859 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,8 +24,6 @@ matrix: - env: TOXENV=py37-qt5-docs allow_failures: - env: TOXENV=flake8 - - env: TOXENV=py27-qt5 - before_install: # install conda From 91347c559a64bbe3758594f0b74141036b7ef170 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 15:14:43 +0100 Subject: [PATCH 156/373] Revert temporary workaround (2) The py27qt5 tests were run without --show-capture=no option to avoid failures. This workaround should not be needed with later versions of pytest. Revert it. --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index 83aece833..bcd8892ee 100644 --- a/tox.ini +++ b/tox.ini @@ -47,10 +47,6 @@ deps= commands= python -m pytest lib/taurus --show-capture=no -[testenv:py27-qt5] -commands= - python -m pytest lib/taurus - [testenv:py37-qt5-docs] commands= sphinx-build -qW doc/source/ build/sphinx/html From 078695e8e341a52a0d34b1b1afbb3cccae3dc0ef Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 19 Feb 2020 15:17:26 +0100 Subject: [PATCH 157/373] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c425490b5..a5ca6cb83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ develop branch) won't be reflected in this file. - (for py2) Improved backwards compatibility of `taurus.qt.qtgui.plot` (#1027) - Exception in DelayedSubscriber (#1030) - Compatibility issue in deprecated TangoAttribute's `isScalar()` and `isSpectrum()` (#1034) +- Some CI test issues (#1075, #1069) ## [4.6.1] - 2019-08-19 From 32bd11ef71f4943ff6a31a7453bacd3e25cdf2d7 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 20 Feb 2020 08:34:52 +0100 Subject: [PATCH 158/373] Expose new formatters as public API Make expFormatter and floatFormatter available from taurus.qt.qtgui.base and refer to the formatters from their "public" location when registering them in the taurus.qt.formatters entry-point --- lib/taurus/qt/qtgui/base/taurusbase.py | 10 ++++++++-- setup.py | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index c3a23be16..161cdd6a7 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -59,8 +59,14 @@ from taurus.core.units import Quantity -__all__ = ["TaurusBaseComponent", "TaurusBaseWidget", - "TaurusBaseWritableWidget", "defaultFormatter"] +__all__ = [ + "TaurusBaseComponent", + "TaurusBaseWidget", + "TaurusBaseWritableWidget", + "defaultFormatter", + "expFormatter", + "floatFormatter", +] __docformat__ = 'restructuredtext' diff --git a/setup.py b/setup.py index 34d777f71..ffaa8a661 100644 --- a/setup.py +++ b/setup.py @@ -119,10 +119,10 @@ def get_release_info(): ] formatters = [ - 'taurus = taurus.qt.qtgui.base.taurusbase:defaultFormatter', - 'tango = taurus.core.tango.util.formatter:tangoFormatter', - '{:2.3e} = taurus.qt.qtgui.base.taurusbase:expFormatter', - '{:.5f} = taurus.qt.qtgui.base.taurusbase:floatFormatter', + 'taurus = taurus.qt.qtgui.base:defaultFormatter', + 'tango = taurus.core.tango.util:tangoFormatter', + '{:2.3e} = taurus.qt.qtgui.base:expFormatter', + '{:.5f} = taurus.qt.qtgui.base:floatFormatter', ] entry_points = { From 0a77e319597edd6bfe8c44962fb4a7edb9bb4d1d Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 20 Feb 2020 15:32:59 +0100 Subject: [PATCH 159/373] (m, doc) Use the public API for tangoFormatter in the settings The Docs for tauruscustomsettings.DEFAULT_FORMATTER suggest using 'taurus.core.tango.util.formatter.tangoFormatter'. Suggest 'taurus.core.tango.util.tangoFormatter' instead --- lib/taurus/tauruscustomsettings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index 8b4728248..7de0580a2 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -88,9 +88,9 @@ #: The formatter can be a python format string or the name of a formatter #: callable, e.g. #: DEFAULT_FORMATTER = '{0}' -#: DEFAULT_FORMATTER = 'taurus.core.tango.util.formatter.tangoFormatter' +#: DEFAULT_FORMATTER = 'taurus.core.tango.util.tangoFormatter' #: If not defined, taurus.qt.qtgui.base.defaultFormatter will be used -pass + #: Default serialization mode **for the tango scheme**. Possible values are: #: 'Serial', 'Concurrent', or 'TangoSerial' (default) From 5e32a69ccadb5956a6a954ee461bab6cc273b164 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 20 Feb 2020 15:38:51 +0100 Subject: [PATCH 160/373] Refactor showFormatterDlg - Avoid caching formatters - Simplify logic - support py3 and py2 - Offer the registered formatters, the DEFAULT_FORMATTER, the class formatter and the current formatter in the combobox --- lib/taurus/qt/qtgui/base/taurusbase.py | 77 ++++++++++++-------------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index 161cdd6a7..1cb15f92f 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -93,7 +93,8 @@ def defaultFormatter(dtype=None, basecomponent=None, **kwargs): expFormatter = '{:2.3e}' floatFormatter = '{:.5f}' - +kkFormatter = '{:.7g}' +def kkFormatter2(*a, **kw):return {} class TaurusBaseComponent(TaurusListener, BaseConfigurableClass): """A generic Taurus component. @@ -1372,7 +1373,6 @@ def __init__(self, name='', parent=None, designMode=False, **kwargs): self.call__init__(TaurusBaseComponent, name, parent=parent, designMode=designMode) self._setText = self._findSetTextMethod() - self._formatters = None def showFormatterDlg(self): """ @@ -1380,46 +1380,41 @@ def showFormatterDlg(self): :return: formatter: python fromat string or formatter callable (in string version) or None """ - if self._formatters is None: - # initialize cache - self._formatters = TaurusBaseWidget.get_registered_formatters() - - # Find index of the current formatter - try: - ind = self._formatters.values().index(self.FORMAT) - except ValueError: - ind = 0 - - uri = "http://taurus-scada.org/devel/api/taurus/qt/qtgui/base/_TaurusBaseComponent.html" - help = " Find more info in doc".format(uri) - - msg = "Choose/Enter a formatter: {}".format(help) - formatter, ok = Qt.QInputDialog.getItem(self, "Set formatter", - msg, - list(self._formatters.keys()), - current=ind, - editable=True) - - try: - # Try to register new formatter - moduleName, formatterName = formatter.rsplit('.', 1) - f = pkg_resources.get_entry_map('taurus')['taurus.qt.formatters'] - ep = pkg_resources.EntryPoint.parse('{} = {}:{}'.format( - formatterName, moduleName, formatterName), - dist=pkg_resources.get_distribution('taurus')) - f[formatterName] = ep - k, v = formatterName, ep.load() - except: - # Python format string - k = v = formatter - - if not self._formatters.has_key(k): - self._formatters[k] = v - - if ok and formatter: - return self._formatters[k] + # add formatters from plugins + known_formatters = TaurusBaseWidget.get_registered_formatters() + # add default formatter + from taurus import tauruscustomsettings + default = getattr(taurus.tauruscustomsettings, + 'DEFAULT_FORMATTER', + defaultFormatter) + known_formatters[''] = default + # add the formatter of this class + cls = self.__class__ + known_formatters['<{}.FORMAT>'.format(cls.__name__)] = cls.FORMAT + # add current formatter of this object + if self.FORMAT not in known_formatters.values(): + known_formatters[str(self.FORMAT)] = self.FORMAT + + names, formatters = list(zip(*sorted(known_formatters.items()))) + + url = ("http://taurus-scada.org/devel/api/taurus/qt/qtgui/base/" + + "_TaurusBaseComponent.html") + + choice, ok = Qt.QInputDialog.getItem( + self, + "Set formatter", + 'Choose/Enter a formatter (help)'.format(url), + names, + current=formatters.index(self.FORMAT), + editable=True + ) + if not ok: + return None - return None + if choice in names: + return known_formatters[choice] + else: + return choice def onSetFormatter(self): """Slot to allow interactive setting of the Formatter. From 3b7ca663b0cf877d6395cadfaeb71a5e1a4196ff Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 20 Feb 2020 16:21:21 +0100 Subject: [PATCH 161/373] Do not sort known formatters The sorting of known_formatters fails on py2. Since it is not critical, just do not do it. --- lib/taurus/qt/qtgui/base/taurusbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index f69457b37..9a39ab821 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -1397,7 +1397,7 @@ def showFormatterDlg(self): if self.FORMAT not in known_formatters.values(): known_formatters[str(self.FORMAT)] = self.FORMAT - names, formatters = list(zip(*sorted(known_formatters.items()))) + names, formatters = list(zip(*known_formatters.items())) url = ("http://taurus-scada.org/devel/api/taurus/qt/qtgui/base/" + "_TaurusBaseComponent.html") From cf9be814884cd3e8718a5d8aae07cca9470abbc7 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 21 Feb 2020 10:19:23 +0100 Subject: [PATCH 162/373] Deprecate showFormatterDlg TauruBaseWidget.showFormatterDlg's API does not allow to distinguish between a cancelled dialog and a selection of None as formatter. Deprecate it and fix the API directly in onSetFormatter() --- CHANGELOG.md | 4 ++++ lib/taurus/qt/qtgui/base/taurusbase.py | 33 +++++++++++++++++--------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5ca6cb83..f3be5381e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ develop branch) won't be reflected in this file. - check-deps subcommand (#988) - `taurus.cli.register_subcommands()` and `taurus.cli.taurus_cmd()` (#991) - Support for spyder v4 in `taurus.qt.qtgui.editor` (#1038) +- Entry-point ("taurus.qt.formatters") for registering formatters via plugins (#1039) ### Removed ### Changed @@ -20,8 +21,11 @@ develop branch) won't be reflected in this file. - (for developers) Support of tox and change to pytest. More platforms being now automatically tested by travis (#994) - TaurusForm provides more debugging info when failing to handle a model (#1049) +- Improved GUI dialog for changing the formatter of a widget (#1039) ### Deprecated +- `TaurusBaseWidget.showFormatterDlg()` (#1039) + ### Fixed - Several issues in TaurusWheelEdit (#1010, #1021) - Several issues affecting synoptics (#1005, #1029) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index 9a39ab821..e9b069a9c 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -830,14 +830,14 @@ def _updateFormat(self, dtype, **kwargs): :param kwargs: keyword arguments that will be passed to :attribute:`FORMAT` if it is a callable """ - if not isinstance(self.FORMAT, string_types): + if self._format is None or isinstance(self.FORMAT, string_types): + self._format = self.FORMAT + else: # unbound method to callable if isinstance(self.FORMAT, MethodType): self.FORMAT = self.FORMAT.__func__ self._format = self.FORMAT(dtype=dtype, basecomponent=self, **kwargs) - else: - self._format = self.FORMAT def setFormat(self, format): """ Method to set the `FORMAT` attribute for this instance. @@ -1376,10 +1376,21 @@ def __init__(self, name='', parent=None, designMode=False, **kwargs): parent=parent, designMode=designMode) self._setText = self._findSetTextMethod() + @deprecation_decorator(rel="4.6.2", alt="onSetFormatter") def showFormatterDlg(self): + """deprecated because it does not distinguish between a user cancelling + the dialog and the user selecting `None` as a formatter.""" + formatter, ok = self.showFormatterDlg() + if not ok: + return None + else: + return formatter + + def __showFormatterDlg(self): """ - showFormatterDlg show a dialog to get the formatter from the user. - :return: formatter: python fromat string or formatter callable + Shows a dialog to get the formatter from the user. + + :return: formatter: python format string or formatter callable (in string version) or None """ # add formatters from plugins @@ -1411,22 +1422,22 @@ def showFormatterDlg(self): editable=True ) if not ok: - return None + return None, False if choice in names: - return known_formatters[choice] + return known_formatters[choice], True else: - return choice + return choice, True def onSetFormatter(self): """Slot to allow interactive setting of the Formatter. - .. seealso:: :meth:`TaurusBaseWidget.showFormatterDlg`, + .. seealso:: :meth:`TaurusBaseWidget.__showFormatterDlg`, :meth:`TaurusBaseComponent.displayValue`, :attr:`tauruscustomsettings.DEFAULT_FORMATTER` """ - format = self.showFormatterDlg() - if format is not None: + format, ok = self.__showFormatterDlg() + if ok: self.debug( 'Default format has been changed to: {0}'.format(format)) self.setFormat(format) From 77c2f643b19f7d760cd278b5e05f7ddb32692cb7 Mon Sep 17 00:00:00 2001 From: zreszela Date: Tue, 25 Feb 2020 17:42:49 +0100 Subject: [PATCH 163/373] Allow to customizer Worker for ThreadPool Tango is not thread safe when using threading.Thread. One must use omni threads instead. This was confirmed for parallel event subscriptions in PyTango#307. For example, in the case of Sardana, the custom Worker will be protected by the EnsureOmniThread context manager intoroduced in PyTango#327. --- lib/taurus/core/util/threadpool.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/taurus/core/util/threadpool.py b/lib/taurus/core/util/threadpool.py index b4e9f806d..4adc43d25 100644 --- a/lib/taurus/core/util/threadpool.py +++ b/lib/taurus/core/util/threadpool.py @@ -50,8 +50,13 @@ class ThreadPool(Logger): NoJob = 6 * (None,) - def __init__(self, name=None, parent=None, Psize=20, Qsize=20, daemons=True): + def __init__(self, name=None, parent=None, Psize=20, Qsize=20, + daemons=True, worker_cls=None): Logger.__init__(self, name, parent) + if worker_cls is None: + self._worker_cls = Worker + else: + self._worker_cls = worker_cls self._daemons = daemons self.localThreadId = 0 self.workers = [] @@ -70,7 +75,7 @@ def set(self, newSize): for i in range(newSize - nb_workers): self.localThreadId += 1 name = "%s.W%03i" % (self.log_name, self.localThreadId) - new = Worker(self, name, self._daemons) + new = self._worker_cls(self, name, self._daemons) self.workers.append(new) self.debug("Starting %s" % name) new.start() From f41aef3b8a7279a7bf98ba1b4e468ea5962e09e9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 26 Feb 2020 13:17:43 +0100 Subject: [PATCH 164/373] Add unit test for bug #1077 --- .../qtgui/graphic/jdraw/test/res/bug1077.jdw | 9 +++++++ .../graphic/jdraw/test/test_jdraw_parser.py | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 lib/taurus/qt/qtgui/graphic/jdraw/test/res/bug1077.jdw create mode 100644 lib/taurus/qt/qtgui/graphic/jdraw/test/test_jdraw_parser.py diff --git a/lib/taurus/qt/qtgui/graphic/jdraw/test/res/bug1077.jdw b/lib/taurus/qt/qtgui/graphic/jdraw/test/res/bug1077.jdw new file mode 100644 index 000000000..edea91d90 --- /dev/null +++ b/lib/taurus/qt/qtgui/graphic/jdraw/test/res/bug1077.jdw @@ -0,0 +1,9 @@ +JDFile v11 { + Global { + } + JDBar { + summit:385,70,422,105 + origin:437,57 + name:"tango:sys/tg_test/1/state" + } +} \ No newline at end of file diff --git a/lib/taurus/qt/qtgui/graphic/jdraw/test/test_jdraw_parser.py b/lib/taurus/qt/qtgui/graphic/jdraw/test/test_jdraw_parser.py new file mode 100644 index 000000000..0abf30b3a --- /dev/null +++ b/lib/taurus/qt/qtgui/graphic/jdraw/test/test_jdraw_parser.py @@ -0,0 +1,24 @@ +import os +from taurus.qt.qtgui.application import TaurusApplication +from taurus.qt.qtgui.graphic.jdraw import ( + TaurusJDrawGraphicsFactory, + jdraw_parser +) +from taurus.qt.qtgui.graphic import TaurusGraphicsScene + + +app = TaurusApplication.instance() +if app is None: + app = TaurusApplication([], cmd_line_parser=None) + + +def test_jdraw_parser(): + """Check that jdraw_parser does not break with JDBar elements""" + fname = os.path.join(os.path.dirname(__file__), "res", "bug1077.jdw") + factory = TaurusJDrawGraphicsFactory(None) + p = jdraw_parser.parse(fname, factory) + assert isinstance(p, TaurusGraphicsScene) + + +if __name__ == '__main__': + test_jdraw_parser() From 82050ec85813f8d38142b05f6d2de3e576855bdc Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 25 Feb 2020 10:15:29 +0100 Subject: [PATCH 165/373] Add dummy JDBar support in TaurusJDrawGraphicsFactory The Taurus JDraw viewer gets an exception if JDBar item is used in the jdw file because TaurusJDrawGraphicsFactory does not support it. Work around this by implementing a dummy support for JDBar. Note that this is not proper support, but just a workaround that displays a filled JDRectangle instead of a bar to allow parsing. --- lib/taurus/qt/qtgui/graphic/jdraw/jdraw.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/taurus/qt/qtgui/graphic/jdraw/jdraw.py b/lib/taurus/qt/qtgui/graphic/jdraw/jdraw.py index d20944abe..821733d0b 100644 --- a/lib/taurus/qt/qtgui/graphic/jdraw/jdraw.py +++ b/lib/taurus/qt/qtgui/graphic/jdraw/jdraw.py @@ -151,6 +151,15 @@ def getRectangleObj(self, params): return item + def getBarObj(self, params): + # TODO: properly implement JDBar support + # As a workaround, use a filled rectangle as a substitute of a JDBar + self.warning('JDBar not yet supported. Using a JDRectangle instead') + if 'fillStyle' not in params: + params['fillStyle'] = 1 + + return self.getRectangleObj(params) + def getRoundRectangleObj(self, params): item = self.getGraphicsItem('RoundRectangle', params) x1, y1, x2, y2 = params.get('summit') From 0d90fa17ad37700937217620bf1b160b631d9eaf Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 26 Feb 2020 16:26:06 +0100 Subject: [PATCH 166/373] Avoid using deprecated TaurusValue.value Use .rvalue instead --- lib/taurus/qt/qtgui/graphic/taurusgraphic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py index 62a91bf8b..3744e90bc 100644 --- a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py +++ b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py @@ -1208,10 +1208,10 @@ def updateStyle(self): bg_brush, fg_brush = None, None if self.getModelObj().getType() == DataType.DevState: bg_brush, fg_brush = QT_DEVICE_STATE_PALETTE.qbrush( - v.value) + v.rvalue) elif self.getModelObj().getType() == DataType.Boolean: bg_brush, fg_brush = QT_DEVICE_STATE_PALETTE.qbrush( - (DevState.FAULT, DevState.ON)[v.value]) + (DevState.FAULT, DevState.ON)[v.rvalue]) elif self.getShowQuality(): bg_brush, fg_brush = QT_ATTRIBUTE_QUALITY_PALETTE.qbrush( v.quality) From 13e36205a2acccc064bde2661488db6880f6409e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 26 Feb 2020 16:47:10 +0100 Subject: [PATCH 167/373] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5ca6cb83..6b75d9812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ develop branch) won't be reflected in this file. - check-deps subcommand (#988) - `taurus.cli.register_subcommands()` and `taurus.cli.taurus_cmd()` (#991) - Support for spyder v4 in `taurus.qt.qtgui.editor` (#1038) +- New `worker_cls` argument for `taurus.core.util.ThreadPool` costructor (#1081) ### Removed ### Changed From accdea28869451d0de3fd81762055a4f6a179652 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 27 Feb 2020 16:47:37 +0100 Subject: [PATCH 168/373] Fix bug introduced in last refactoring By mistake, self_format was used instead of self.FORMAT in the previous refactoring. This breaks the formatter API. Fix it. --- lib/taurus/qt/qtgui/base/taurusbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py index e9b069a9c..6a5463c63 100644 --- a/lib/taurus/qt/qtgui/base/taurusbase.py +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -830,7 +830,7 @@ def _updateFormat(self, dtype, **kwargs): :param kwargs: keyword arguments that will be passed to :attribute:`FORMAT` if it is a callable """ - if self._format is None or isinstance(self.FORMAT, string_types): + if self.FORMAT is None or isinstance(self.FORMAT, string_types): self._format = self.FORMAT else: # unbound method to callable From 5e7b5fe6a3e6ebdacf49b37cce703ed9859813a0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 28 Feb 2020 16:50:55 +0100 Subject: [PATCH 169/373] Revert unwanted change that made custom panels not modifiable Panels created with `createCustomPanel` should be modifiable by the user and contain the model in the config. This was removed in a previous commit of this feature branch, but it is not wanted. Revert this precise change. Note: eventually a better solution would be to allow this to be chosen by the user in the advanced options dialog, but until then we leave it as originally. --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 11bd985ce..b106e8e21 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -836,6 +836,10 @@ def createCustomPanel(self, paneldesc=None): w.setCustomWidgetMap(self.getCustomWidgetMap()) if paneldesc.model is not None: w.setModel(paneldesc.model) + if isinstance(w, TaurusBaseComponent): + # TODO: allow to select these options in the dialog + w.setModifiableByUser(True) + w.setModelInConfig(True) self.createPanel(w, paneldesc.name, floating=paneldesc.floating, custom=True, registerconfig=False, instrumentkey=paneldesc.instrumentkey, From cc9260175c381a5480c319583c48e641c745c2c7 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 28 Feb 2020 17:15:43 +0100 Subject: [PATCH 170/373] Do not show a blocking dialog when in safe mode TaurusGui shows a message box to inform that the "safe mode" is used. This is too obtrusive, specially for automated tests. Log an info message in the console and show a short message in the status bar instead. --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index b106e8e21..d5723c3d3 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -361,12 +361,16 @@ def __init__(self, parent=None, confname=None, configRecursionDepth=None, self.newShortMessage.emit(msg) if self.defaultConfigRecursionDepth >= 0: - Qt.QMessageBox.information(self, "Fail-proof mode", - ('Running in fail-proof mode.' + - '\nLoading of potentially problematic settings is disabled.' + - '\nSome panels may not be loaded or may ignore previous user configuration' + - '\nThis will also apply when loading perspectives'), - Qt.QMessageBox.Ok, Qt.QMessageBox.NoButton) + self.newShortMessage.emit( + "Running in Safe Mode. Settings not loaded" + ) + self.warning( + "Safe mode: \n" + + '\n\tLoading of potentially problematic settings is disabled.' + + '\n\tSome panels may not be loaded or may ignore previous ' + + 'user configuration' + + '\n\tThis will also apply when loading perspectives' + ) def closeEvent(self, event): try: From 6a1ecf6a39755f60d94099d3491fee7244d1d2d7 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 28 Feb 2020 17:17:14 +0100 Subject: [PATCH 171/373] Add unit test for the TaurusPanel creation --- .../qt/qtgui/taurusgui/test/__init__.py | 24 ++++++++++++++++++ .../qt/qtgui/taurusgui/test/test_taurusgui.py | 25 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 lib/taurus/qt/qtgui/taurusgui/test/__init__.py create mode 100644 lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py diff --git a/lib/taurus/qt/qtgui/taurusgui/test/__init__.py b/lib/taurus/qt/qtgui/taurusgui/test/__init__.py new file mode 100644 index 000000000..cedab5731 --- /dev/null +++ b/lib/taurus/qt/qtgui/taurusgui/test/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +########################################################################### diff --git a/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py new file mode 100644 index 000000000..4ed2309a0 --- /dev/null +++ b/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py @@ -0,0 +1,25 @@ +from taurus.qt.qtgui.application import TaurusApplication +from taurus.qt.qtgui.taurusgui import TaurusGui, utils + +app = TaurusApplication.instance() +if app is None: + app = TaurusApplication([], cmd_line_parser=None) + +p1 = utils.PanelDescription( + "testpanel1", + classname="taurus.qt.qtgui.panel.TaurusForm", + model=["eval:1"], + widget_properties={ + "withButtons": False, + "foobar": 34 + } +) + + +def test_paneldescription(): + gui = TaurusGui(confname=__file__, configRecursionDepth=0) + w1 = gui.getPanel('testpanel1').widget() + assert w1.withButtons is False + assert w1.isWithButtons() is False + assert not hasattr(w1, "foobar") + assert w1.modifiableByUser is False \ No newline at end of file From f4c813d04c450bfd291e98a931a54c812cfecbc6 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 28 Feb 2020 17:19:12 +0100 Subject: [PATCH 172/373] m, refactor code for clarity --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 33 ++++++++++--------- lib/taurus/qt/qtgui/taurusgui/utils.py | 38 ++++++++++++---------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index d5723c3d3..3623378a4 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1292,22 +1292,23 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): except AttributeError: pass w = p.getWidget(sdm=Qt.qApp.SDM, setModel=False) - widget_properties = p.widget_properties - if widget_properties: - for key in widget_properties: - if hasattr(w, key): - try: - value = widget_properties[key] - setattr(w, key, value) - except Exception as e: - msg = "Cannot set attribute '%s' of widget '%s' " \ - "to value '%s'" % (key, p.classname, str(value)) - self.error(msg) - self.traceback(level=taurus.Info) - result = Qt.QMessageBox.critical(self, "Initialization error", "%s\n\n%s" % ( - msg, repr(e)), Qt.QMessageBox.Abort | Qt.QMessageBox.Ignore) - if result == Qt.QMessageBox.Abort: - sys.exit() + for key, value in p.widget_properties.items(): + # set additional configuration for the + if hasattr(w, key): + try: + setattr(w, key, value) + except Exception as e: + msg = "Cannot set %r.%s=%s" % (w, key, str(value)) + self.error(msg) + self.traceback(level=taurus.Info) + result = Qt.QMessageBox.critical( + self, + "Initialization error", + "%s\n\n%r" % (msg, e), + Qt.QMessageBox.Abort | Qt.QMessageBox.Ignore + ) + if result == Qt.QMessageBox.Abort: + sys.exit() if hasattr(w, "setCustomWidgetMap"): w.setCustomWidgetMap(self.getCustomWidgetMap()) diff --git a/lib/taurus/qt/qtgui/taurusgui/utils.py b/lib/taurus/qt/qtgui/taurusgui/utils.py index bedf7dc19..0d4f433b5 100644 --- a/lib/taurus/qt/qtgui/taurusgui/utils.py +++ b/lib/taurus/qt/qtgui/taurusgui/utils.py @@ -366,22 +366,24 @@ class PanelDescription(TaurusGuiComponentDescription): def __init__(self, *args, **kwargs): """ + Constructor. The following arguments are processed (the rest are + directly passed to the constructor of + :class:`TaurusGuiComponentDescription` ) - :param args: :param instrumentkey: (str) :param model_in_config: (bool) whther to store model in settigns file or not :param modifiable_by_user: (bool) whether user can edit widget or not :param widget_formatter: (str) formatter used by this widget + :param widget_properties: (dict) a dictionary of property_names:values + to be set on the widget - Additionally, extra configuration options can be passed in constructor as key word arguments. - Proper widget attributes will be set to corresponding values """ self.instrumentkey = kwargs.pop('instrumentkey', None) self.icon = kwargs.pop("icon", None) self.model_in_config = kwargs.pop("model_in_config", None) self.modifiable_by_user = kwargs.pop("modifiable_by_user", None) self.widget_formatter = kwargs.pop("widget_formatter", None) - self.widget_properties = kwargs.pop("widget_properties", None) + self.widget_properties = kwargs.pop("widget_properties", {}) TaurusGuiComponentDescription.__init__(self, *args, **kwargs) @staticmethod @@ -413,19 +415,21 @@ def fromPanel(panel): else: # ignore other "model" attributes (they are not from Taurus) model = None - icon = panel.icon - model_in_config = panel.model_in_config - modifiable_by_user = panel.modifiable_by_user - widget_formatter = panel.widget_formatter - widget_properties = panel.widget_properties - return PanelDescription(name, classname=classname, - modulename=modulename, widgetname=widgetname, - floating=floating, - sharedDataWrite=sharedDataWrite, - sharedDataRead=sharedDataRead, model=model, - icon=icon, model_in_config=model_in_config, - modifiable_by_user=modifiable_by_user, - widget_formatter=widget_formatter, widget_properties=widget_properties) + return PanelDescription( + name, + classname=classname, + modulename=modulename, + widgetname=widgetname, + floating=floating, + sharedDataWrite=sharedDataWrite, + sharedDataRead=sharedDataRead, + model=model, + icon=panel.icon, + model_in_config=panel.model_in_config, + modifiable_by_user=panel.modifiable_by_user, + widget_formatter=panel.widget_formatter, + widget_properties=panel.widget_properties + ) class ToolBarDescription(TaurusGuiComponentDescription): From efc54d7f3b2d1d7beb9639caf7d2994ea54361f2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 28 Feb 2020 17:19:30 +0100 Subject: [PATCH 173/373] m, PEP8 --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 3623378a4..eb57e07b0 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1330,8 +1330,15 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): # so we do not store their config registerconfig = p not in poolinstruments # create a panel - self.createPanel(w, p.name, floating=p.floating, registerconfig=registerconfig, - instrumentkey=instrumentkey, permanent=True, icon=icon) + self.createPanel( + w, + p.name, + floating=p.floating, + registerconfig=registerconfig, + instrumentkey=instrumentkey, + permanent=True, + icon=icon + ) except Exception as e: msg = "Cannot create panel %s" % getattr( p, "name", "__Unknown__") From c2bc8677436612d7f2e4cd93005e364eee1716a4 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Mon, 2 Mar 2020 13:08:34 +0100 Subject: [PATCH 174/373] Add missing import --- lib/taurus/cli/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index a13cf150e..735210324 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -22,6 +22,7 @@ ############################################################################# import click +import logging log_port = click.option( From bd362ec2512ab8dfc35c518cbd92fb9ed0bd74fb Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Mon, 2 Mar 2020 13:14:04 +0100 Subject: [PATCH 175/373] Move options from taurus_cmd to cli.common --- lib/taurus/cli/cli.py | 22 ++++++---------------- lib/taurus/cli/common.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/lib/taurus/cli/cli.py b/lib/taurus/cli/cli.py index 5ac14f882..593512630 100644 --- a/lib/taurus/cli/cli.py +++ b/lib/taurus/cli/cli.py @@ -25,26 +25,16 @@ import click import taurus +from taurus.cli import common as cli_common + @click.group('taurus') -@click.option('--log-level', 'log_level', - type=click.Choice(['Critical', 'Error', 'Warning', 'Info', - 'Debug', 'Trace']), - default='Info', show_default=True, - help='Show only logs with priority LEVEL or above') -@click.option("--polling-period", "polling_period", - type=click.INT, metavar="MILLISEC", default=None, - help='Change the Default Taurus polling period') -@click.option("--serialization-mode", "serialization_mode", - type=click.Choice(['Serial', 'Concurrent', 'TangoSerial']), - default=None, show_default=True, - help=("Set the default Taurus serialization mode for those " - + "models that do not explicitly define it)")) +@cli_common.log_level +@cli_common.poll_period +@cli_common.serial_mode +@cli_common.default_formatter @click.option("--rconsole", "rconsole_port", type=click.INT, metavar="PORT", default=None, help="Enable remote debugging with rfoo on the given PORT") -@click.option("--default-formatter", "default_formatter", - type=click.STRING, metavar="FORMATTER", default=None, - help="Override the default formatter (use with caution!)") @click.version_option(version=taurus.Release.version) def taurus_cmd(log_level, polling_period, serialization_mode, rconsole_port, default_formatter): diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 735210324..b497be627 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -96,3 +96,28 @@ def buffer(default): + "discarded)"), ) return o + +poll_period = click.option( + "--polling-period", "polling_period", + type=click.INT, + metavar="MILLISEC", + default=None, + help='Change the Default Taurus polling period', +) + +serial_mode = click.option( + "--serialization-mode", "serialization_mode", + type=click.Choice(['Serial', 'Concurrent', 'TangoSerial']), + default=None, + show_default=True, + help=("Set the default Taurus serialization mode for those " + + "models that do not explicitly define it)"), +) + +default_formatter = click.option( + "--default-formatter", "default_formatter", + type=click.STRING, + metavar="FORMATTER", + default=None, + help="Override the default formatter (use with caution!)", +) From d10c7f6187c500cd77c879f447edd408211b7ed5 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Mon, 2 Mar 2020 13:14:20 +0100 Subject: [PATCH 176/373] Formatting --- lib/taurus/cli/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/taurus/cli/cli.py b/lib/taurus/cli/cli.py index 593512630..0334bbeaa 100644 --- a/lib/taurus/cli/cli.py +++ b/lib/taurus/cli/cli.py @@ -27,6 +27,7 @@ from taurus.cli import common as cli_common + @click.group('taurus') @cli_common.log_level @cli_common.poll_period From 71113039e9ff830859136ac495fbd93c109bfb53 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Mon, 2 Mar 2020 13:34:55 +0100 Subject: [PATCH 177/373] Add docstring with examples --- lib/taurus/cli/__init__.py | 3 ++ lib/taurus/cli/common.py | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/lib/taurus/cli/__init__.py b/lib/taurus/cli/__init__.py index 2321f0456..d94b9b80c 100644 --- a/lib/taurus/cli/__init__.py +++ b/lib/taurus/cli/__init__.py @@ -34,6 +34,9 @@ .. todo:: add click link , add code snippet for plugins, etc. +This module provides also common options/flags for command-line interfaces +that can be used by taurus plugins. For list of available options, +see :mod:`taurus.cli.common` """ diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index b497be627..d635b10a8 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -21,6 +21,67 @@ ## ############################################################################# +""" +This module provides the taurus Command Line Interface common options. + +It is based on the click module to provide commonly used flags/options. +They are used by taurus commands and can be used by plugin to easily +extend its functionality. + +Example 1: Add custom subcommand +script:: + + import click + + from taurus.cli import common as cli_common + + + @click.command("bar") + @cli_common.poll_period + @cli_common.default_formatter + @cli_common.window_name("Super Bar") + def bar(poll_period, default_formatter, window_name): + ... + + + if __name__ == '__main__': + bar() + +Example 2: Add custom subcommands' group +script:: + + import click + + from taurus.cli import common as cli_common + + + @click.group("foo") + def foo(): + pass + + + @qwt5.command('cmd1') + @cli_common.models + @cli_common.config_file + @cli_common.window_name("Super Foo (cmd1)") + def cmd1(models, config_file, window_name): + ... + + + @qwt5.command('trend') + @cli_common.model + @cli_common.serial_mode + @cli_common.poll_period + @cli_common.default_formatter + def cmd2(model, serial_mode, poll_period, default_formatter): + ... + + + if __name__ == '__main__': + foo() + +""" + import click import logging From d6ddeb13812c1d2cf4df063f5c560eebb03d3a3f Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Mon, 2 Mar 2020 17:58:03 +0100 Subject: [PATCH 178/373] Make sure the tooltip can be generated despite DevFail --- lib/taurus/core/tango/tangodevice.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/taurus/core/tango/tangodevice.py b/lib/taurus/core/tango/tangodevice.py index 91731b745..426087ed5 100644 --- a/lib/taurus/core/tango/tangodevice.py +++ b/lib/taurus/core/tango/tangodevice.py @@ -185,8 +185,11 @@ def getDisplayDescrObj(self, cache=True): ret = [] for name, value in desc_obj: if name.lower() == u'device state' and self.stateObj is not None: - tg_state = self.stateObj.read(cache).rvalue.name - value = u"%s (%s)" % (value, tg_state) + try: + tg_state = self.stateObj.read(cache).rvalue.name + value = u"%s (%s)" % (value, tg_state) + except Exception as e: + value = u"cannot read state" ret.append((name, value)) return ret From 68dbb38642a18ec10e171b58948e8312828ea57b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 4 Mar 2020 15:47:41 +0100 Subject: [PATCH 179/373] Issue an error when cmd_line_parser arg is of an unsupported type This fixes #1065 --- .../qt/qtgui/application/taurusapplication.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/taurus/qt/qtgui/application/taurusapplication.py b/lib/taurus/qt/qtgui/application/taurusapplication.py index 40eea1055..a0121290f 100644 --- a/lib/taurus/qt/qtgui/application/taurusapplication.py +++ b/lib/taurus/qt/qtgui/application/taurusapplication.py @@ -211,19 +211,25 @@ def __init__(self, *args, **kwargs): if parser is None: p, opt, args = None, None, None else: - if parser.version is None and app_version: - # if the parser does not contain version information - # but we have an application version, add the - # --version capability - v = app_version - if app_name: - v = app_name + " " + app_version - parser.version = v - parser._add_version_option() - - import taurus.core.util.argparse - p, opt, args = taurus.core.util.argparse.init_taurus_args( - parser=parser, args=args[0][1:]) + try: + if parser.version is None and app_version: + # if the parser does not contain version information + # but we have an application version, add the + # --version capability + v = app_version + if app_name: + v = app_name + " " + app_version + parser.version = v + parser._add_version_option() + + import taurus.core.util.argparse + p, opt, args = taurus.core.util.argparse.init_taurus_args( + parser=parser, args=args[0][1:]) + except Exception: + self.error("Error while using the given cmd_line_parser. \n" + + "hint: it must be either None or an " + + "optparse.OptionParser instance") + raise self._cmd_line_parser = p self._cmd_line_options = opt From cf0d92efe7f5c23216c3610a57896ad27e134bdc Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 4 Mar 2020 15:52:25 +0100 Subject: [PATCH 180/373] Implicit optparse legacy support: TaurusApplication implicitly uses a `optparse.OptionParser` instance if it does not receive an explicit `cmd_line_parser` keyword argument. This is inconvenient because it forces the user to explicitly pass `cmd_line_parser=None` when using other mechanisms such as `click` or `argparse` to parse CLI options. Change this so that, by default cmd_line_parser=None is assumed if not explicitly passed, but allow the user to revert to the old behaviour by setting IMPLICIT_OPTPARSE=True in tauruscustomsettings. --- .../qt/qtgui/application/taurusapplication.py | 17 ++++++++++++----- lib/taurus/tauruscustomsettings.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/taurus/qt/qtgui/application/taurusapplication.py b/lib/taurus/qt/qtgui/application/taurusapplication.py index a0121290f..f7daf58db 100644 --- a/lib/taurus/qt/qtgui/application/taurusapplication.py +++ b/lib/taurus/qt/qtgui/application/taurusapplication.py @@ -31,12 +31,12 @@ import os import sys import logging -import optparse import threading from taurus.external.qt import Qt from taurus.core.util.log import LogExceptHook, Logger +from taurus import tauruscustomsettings __all__ = ["TaurusApplication"] @@ -133,11 +133,12 @@ class TaurusApplication(Qt.QApplication, Logger): - org_domain: (str) organization domain - cmd_line_parser (None [or DEPRECATED :class:`optparse.OptionParser`]) - If cmd_line_parser is explicitly set to None (recommended), no parsing will + If cmd_line_parser is explicitly set to None, no parsing will be done at all. If a :class:`optparse.OptionParser` instance is passed as cmd_line_parser (deprecated), it will be used for parsing the command line - arguments. If it is not explicitly passed (not recommended), a default - parser will be assumed with the default taurus options. + arguments. If cmd_line_parser is not explicitly passed, the behaviour will + depend on the value of tauruscustomsettings.IMPLICIT_OPTPARSE (which by + default is False, indicating that no parsing is done). Simple example:: @@ -173,7 +174,13 @@ def __init__(self, *args, **kwargs): app_version = kwargs.pop('app_version', None) org_name = kwargs.pop('org_name', None) org_domain = kwargs.pop('org_domain', None) - parser = kwargs.pop('cmd_line_parser', optparse.OptionParser()) + + if getattr(tauruscustomsettings, 'IMPLICIT_OPTPARSE', True): + import optparse + default_parser = optparse.OptionParser() + else: + default_parser = None + parser = kwargs.pop('cmd_line_parser', default_parser) ####################################################################### # Workaround for XInitThreads-related crash. diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index 7de0580a2..5f0ca8e55 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -149,6 +149,16 @@ #: (note that "logos:" is a Qt a registered path for "/qt/qtgui/icon/logos/") ORGANIZATION_LOGO = "logos:taurus.png" +#: Implicit optparse legacy support: +#: In taurus < 4.6.5 if TaurusApplication did not receive an explicit +#: `cmd_line_parser` keyword argument, it implicitly used a +#: `optparse.OptionParser` instance. This was inconvenient because it forced +#: the user to explicitly pass `cmd_line_parser=None` when using other +#: mechanisms such as `click` or `argparse` to parse CLI options. +#: In taurus >=4.6.5 this is no longer the case by default, but the old +#: behaviour can be restored by setting IMPLICIT_OPTPARSE=True +IMPLICIT_OPTPARSE = False + # ---------------------------------------------------------------------------- # Deprecation handling: # Note: this API is still experimental and may be subject to change From 79f978a8ffb58b22c41b5a4b8a4ce2aad8702919 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 08:15:30 +0100 Subject: [PATCH 181/373] Fix typos in documentation --- lib/taurus/cli/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index d635b10a8..974d6f0f1 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -60,7 +60,7 @@ def foo(): pass - @qwt5.command('cmd1') + @foo.command('cmd1') @cli_common.models @cli_common.config_file @cli_common.window_name("Super Foo (cmd1)") @@ -68,7 +68,7 @@ def cmd1(models, config_file, window_name): ... - @qwt5.command('trend') + @foo.command('trend') @cli_common.model @cli_common.serial_mode @cli_common.poll_period From 836bfd70318df31aba8bc38a091566813f1302c6 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 08:24:58 +0100 Subject: [PATCH 182/373] Use predominant import style --- lib/taurus/cli/cli.py | 10 ++++---- lib/taurus/cli/common.py | 24 +++++++++---------- lib/taurus/core/util/remotelogmonitor.py | 8 +++---- lib/taurus/qt/qtgui/extra_guiqwt/plot.py | 8 +++---- .../qt/qtgui/extra_guiqwt/taurustrend2d.py | 10 ++++---- .../qt/qtgui/panel/taurusdevicepanel.py | 4 ++-- lib/taurus/qt/qtgui/panel/taurusform.py | 8 +++---- lib/taurus/qt/qtgui/qwt5/cli.py | 24 +++++++++---------- lib/taurus/qt/qtgui/table/qlogtable.py | 8 +++---- 9 files changed, 52 insertions(+), 52 deletions(-) diff --git a/lib/taurus/cli/cli.py b/lib/taurus/cli/cli.py index 0334bbeaa..bb396fbbe 100644 --- a/lib/taurus/cli/cli.py +++ b/lib/taurus/cli/cli.py @@ -25,14 +25,14 @@ import click import taurus -from taurus.cli import common as cli_common +import taurus.cli.common @click.group('taurus') -@cli_common.log_level -@cli_common.poll_period -@cli_common.serial_mode -@cli_common.default_formatter +@taurus.cli.common.log_level +@taurus.cli.common.poll_period +@taurus.cli.common.serial_mode +@taurus.cli.common.default_formatter @click.option("--rconsole", "rconsole_port", type=click.INT, metavar="PORT", default=None, help="Enable remote debugging with rfoo on the given PORT") diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 974d6f0f1..f7778a146 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -33,13 +33,13 @@ import click - from taurus.cli import common as cli_common + import taurus.cli.common @click.command("bar") - @cli_common.poll_period - @cli_common.default_formatter - @cli_common.window_name("Super Bar") + @aurus.cli.common.poll_period + @aurus.cli.common.default_formatter + @aurus.cli.common.window_name("Super Bar") def bar(poll_period, default_formatter, window_name): ... @@ -52,7 +52,7 @@ def bar(poll_period, default_formatter, window_name): import click - from taurus.cli import common as cli_common + import taurus.cli.common @click.group("foo") @@ -61,18 +61,18 @@ def foo(): @foo.command('cmd1') - @cli_common.models - @cli_common.config_file - @cli_common.window_name("Super Foo (cmd1)") + @aurus.cli.common.models + @aurus.cli.common.config_file + @aurus.cli.common.window_name("Super Foo (cmd1)") def cmd1(models, config_file, window_name): ... @foo.command('trend') - @cli_common.model - @cli_common.serial_mode - @cli_common.poll_period - @cli_common.default_formatter + @aurus.cli.common.model + @aurus.cli.common.serial_mode + @aurus.cli.common.poll_period + @aurus.cli.common.default_formatter def cmd2(model, serial_mode, poll_period, default_formatter): ... diff --git a/lib/taurus/core/util/remotelogmonitor.py b/lib/taurus/core/util/remotelogmonitor.py index 2264ca772..c249dcccd 100644 --- a/lib/taurus/core/util/remotelogmonitor.py +++ b/lib/taurus/core/util/remotelogmonitor.py @@ -42,7 +42,7 @@ import socketserver -from taurus.cli import common as cli_common +import taurus.cli.common _all__ = ["LogRecordStreamHandler", "LogRecordSocketReceiver", "log"] @@ -185,9 +185,9 @@ def log(host, port, name=None, level=None): @click.command('logmon') -@cli_common.log_port -@cli_common.log_name -@cli_common.log_level +@taurus.cli.common.log_port +@taurus.cli.common.log_name +@taurus.cli.common.log_level def logmon_cmd(port, log_name, log_level): """Show the console-based Taurus Remote Log Monitor""" import taurus diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py index 465a900fe..707697ff5 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py @@ -43,7 +43,7 @@ from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_ATTR_MIME_TYPE from taurus.qt.qtgui.extra_guiqwt.builder import make from taurus.qt.qtgui.extra_guiqwt.curve import TaurusCurveItem, TaurusTrendParam, TaurusTrendItem -from taurus.cli import common as cli_common +import taurus.cli.common __all__ = ["TaurusCurveDialog", "TaurusTrendDialog", "TaurusImageDialog"] @@ -636,9 +636,9 @@ def taurusTrendDlgMain(): show_default=True, help=('Color mode expected from the attribute') ) -@cli_common.model -@cli_common.demo -@cli_common.window_name('TaurusPlot (qwt5)') +@taurus.cli.common.model +@taurus.cli.common.demo +@taurus.cli.common.window_name('TaurusPlot (qwt5)') def image_cmd(model, color_mode, demo, window_name): from taurus.qt.qtgui.application import TaurusApplication import sys diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 3f55b163d..af6c93154 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -40,7 +40,7 @@ TimeAxisTool, AutoScrollTool, AutoScaleXTool, AutoScaleYTool, AutoScaleZTool) -from taurus.cli import common as cli_common +import taurus.cli.common class TaurusTrend2DDialog(ImageDialog, TaurusBaseWidget): @@ -317,10 +317,10 @@ def setModifiableByUser(self, modifiable): help=("interpret X values as timestamps (t), " + "time deltas (d) or event numbers (e). ") ) -@cli_common.buffer(512) -@cli_common.model -@cli_common.demo -@cli_common.window_name('TaurusPlot (qwt5)') +@taurus.cli.common.buffer(512) +@taurus.cli.common.model +@taurus.cli.common.demo +@taurus.cli.common.window_name('TaurusPlot (qwt5)') def trend2d_cmd(model, x_axis_mode, max_buffer_size, demo, window_name): from taurus.qt.qtgui.application import TaurusApplication diff --git a/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py b/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py index d051d8e9a..bbfa9a2fe 100644 --- a/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py +++ b/lib/taurus/qt/qtgui/panel/taurusdevicepanel.py @@ -49,7 +49,7 @@ from taurus.qt.qtgui.panel.taurusform import TaurusCommandsForm from taurus.qt.qtgui.util.ui import UILoadable from taurus.qt.qtgui.icon import getCachedPixmap -from taurus.cli import common as cli_common +import taurus.cli.common __all__ = ["TaurusDevicePanel", "TaurusDevPanel"] @@ -612,7 +612,7 @@ def getQtDesignerPluginInfo(cls): + '(it can be passed more than once)'), multiple=True, ) -@cli_common.config_file +@taurus.cli.common.config_file def device_cmd(dev, filter, config_file): """Show a Device Panel""" import sys diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index c93c709c1..6b060d960 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -44,7 +44,7 @@ from taurus.qt.qtgui.container import TaurusWidget, TaurusScrollArea from taurus.qt.qtgui.button import QButtonBox, TaurusCommandButton from .taurusmodelchooser import TaurusModelChooser -from taurus.cli import common as cli_common +import taurus.cli.common __all__ = ["TaurusAttrForm", "TaurusCommandsForm", "TaurusForm"] @@ -1050,9 +1050,9 @@ def setModel(self, model): @click.command('form') -@cli_common.window_name("TaurusForm") -@cli_common.config_file -@cli_common.models +@taurus.cli.common.window_name("TaurusForm") +@taurus.cli.common.config_file +@taurus.cli.common.models def form_cmd(window_name, config_file, models): """Shows a Taurus form populated with the given model names""" from taurus.qt.qtgui.application import TaurusApplication diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index d8cd55444..3376bfb25 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -23,7 +23,7 @@ import click from .taurustrend import TaurusTrend -from taurus.cli import common as cli_common +import taurus.cli.common @click.group('qwt5') @@ -33,11 +33,11 @@ def qwt5(): @qwt5.command('plot') -@cli_common.models -@cli_common.config_file -@cli_common.x_axis_mode -@cli_common.demo -@cli_common.window_name("TaurusPlot (qwt5)") +@taurus.cli.common.models +@taurus.cli.common.config_file +@taurus.cli.common.x_axis_mode +@taurus.cli.common.demo +@taurus.cli.common.window_name("TaurusPlot (qwt5)") def plot_cmd(models, config_file, x_axis_mode, demo, window_name): """Shows a plot for the given models""" from .taurusplot import plot_main @@ -50,12 +50,12 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): @qwt5.command('trend') -@cli_common.models -@cli_common.x_axis_mode -@cli_common.config_file -@cli_common.demo -@cli_common.window_name('TaurusPlot (qwt5)') -@cli_common.buffer(TaurusTrend.DEFAULT_MAX_BUFFER_SIZE) +@taurus.cli.common.models +@taurus.cli.common.x_axis_mode +@taurus.cli.common.config_file +@taurus.cli.common.demo +@taurus.cli.common.window_name('TaurusPlot (qwt5)') +@taurus.cli.common.buffer(TaurusTrend.DEFAULT_MAX_BUFFER_SIZE) @click.option('-a', '--use-archiving', 'use_archiving', is_flag=True, default=False, diff --git a/lib/taurus/qt/qtgui/table/qlogtable.py b/lib/taurus/qt/qtgui/table/qlogtable.py index eb1cc8175..b78664240 100644 --- a/lib/taurus/qt/qtgui/table/qlogtable.py +++ b/lib/taurus/qt/qtgui/table/qlogtable.py @@ -47,7 +47,7 @@ from taurus.external.qt import Qt from taurus.qt.qtgui.model import FilterToolBar from taurus.qt.qtgui.util import ActionFactory -from taurus.cli import common as cli_common +import taurus.cli.common from .qtable import QBaseTableWidget @@ -601,9 +601,9 @@ def main(): @click.command('qlogmon') -@cli_common.log_port -@cli_common.log_name -@cli_common.log_level +@taurus.cli.common.log_port +@taurus.cli.common.log_name +@taurus.cli.common.log_level def qlogmon_cmd(port, log_name, log_level): """Show the Taurus Remote Log Monitor""" import taurus From 85bb39f2b30453517eb6070a039df9675acb8d37 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 08:32:19 +0100 Subject: [PATCH 183/373] Revert making log_port and log_name common CLI flags --- lib/taurus/cli/common.py | 14 -------------- lib/taurus/core/util/remotelogmonitor.py | 13 +++++++++++-- lib/taurus/qt/qtgui/table/qlogtable.py | 13 +++++++++++-- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index f7778a146..b850d4529 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -83,22 +83,8 @@ def cmd2(model, serial_mode, poll_period, default_formatter): """ import click -import logging -log_port = click.option( - '--port', 'port', type=int, - default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, - show_default=True, - help='port where log server is running', -) - -log_name = click.option( - '--log-name', 'log_name', - default=None, - help='filter specific log object', -) - log_level = click.option( '--log-level', 'log_level', type=click.Choice(['critical', 'error', 'warning', 'info', diff --git a/lib/taurus/core/util/remotelogmonitor.py b/lib/taurus/core/util/remotelogmonitor.py index c249dcccd..818964b97 100644 --- a/lib/taurus/core/util/remotelogmonitor.py +++ b/lib/taurus/core/util/remotelogmonitor.py @@ -185,8 +185,17 @@ def log(host, port, name=None, level=None): @click.command('logmon') -@taurus.cli.common.log_port -@taurus.cli.common.log_name +@click.option( + '--port', 'port', type=int, + default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, + show_default=True, + help='port where log server is running', +) +@click.option( + '--log-name', 'log_name', + default=None, + help='filter specific log object', +) @taurus.cli.common.log_level def logmon_cmd(port, log_name, log_level): """Show the console-based Taurus Remote Log Monitor""" diff --git a/lib/taurus/qt/qtgui/table/qlogtable.py b/lib/taurus/qt/qtgui/table/qlogtable.py index b78664240..d3138c3bf 100644 --- a/lib/taurus/qt/qtgui/table/qlogtable.py +++ b/lib/taurus/qt/qtgui/table/qlogtable.py @@ -601,8 +601,17 @@ def main(): @click.command('qlogmon') -@taurus.cli.common.log_port -@taurus.cli.common.log_name +@click.option( + '--port', 'port', type=int, + default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, + show_default=True, + help='port where log server is running', +) +@click.option( + '--log-name', 'log_name', + default=None, + help='filter specific log object', +) @taurus.cli.common.log_level def qlogmon_cmd(port, log_name, log_level): """Show the Taurus Remote Log Monitor""" From 6027b2c52409e71540cd9fbe6270143ba905fb45 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 08:35:18 +0100 Subject: [PATCH 184/373] Revert making x_axis_mode common CLI flag --- lib/taurus/cli/common.py | 9 --------- lib/taurus/qt/qtgui/qwt5/cli.py | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index b850d4529..080108284 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -111,15 +111,6 @@ def window_name(default): 'models', nargs=-1, ) -x_axis_mode = click.option( - "-x", "--x-axis-mode", "x_axis_mode", - type=click.Choice(['t', 'n']), - default='n', - show_default=True, - help=('X axis mode. "t" implies using a Date axis' - + '"n" uses the regular axis'), -) - demo = click.option( "--demo", is_flag=True, diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index 3376bfb25..ccb737781 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -26,6 +26,16 @@ import taurus.cli.common +x_axis_mode = click.option( + "-x", "--x-axis-mode", "x_axis_mode", + type=click.Choice(['t', 'n']), + default='n', + show_default=True, + help=('X axis mode. "t" implies using a Date axis' + + '"n" uses the regular axis'), +) + + @click.group('qwt5') def qwt5(): """Qwt5 related commands""" @@ -35,7 +45,7 @@ def qwt5(): @qwt5.command('plot') @taurus.cli.common.models @taurus.cli.common.config_file -@taurus.cli.common.x_axis_mode +@x_axis_mode @taurus.cli.common.demo @taurus.cli.common.window_name("TaurusPlot (qwt5)") def plot_cmd(models, config_file, x_axis_mode, demo, window_name): @@ -51,7 +61,7 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): @qwt5.command('trend') @taurus.cli.common.models -@taurus.cli.common.x_axis_mode +@x_axis_mode @taurus.cli.common.config_file @taurus.cli.common.demo @taurus.cli.common.window_name('TaurusPlot (qwt5)') From 5504426645c0f41a18a9ddb2a3e6a92ee9fe4743 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 08:38:33 +0100 Subject: [PATCH 185/373] Revert buffer common CLI flag --- lib/taurus/cli/common.py | 12 ------------ lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py | 3 ++- lib/taurus/qt/qtgui/qwt5/cli.py | 14 +++++++++++++- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 080108284..af7b6a6d3 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -123,18 +123,6 @@ def window_name(default): required=False, ) -def buffer(default): - o = click.option( - '-b', '--buffer', 'max_buffer_size', - type=int, - default=default, - show_default=True, - help=("maximum number of values to be stacked " - + "(when reached, the oldest values will be " - + "discarded)"), - ) - return o - poll_period = click.option( "--polling-period", "polling_period", type=click.INT, diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index af6c93154..71ea79b0f 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -41,6 +41,7 @@ AutoScaleXTool, AutoScaleYTool, AutoScaleZTool) import taurus.cli.common +import taurus.qt.qtgui.qwt5.cli class TaurusTrend2DDialog(ImageDialog, TaurusBaseWidget): @@ -317,7 +318,7 @@ def setModifiableByUser(self, modifiable): help=("interpret X values as timestamps (t), " + "time deltas (d) or event numbers (e). ") ) -@taurus.cli.common.buffer(512) +@taurus.qt.qtgui.qwt5.cli.buffer(512) @taurus.cli.common.model @taurus.cli.common.demo @taurus.cli.common.window_name('TaurusPlot (qwt5)') diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index ccb737781..30f87d956 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -35,6 +35,18 @@ + '"n" uses the regular axis'), ) +def buffer(default): + o = click.option( + '-b', '--buffer', 'max_buffer_size', + type=int, + default=default, + show_default=True, + help=("maximum number of values to be stacked " + + "(when reached, the oldest values will be " + + "discarded)"), + ) + return o + @click.group('qwt5') def qwt5(): @@ -65,7 +77,7 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): @taurus.cli.common.config_file @taurus.cli.common.demo @taurus.cli.common.window_name('TaurusPlot (qwt5)') -@taurus.cli.common.buffer(TaurusTrend.DEFAULT_MAX_BUFFER_SIZE) +@buffer(TaurusTrend.DEFAULT_MAX_BUFFER_SIZE) @click.option('-a', '--use-archiving', 'use_archiving', is_flag=True, default=False, From 975dc7f1bf28f3a5c6fd0da362bcd762741c7208 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 08:48:13 +0100 Subject: [PATCH 186/373] Fix formatting and order of arguments --- lib/taurus/cli/cli.py | 4 ++-- lib/taurus/qt/qtgui/extra_guiqwt/plot.py | 15 ++++++------- .../qt/qtgui/extra_guiqwt/taurustrend2d.py | 21 ++++++++++--------- lib/taurus/qt/qtgui/qwt5/cli.py | 4 ++-- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/taurus/cli/cli.py b/lib/taurus/cli/cli.py index bb396fbbe..6f445eb7c 100644 --- a/lib/taurus/cli/cli.py +++ b/lib/taurus/cli/cli.py @@ -37,8 +37,8 @@ metavar="PORT", default=None, help="Enable remote debugging with rfoo on the given PORT") @click.version_option(version=taurus.Release.version) -def taurus_cmd(log_level, polling_period, serialization_mode, rconsole_port, - default_formatter): +def taurus_cmd(log_level, polling_period, serialization_mode, default_formatter, + rconsole_port): """The main taurus command""" # set log level diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py index 707697ff5..739a11a58 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py @@ -630,16 +630,17 @@ def taurusTrendDlgMain(): @click.command('image') -@click.option('-c', '--color-mode', 'color_mode', - type=click.Choice(['gray', 'rgb']), - default='gray', - show_default=True, - help=('Color mode expected from the attribute') - ) @taurus.cli.common.model @taurus.cli.common.demo @taurus.cli.common.window_name('TaurusPlot (qwt5)') -def image_cmd(model, color_mode, demo, window_name): +@click.option( + '-c', '--color-mode', 'color_mode', + type=click.Choice(['gray', 'rgb']), + default='gray', + show_default=True, + help=('Color mode expected from the attribute'), +) +def image_cmd(model, demo, window_name, color_mode): from taurus.qt.qtgui.application import TaurusApplication import sys diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 71ea79b0f..b94a9bd76 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -311,19 +311,20 @@ def setModifiableByUser(self, modifiable): @click.command('trend2d') -@click.option("-x", "--x-axis-mode", "x_axis_mode", - type=click.Choice(['t', 'd', 'e']), - default='d', - show_default=True, - help=("interpret X values as timestamps (t), " - + "time deltas (d) or event numbers (e). ") - ) -@taurus.qt.qtgui.qwt5.cli.buffer(512) @taurus.cli.common.model @taurus.cli.common.demo @taurus.cli.common.window_name('TaurusPlot (qwt5)') -def trend2d_cmd(model, x_axis_mode, max_buffer_size, - demo, window_name): +@taurus.qt.qtgui.qwt5.cli.buffer(512) +@click.option( + "-x", "--x-axis-mode", "x_axis_mode", + type=click.Choice(['t', 'd', 'e']), + default='d', + show_default=True, + help=("interpret X values as timestamps (t), time deltas (d) " + + " or event numbers (e). ") +) +def trend2d_cmd(model, demo, window_name, max_buffer_size, + x_axis_mode): from taurus.qt.qtgui.application import TaurusApplication import sys diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index 30f87d956..3ebf434e7 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -86,8 +86,8 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): default=-1, metavar="MILLISECONDS", help="force re-reading of the attributes every MILLISECONDS ms") -def trend_cmd(models, x_axis_mode, use_archiving, max_buffer_size, - forced_read_period, config_file, demo, window_name): +def trend_cmd(models, x_axis_mode, config_file, demo, window_name, + max_buffer_size, use_archiving, forced_read_period): """Shows a trend for the given models""" from .taurustrend import trend_main return trend_main(models=models, From 470519bbfca0a05dc72c0b74c9c4b0a1b5547027 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 09:11:44 +0100 Subject: [PATCH 187/373] Add LazyModule for lazy import of plugins --- lib/taurus/qt/qtgui/__init__.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/taurus/qt/qtgui/__init__.py b/lib/taurus/qt/qtgui/__init__.py index d4fe4fe3c..f8f08fcda 100644 --- a/lib/taurus/qt/qtgui/__init__.py +++ b/lib/taurus/qt/qtgui/__init__.py @@ -35,6 +35,7 @@ import sys import glob import pkg_resources +from types import ModuleType from taurus import tauruscustomsettings as __S from taurus import info as __info from taurus import warning as __warning @@ -54,16 +55,32 @@ # Note: this is an experimental feature introduced in v 4.3.0a # It may be removed or changed in future releases +class LazyModule(ModuleType): + + def __init__(self, name, package, entry_point): + super(LazyModule, self).__init__(name) + self.__package__ = package + self.ep = entry_point + + def __getattr__(self, member): + mod = self.ep.load() + # Replace lazy module with actual module for package + setattr(sys.modules[self.__package__], self.__name__, mod) + # Replace lazy module with actual module in sys.modules + modname = "%s.%s" % (self.__package__, self.__name__) + sys.modules[modname] = mod + return getattr(mod, memeber) + # Discover the taurus.qt.qtgui plugins __mod = __modname = None for __p in pkg_resources.iter_entry_points('taurus.qt.qtgui'): try: __modname = '%s.%s' % (__name__, __p.name) - __mod = __p.load() + __lazy_mod = LazyModule(__p.name, __name__, __p) # Add it to the current module - setattr(sys.modules[__name__], __p.name, __mod) + setattr(sys.modules[__name__], __p.name, __lazy_mod) # Add it to sys.modules - sys.modules[__modname] = __mod + sys.modules[__modname] = __lazy_mod __info('Plugin "%s" loaded as "%s"', __p.module_name, __modname) except Exception as e: __warning('Could not load plugin "%s". Reason: %s', __p.module_name, e) From e886e3200d1d94930798a356d82b61614f395481 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 09:12:04 +0100 Subject: [PATCH 188/373] Fix typo --- lib/taurus/qt/qtgui/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/__init__.py b/lib/taurus/qt/qtgui/__init__.py index f8f08fcda..d65fcebf9 100644 --- a/lib/taurus/qt/qtgui/__init__.py +++ b/lib/taurus/qt/qtgui/__init__.py @@ -69,7 +69,7 @@ def __getattr__(self, member): # Replace lazy module with actual module in sys.modules modname = "%s.%s" % (self.__package__, self.__name__) sys.modules[modname] = mod - return getattr(mod, memeber) + return getattr(mod, member) # Discover the taurus.qt.qtgui plugins __mod = __modname = None From 47e8c8eea8cc09f2709d24decb8ecb9a1d07b8aa Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 09:19:14 +0100 Subject: [PATCH 189/373] Try to fix improt issues --- lib/taurus/cli/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/taurus/cli/cli.py b/lib/taurus/cli/cli.py index 6f445eb7c..c4172bed4 100644 --- a/lib/taurus/cli/cli.py +++ b/lib/taurus/cli/cli.py @@ -26,13 +26,14 @@ import taurus import taurus.cli.common +from .import common @click.group('taurus') -@taurus.cli.common.log_level -@taurus.cli.common.poll_period -@taurus.cli.common.serial_mode -@taurus.cli.common.default_formatter +@common.log_level +@common.poll_period +@common.serial_mode +@common.default_formatter @click.option("--rconsole", "rconsole_port", type=click.INT, metavar="PORT", default=None, help="Enable remote debugging with rfoo on the given PORT") From 25ad094f67585ae3821ed538a1707d1a4b54bcc3 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 09:30:42 +0100 Subject: [PATCH 190/373] Capitalize first letter of log level to avoid clashing with log functions --- lib/taurus/cli/common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index af7b6a6d3..05e4646c9 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -87,9 +87,9 @@ def cmd2(model, serial_mode, poll_period, default_formatter): log_level = click.option( '--log-level', 'log_level', - type=click.Choice(['critical', 'error', 'warning', 'info', - 'debug', 'trace']), - default='debug', show_default=True, + type=click.Choice(['Critical', 'Error', 'Warning', 'Info', + 'Debug', 'Trace']), + default='Debug', show_default=True, help='filter specific log level', ) From cc3e8a27d84d5891f908211e88a5316685c078bf Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 09:40:22 +0100 Subject: [PATCH 191/373] Reduce number of rasied deprecation warnings --- lib/taurus/qt/qtgui/extra_guiqwt/cli.py | 13 +++++++++++++ lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/cli.py b/lib/taurus/qt/qtgui/extra_guiqwt/cli.py index 1a00f8dfc..7592d10ae 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/cli.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/cli.py @@ -28,6 +28,19 @@ from .taurustrend2d import trend2d_cmd +def buffer(default): + o = click.option( + '-b', '--buffer', 'max_buffer_size', + type=int, + default=default, + show_default=True, + help=("maximum number of values to be stacked " + + "(when reached, the oldest values will be " + + "discarded)"), + ) + return o + + @click.group('guiqwt') def guiqwt(): """guiqwt related commands (image, trend2d)""" diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index b94a9bd76..52333d21c 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -41,7 +41,7 @@ AutoScaleXTool, AutoScaleYTool, AutoScaleZTool) import taurus.cli.common -import taurus.qt.qtgui.qwt5.cli +import taurus.qt.qtgui.extra_guiqwt.cli class TaurusTrend2DDialog(ImageDialog, TaurusBaseWidget): @@ -314,7 +314,7 @@ def setModifiableByUser(self, modifiable): @taurus.cli.common.model @taurus.cli.common.demo @taurus.cli.common.window_name('TaurusPlot (qwt5)') -@taurus.qt.qtgui.qwt5.cli.buffer(512) +@taurus.qt.qtgui.extra_guiqwt.cli.buffer(512) @click.option( "-x", "--x-axis-mode", "x_axis_mode", type=click.Choice(['t', 'd', 'e']), From 8627e16c6f6d6b16ab00f5667b80eaf931f725cf Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 09:42:29 +0100 Subject: [PATCH 192/373] Update log_level common CLI flag --- lib/taurus/cli/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 05e4646c9..8cf54d662 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -89,8 +89,8 @@ def cmd2(model, serial_mode, poll_period, default_formatter): '--log-level', 'log_level', type=click.Choice(['Critical', 'Error', 'Warning', 'Info', 'Debug', 'Trace']), - default='Debug', show_default=True, - help='filter specific log level', + default='Info', show_default=True, + help='Show only logs with priority LEVEL or above', ) config_file = click.option( From 9219ec6993a84ff6219bff4203ca6681ec464d28 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 10:13:40 +0100 Subject: [PATCH 193/373] Fix typo in docs --- lib/taurus/cli/common.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 8cf54d662..0233ae23f 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -37,9 +37,9 @@ @click.command("bar") - @aurus.cli.common.poll_period - @aurus.cli.common.default_formatter - @aurus.cli.common.window_name("Super Bar") + @taurus.cli.common.poll_period + @taurus.cli.common.default_formatter + @taurus.cli.common.window_name("Super Bar") def bar(poll_period, default_formatter, window_name): ... @@ -61,18 +61,18 @@ def foo(): @foo.command('cmd1') - @aurus.cli.common.models - @aurus.cli.common.config_file - @aurus.cli.common.window_name("Super Foo (cmd1)") + @taurus.cli.common.models + @taurus.cli.common.config_file + @taurus.cli.common.window_name("Super Foo (cmd1)") def cmd1(models, config_file, window_name): ... @foo.command('trend') - @aurus.cli.common.model - @aurus.cli.common.serial_mode - @aurus.cli.common.poll_period - @aurus.cli.common.default_formatter + @taurus.cli.common.model + @taurus.cli.common.serial_mode + @taurus.cli.common.poll_period + @taurus.cli.common.default_formatter def cmd2(model, serial_mode, poll_period, default_formatter): ... From ed39a3f1a3adfd4ce42ce5cf999a48c866461785 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 10:16:10 +0100 Subject: [PATCH 194/373] Capitalize first letter of help mesages --- lib/taurus/cli/common.py | 4 ++-- lib/taurus/core/util/remotelogmonitor.py | 4 ++-- lib/taurus/qt/qtgui/extra_guiqwt/cli.py | 2 +- lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py | 2 +- lib/taurus/qt/qtgui/qwt5/cli.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 0233ae23f..a4e88be10 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -96,7 +96,7 @@ def cmd2(model, serial_mode, poll_period, default_formatter): config_file = click.option( '--config', 'config_file', type=click.File('rb'), - help='configuration file for initialization', + help='Configuration file for initialization', ) def window_name(default): @@ -114,7 +114,7 @@ def window_name(default): demo = click.option( "--demo", is_flag=True, - help="show a demo of the widget", + help="Show a demo of the widget", ) model = click.argument( diff --git a/lib/taurus/core/util/remotelogmonitor.py b/lib/taurus/core/util/remotelogmonitor.py index 818964b97..c75c23322 100644 --- a/lib/taurus/core/util/remotelogmonitor.py +++ b/lib/taurus/core/util/remotelogmonitor.py @@ -189,12 +189,12 @@ def log(host, port, name=None, level=None): '--port', 'port', type=int, default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, show_default=True, - help='port where log server is running', + help='Port where log server is running', ) @click.option( '--log-name', 'log_name', default=None, - help='filter specific log object', + help='Filter specific log object', ) @taurus.cli.common.log_level def logmon_cmd(port, log_name, log_level): diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/cli.py b/lib/taurus/qt/qtgui/extra_guiqwt/cli.py index 7592d10ae..a8ba84e2c 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/cli.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/cli.py @@ -34,7 +34,7 @@ def buffer(default): type=int, default=default, show_default=True, - help=("maximum number of values to be stacked " + help=("Maximum number of values to be stacked " + "(when reached, the oldest values will be " + "discarded)"), ) diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 52333d21c..700709a2a 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -320,7 +320,7 @@ def setModifiableByUser(self, modifiable): type=click.Choice(['t', 'd', 'e']), default='d', show_default=True, - help=("interpret X values as timestamps (t), time deltas (d) " + help=("Interpret X values as timestamps (t), time deltas (d) " + " or event numbers (e). ") ) def trend2d_cmd(model, demo, window_name, max_buffer_size, diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index 3ebf434e7..91c04b09b 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -41,7 +41,7 @@ def buffer(default): type=int, default=default, show_default=True, - help=("maximum number of values to be stacked " + help=("Maximum number of values to be stacked " + "(when reached, the oldest values will be " + "discarded)"), ) From 2136a86eebf7a769ce183e4172ae14daa53f9c61 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 10:17:02 +0100 Subject: [PATCH 195/373] Fix window name --- lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 700709a2a..80af498c3 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -313,7 +313,7 @@ def setModifiableByUser(self, modifiable): @click.command('trend2d') @taurus.cli.common.model @taurus.cli.common.demo -@taurus.cli.common.window_name('TaurusPlot (qwt5)') +@taurus.cli.common.window_name('TaurusTrend2D') @taurus.qt.qtgui.extra_guiqwt.cli.buffer(512) @click.option( "-x", "--x-axis-mode", "x_axis_mode", From 5930f9d9adba2c82a6b8fe3a83e5a5520d9af53c Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 11:02:05 +0100 Subject: [PATCH 196/373] Fix window name --- lib/taurus/qt/qtgui/qwt5/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index 91c04b09b..2039ef04d 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -76,7 +76,7 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): @x_axis_mode @taurus.cli.common.config_file @taurus.cli.common.demo -@taurus.cli.common.window_name('TaurusPlot (qwt5)') +@taurus.cli.common.window_name('TaurusTrend(qwt5)') @buffer(TaurusTrend.DEFAULT_MAX_BUFFER_SIZE) @click.option('-a', '--use-archiving', 'use_archiving', is_flag=True, From 054470291dfbcb93c2930bd070606f67e3e9afb8 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 20:59:56 +0100 Subject: [PATCH 197/373] Avoid circullar import --- lib/taurus/qt/qtgui/extra_guiqwt/cli.py | 13 ------------- lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/cli.py b/lib/taurus/qt/qtgui/extra_guiqwt/cli.py index a8ba84e2c..1a00f8dfc 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/cli.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/cli.py @@ -28,19 +28,6 @@ from .taurustrend2d import trend2d_cmd -def buffer(default): - o = click.option( - '-b', '--buffer', 'max_buffer_size', - type=int, - default=default, - show_default=True, - help=("Maximum number of values to be stacked " - + "(when reached, the oldest values will be " - + "discarded)"), - ) - return o - - @click.group('guiqwt') def guiqwt(): """guiqwt related commands (image, trend2d)""" diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 80af498c3..dff93b85b 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -41,7 +41,6 @@ AutoScaleXTool, AutoScaleYTool, AutoScaleZTool) import taurus.cli.common -import taurus.qt.qtgui.extra_guiqwt.cli class TaurusTrend2DDialog(ImageDialog, TaurusBaseWidget): @@ -314,7 +313,15 @@ def setModifiableByUser(self, modifiable): @taurus.cli.common.model @taurus.cli.common.demo @taurus.cli.common.window_name('TaurusTrend2D') -@taurus.qt.qtgui.extra_guiqwt.cli.buffer(512) +@click.option( + '-b', '--buffer', 'max_buffer_size', + type=int, + default=512, + show_default=True, + help=("Maximum number of values to be stacked " + + "(when reached, the oldest values will be " + + "discarded)"), +) @click.option( "-x", "--x-axis-mode", "x_axis_mode", type=click.Choice(['t', 'd', 'e']), From 044866e4965fc26ffdfc638c870d8c182851c225 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Thu, 5 Mar 2020 21:08:10 +0100 Subject: [PATCH 198/373] Add missing capitalization --- lib/taurus/qt/qtgui/table/qlogtable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/table/qlogtable.py b/lib/taurus/qt/qtgui/table/qlogtable.py index d3138c3bf..bf9e691dd 100644 --- a/lib/taurus/qt/qtgui/table/qlogtable.py +++ b/lib/taurus/qt/qtgui/table/qlogtable.py @@ -605,12 +605,12 @@ def main(): '--port', 'port', type=int, default=logging.handlers.DEFAULT_TCP_LOGGING_PORT, show_default=True, - help='port where log server is running', + help='Port where log server is running', ) @click.option( '--log-name', 'log_name', default=None, - help='filter specific log object', + help='Filter specific log object', ) @taurus.cli.common.log_level def qlogmon_cmd(port, log_name, log_level): From 43989ecaae97d2c57de0418e99928ffce3ca9dec Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Fri, 6 Mar 2020 08:30:17 +0100 Subject: [PATCH 199/373] Import required sys module just for short moment --- lib/taurus/qt/qtgui/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/__init__.py b/lib/taurus/qt/qtgui/__init__.py index d65fcebf9..674f91e94 100644 --- a/lib/taurus/qt/qtgui/__init__.py +++ b/lib/taurus/qt/qtgui/__init__.py @@ -63,12 +63,14 @@ def __init__(self, name, package, entry_point): self.ep = entry_point def __getattr__(self, member): + import sys as __sys mod = self.ep.load() # Replace lazy module with actual module for package - setattr(sys.modules[self.__package__], self.__name__, mod) + setattr(__sys.modules[self.__package__], self.__name__, mod) # Replace lazy module with actual module in sys.modules modname = "%s.%s" % (self.__package__, self.__name__) - sys.modules[modname] = mod + __sys.modules[modname] = mod + del __sys return getattr(mod, member) # Discover the taurus.qt.qtgui plugins From 3893ef46e85eb38e561332f7565737436c40b91b Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Mon, 9 Mar 2020 10:57:51 +0100 Subject: [PATCH 200/373] Catch and report errors when actually loading plugin --- lib/taurus/qt/qtgui/__init__.py | 46 ++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/taurus/qt/qtgui/__init__.py b/lib/taurus/qt/qtgui/__init__.py index 674f91e94..6bcccfb30 100644 --- a/lib/taurus/qt/qtgui/__init__.py +++ b/lib/taurus/qt/qtgui/__init__.py @@ -38,7 +38,6 @@ from types import ModuleType from taurus import tauruscustomsettings as __S from taurus import info as __info -from taurus import warning as __warning __docformat__ = 'restructuredtext' @@ -64,31 +63,36 @@ def __init__(self, name, package, entry_point): def __getattr__(self, member): import sys as __sys - mod = self.ep.load() - # Replace lazy module with actual module for package - setattr(__sys.modules[self.__package__], self.__name__, mod) - # Replace lazy module with actual module in sys.modules - modname = "%s.%s" % (self.__package__, self.__name__) - __sys.modules[modname] = mod - del __sys - return getattr(mod, member) + + from taurus import warning as __warning + + try: + mod = self.ep.load() + # Replace lazy module with actual module for package + setattr(__sys.modules[self.__package__], self.__name__, mod) + # Replace lazy module with actual module in sys.modules + modname = "%s.%s" % (self.__package__, self.__name__) + __sys.modules[modname] = mod + return getattr(mod, member) + except Exception as e: + __warning('Could not load plugin "%s". Reason: %s', __p.module_name, e) + return None + finally: + del __sys, __warning + # Discover the taurus.qt.qtgui plugins __mod = __modname = None for __p in pkg_resources.iter_entry_points('taurus.qt.qtgui'): - try: - __modname = '%s.%s' % (__name__, __p.name) - __lazy_mod = LazyModule(__p.name, __name__, __p) - # Add it to the current module - setattr(sys.modules[__name__], __p.name, __lazy_mod) - # Add it to sys.modules - sys.modules[__modname] = __lazy_mod - __info('Plugin "%s" loaded as "%s"', __p.module_name, __modname) - except Exception as e: - __warning('Could not load plugin "%s". Reason: %s', __p.module_name, e) + __modname = '%s.%s' % (__name__, __p.name) + __lazy_mod = LazyModule(__p.name, __name__, __p) + # Add it to the current module + setattr(sys.modules[__name__], __p.name, __lazy_mod) + # Add it to sys.modules + sys.modules[__modname] = __lazy_mod + __info('Plugin "%s" loaded as "%s"', __p.module_name, __modname) # ------------------------------------------------------------------------ -del (os, glob, __icon, icon_dir, pkg_resources, sys, __mod, __modname, __info, - __warning) +del (os, glob, __icon, icon_dir, pkg_resources, sys, __mod, __modname, __info) From c33eeb47e7b9f312ba030b4e77aafd5af5c4cd27 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 10 Mar 2020 09:03:51 +0100 Subject: [PATCH 201/373] Add special module for LazyModule --- lib/taurus/core/util/lazymodule.py | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 lib/taurus/core/util/lazymodule.py diff --git a/lib/taurus/core/util/lazymodule.py b/lib/taurus/core/util/lazymodule.py new file mode 100644 index 000000000..46b2c7c1d --- /dev/null +++ b/lib/taurus/core/util/lazymodule.py @@ -0,0 +1,56 @@ +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +""" +This module provides LazyModule for loading plugins in lazy way - +only when members of actual plugin are requested. +""" + +__all__ = ["LazyModule"] + +from types import ModuleType +import sys + +from taurus import warning + + +class LazyModule(ModuleType): + + def __init__(self, name, package, entry_point): + super(LazyModule, self).__init__(name) + self.__package__ = package + self.ep = entry_point + + def __getattr__(self, member): + + try: + mod = self.ep.load() + # Replace lazy module with actual module for package + setattr(sys.modules[self.__package__], self.__name__, mod) + # Replace lazy module with actual module in sys.modules + modname = "%s.%s" % (self.__package__, self.__name__) + sys.modules[modname] = mod + return getattr(mod, member) + except Exception as e: + warning('Could not load plugin "%s". Reason: %s', self.ep.name, e) + return None From 1bf8b89b4f356c80a97629ee856e22eec9de1955 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 10 Mar 2020 09:04:37 +0100 Subject: [PATCH 202/373] Use LazyModule from taurus.core.util.lazymodule --- lib/taurus/qt/qtgui/__init__.py | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/lib/taurus/qt/qtgui/__init__.py b/lib/taurus/qt/qtgui/__init__.py index 6bcccfb30..ef2318a59 100644 --- a/lib/taurus/qt/qtgui/__init__.py +++ b/lib/taurus/qt/qtgui/__init__.py @@ -35,9 +35,10 @@ import sys import glob import pkg_resources -from types import ModuleType + from taurus import tauruscustomsettings as __S from taurus import info as __info +from taurus.core.util.lazymodule import LazyModule __docformat__ = 'restructuredtext' @@ -54,33 +55,6 @@ # Note: this is an experimental feature introduced in v 4.3.0a # It may be removed or changed in future releases -class LazyModule(ModuleType): - - def __init__(self, name, package, entry_point): - super(LazyModule, self).__init__(name) - self.__package__ = package - self.ep = entry_point - - def __getattr__(self, member): - import sys as __sys - - from taurus import warning as __warning - - try: - mod = self.ep.load() - # Replace lazy module with actual module for package - setattr(__sys.modules[self.__package__], self.__name__, mod) - # Replace lazy module with actual module in sys.modules - modname = "%s.%s" % (self.__package__, self.__name__) - __sys.modules[modname] = mod - return getattr(mod, member) - except Exception as e: - __warning('Could not load plugin "%s". Reason: %s', __p.module_name, e) - return None - finally: - del __sys, __warning - - # Discover the taurus.qt.qtgui plugins __mod = __modname = None for __p in pkg_resources.iter_entry_points('taurus.qt.qtgui'): From 8c45f0048739a2db69b74492be8d930da2b3c735 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Tue, 10 Mar 2020 09:05:33 +0100 Subject: [PATCH 203/373] Improve log message --- lib/taurus/qt/qtgui/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/__init__.py b/lib/taurus/qt/qtgui/__init__.py index ef2318a59..567b915da 100644 --- a/lib/taurus/qt/qtgui/__init__.py +++ b/lib/taurus/qt/qtgui/__init__.py @@ -64,7 +64,7 @@ setattr(sys.modules[__name__], __p.name, __lazy_mod) # Add it to sys.modules sys.modules[__modname] = __lazy_mod - __info('Plugin "%s" loaded as "%s"', __p.module_name, __modname) + __info('Plugin "%s" lazy-loaded as "%s"', __p.module_name, __modname) # ------------------------------------------------------------------------ From ac25b32b8a3bc28e15173ba18c6bcf00d5de99da Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 23 Mar 2020 14:38:25 +0100 Subject: [PATCH 204/373] Update conda environment instructions - Remove itango since it is not mandatory and it introduces dependency constraints - Remove instruction to use tiagocoutinho's channel for windows (no longer needed) --- doc/source/users/getting_started.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/users/getting_started.rst b/doc/source/users/getting_started.rst index 9f39fda58..89b9cdd43 100644 --- a/doc/source/users/getting_started.rst +++ b/doc/source/users/getting_started.rst @@ -47,8 +47,8 @@ Installing in a conda environment (Windows and linux) First create a Conda_ environment with all the dependencies and activate it:: conda config --add channels conda-forge - conda config --add channels tango-controls # for windows, use "tcoutinho" instead of "tango-controls" - conda create -n py3qt5 python=3 pyqt=5 itango pytango lxml future guidata guiqwt ipython pillow pint ply pyqtgraph pythonqwt numpy scipy pymca click + conda config --add channels tango-controls + conda create -n py3qt5 python=3 pyqt=5 pytango lxml future guidata guiqwt ipython pillow pint ply pyqtgraph pythonqwt numpy scipy pymca click conda activate py3qt5 Then install taurus and taurus_pyqtgraph using pip (as explained above) From 0a713dea830718f222cd1fd3ab1443d8f88a007b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 23 Mar 2020 15:37:03 +0100 Subject: [PATCH 205/373] (d) Update dependency links --- doc/source/users/getting_started.rst | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/doc/source/users/getting_started.rst b/doc/source/users/getting_started.rst index 89b9cdd43..80816ba62 100644 --- a/doc/source/users/getting_started.rst +++ b/doc/source/users/getting_started.rst @@ -22,7 +22,6 @@ You can test the installation by running:: python -c "import taurus; print taurus.Release.version" -Note: pip is already included in python>2.7.9 Note: some "extra" features of taurus have additional dependencies_. @@ -30,11 +29,11 @@ Note: some "extra" features of taurus have additional dependencies_. Linux (Debian-based) ~~~~~~~~~~~~~~~~~~~~ -Since v3.0, Taurus is part of the official repositories of Debian (and Ubuntu +Taurus is part of the official repositories of Debian (and Ubuntu and other Debian-based distros). You can install it and all its dependencies by doing (as root):: - aptitude install python-taurus + apt-get install python-taurus Note: `python3-taurus` and `python3-taurus-pyqtgraph` packages are already built in https://salsa.debian.org , but are not yet part of the official debian @@ -75,7 +74,7 @@ on each change:: Dependencies ------------ -Strictly speaking, Taurus only depends on numpy_, pint_ and future_ +Strictly speaking, Taurus only depends on numpy_, click_, pint_ and future_ but that will leave out most of the features normally expected of Taurus (which are considered "extras"). For example: @@ -85,7 +84,7 @@ expected of Taurus (which are considered "extras"). For example: - Using the taurus Qt_ widgets, requires either PyQt_ (v4 or v5) or PySide_ (v1 or v2). Note that most development and testing of - is done with PyQt4 and PyQt5, so many features may not be + is done with PyQt, so many features may not be regularly tested with PySide and PySide2. - The :mod:`taurus.qt.qtgui.qwt5` module requires PyQwt_, which is @@ -137,12 +136,12 @@ installation method: .. _PySide: https://wiki.qt.io/Qt_for_Python .. _PyQwt: http://pyqwt.sourceforge.net/ .. _taurus_pyqtgraph: https://github.com/taurus-org/taurus_pyqtgraph -.. _guiqwt: https://pypi.python.org/pypi/guiqtw +.. _guiqwt: https://pypi.org/project/guiqwt/ .. _IPython: http://ipython.org .. _PyMca5: http://pymca.sourceforge.net/ -.. _pyepics: http://pypi.python.org/pypi/pyepics +.. _pyepics: https://pypi.org/project/pyepics/ .. _spyder: http://pythonhosted.org/spyder .. _lxml: http://lxml.de -.. _PyPI: http://pypi.python.org/pypi .. _Conda: http://conda.io/docs/ +.. _click: https://pypi.org/project/click/ .. _taurus-test Docker container: http://hub.docker.com/r/cpascual/taurus-test/ From 86d28bd5967d5dde0209ec59e8df6423072299d1 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 30 Mar 2020 21:33:43 +0200 Subject: [PATCH 206/373] m, doc, Adapt doc to python3 --- doc/source/users/getting_started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/users/getting_started.rst b/doc/source/users/getting_started.rst index 80816ba62..c5eb436a2 100644 --- a/doc/source/users/getting_started.rst +++ b/doc/source/users/getting_started.rst @@ -20,7 +20,7 @@ download and install the latest release of Taurus (see pip --help for options):: You can test the installation by running:: - python -c "import taurus; print taurus.Release.version" + python -c "import taurus; print(taurus.Release.version)" Note: some "extra" features of taurus have additional dependencies_. From 7ab2044e0506b449c055395da8315beb0b9185cb Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Fri, 3 Apr 2020 16:59:07 +0200 Subject: [PATCH 207/373] Fix #1097: Tango extension image devices resubscribe at every frame --- lib/taurus/core/tango/img/img.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/taurus/core/tango/img/img.py b/lib/taurus/core/tango/img/img.py index 29fbe80a4..a124ba4d7 100644 --- a/lib/taurus/core/tango/img/img.py +++ b/lib/taurus/core/tango/img/img.py @@ -72,6 +72,7 @@ def __init__(self, name, image_name='image', image_ct='imagecounter', self._image_data = CaselessDict() self._image_id_attr_name = image_ct self.call__init__(ImageDevice, name, image_name, **kw) + self.image_attr = self.getAttribute(image_name) self._image_id_attr = self.getAttribute(self._image_id_attr_name) self.discard_event = False self._image_id_attr.addListener(self) From 8d7252bf9d95419cf4df5f42e39cb7694150c7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=C5=82ek?= Date: Tue, 14 Apr 2020 13:16:16 +0200 Subject: [PATCH 208/373] Catch RuntimeError in tauruscombobox postDetach In case of disconnecting signal which was not connected before, PyQt5 raises `TypeError`, but PySide2 raises `RuntimeError`. --- lib/taurus/qt/qtgui/input/tauruscombobox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/tauruscombobox.py b/lib/taurus/qt/qtgui/input/tauruscombobox.py index cfe188a91..5a89e6f08 100644 --- a/lib/taurus/qt/qtgui/input/tauruscombobox.py +++ b/lib/taurus/qt/qtgui/input/tauruscombobox.py @@ -77,7 +77,7 @@ def postDetach(self): TaurusBaseWritableWidget.postDetach(self) try: self.currentIndexChanged.disconnect(self.writeIndexValue) - except TypeError: + except (RuntimeError, TypeError): # In new style-signal if a signal is disconnected without # previously was connected it, it raises a TypeError pass From 61aea7abc6c344aeec72365d5b03d03ac51bf824 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 14 Apr 2020 15:30:04 +0200 Subject: [PATCH 209/373] m, Add PySide info in comment --- lib/taurus/qt/qtgui/input/tauruscombobox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/tauruscombobox.py b/lib/taurus/qt/qtgui/input/tauruscombobox.py index 5a89e6f08..957ade889 100644 --- a/lib/taurus/qt/qtgui/input/tauruscombobox.py +++ b/lib/taurus/qt/qtgui/input/tauruscombobox.py @@ -79,7 +79,8 @@ def postDetach(self): self.currentIndexChanged.disconnect(self.writeIndexValue) except (RuntimeError, TypeError): # In new style-signal if a signal is disconnected without - # previously was connected it, it raises a TypeError + # previously was connected it, it raises a TypeError (PyQt) + # or RuntimeError (PySide) pass #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- From 2c3ca746acf871bdf5b174eb29a0de6b5803c040 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 27 Apr 2020 16:53:52 +0200 Subject: [PATCH 210/373] m, doc, Support MacOSX in getting started The conda installation method works for mac too except for pytango which is not available. --- doc/source/users/getting_started.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/users/getting_started.rst b/doc/source/users/getting_started.rst index c5eb436a2..0358c7b3a 100644 --- a/doc/source/users/getting_started.rst +++ b/doc/source/users/getting_started.rst @@ -40,13 +40,14 @@ built in https://salsa.debian.org , but are not yet part of the official debian repositories -Installing in a conda environment (Windows and linux) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Installing in a conda environment (platform-independent) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First create a Conda_ environment with all the dependencies and activate it:: conda config --add channels conda-forge conda config --add channels tango-controls + # note: MacOSX users, remove "pytango" from the following command conda create -n py3qt5 python=3 pyqt=5 pytango lxml future guidata guiqwt ipython pillow pint ply pyqtgraph pythonqwt numpy scipy pymca click conda activate py3qt5 From 1aa1063667c83f4564f96435eefd7c14be094227 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 7 May 2020 19:52:04 +0200 Subject: [PATCH 211/373] Improve lazymodule - Document members - Raise exception on AttriuteError during "real" import - Add a static method to simplify usage --- lib/taurus/core/util/lazymodule.py | 42 ++++++++++++++++++++++++++---- lib/taurus/qt/qtgui/__init__.py | 16 +++--------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/lib/taurus/core/util/lazymodule.py b/lib/taurus/core/util/lazymodule.py index 46b2c7c1d..56d3b53b6 100644 --- a/lib/taurus/core/util/lazymodule.py +++ b/lib/taurus/core/util/lazymodule.py @@ -22,7 +22,7 @@ ############################################################################# """ -This module provides LazyModule for loading plugins in lazy way - +This module provides LazyModule for loading plugin modules in lazy way - only when members of actual plugin are requested. """ @@ -31,18 +31,28 @@ from types import ModuleType import sys -from taurus import warning +from taurus import warning, info class LazyModule(ModuleType): - + """It provides a ModuleType object that acts as a placeholder for modules + registered via entry-points. It replaces the actual module without actually + importing it until a member of the module is requested, which automatically + triggers the load of the entry-point and the substitution of this + placeholder by the actual module + """ def __init__(self, name, package, entry_point): + """ + + :param name: name of the module + :param package: name of the package to which the module belongs + :param entry_point: entry-point for the module + """ super(LazyModule, self).__init__(name) self.__package__ = package self.ep = entry_point def __getattr__(self, member): - try: mod = self.ep.load() # Replace lazy module with actual module for package @@ -50,7 +60,29 @@ def __getattr__(self, member): # Replace lazy module with actual module in sys.modules modname = "%s.%s" % (self.__package__, self.__name__) sys.modules[modname] = mod - return getattr(mod, member) except Exception as e: warning('Could not load plugin "%s". Reason: %s', self.ep.name, e) return None + return getattr(mod, member) + + @staticmethod + def import_ep(name, package, entry_point): + """ + Lazily imports a module defined in an entry point. The LazyModule is + inserted in sys.modules as . + + :param name: name of the module + :param package: name of the package to which the module belongs + :param entry_point: entry-point for a module + :return: A LazyModule object + """ + m = LazyModule(name, package, entry_point) + setattr(sys.modules[package], name, m) + modname = "{}.{}".format(package, name) + sys.modules[modname] = m + info( + 'Plugin "%s" lazy-loaded as "%s"', + entry_point.module_name, + modname + ) + return m diff --git a/lib/taurus/qt/qtgui/__init__.py b/lib/taurus/qt/qtgui/__init__.py index 567b915da..99be48257 100644 --- a/lib/taurus/qt/qtgui/__init__.py +++ b/lib/taurus/qt/qtgui/__init__.py @@ -37,8 +37,7 @@ import pkg_resources from taurus import tauruscustomsettings as __S -from taurus import info as __info -from taurus.core.util.lazymodule import LazyModule +from taurus.core.util.lazymodule import LazyModule as __LM __docformat__ = 'restructuredtext' @@ -56,17 +55,10 @@ # It may be removed or changed in future releases # Discover the taurus.qt.qtgui plugins -__mod = __modname = None +# __mod = __modname = None for __p in pkg_resources.iter_entry_points('taurus.qt.qtgui'): - __modname = '%s.%s' % (__name__, __p.name) - __lazy_mod = LazyModule(__p.name, __name__, __p) - # Add it to the current module - setattr(sys.modules[__name__], __p.name, __lazy_mod) - # Add it to sys.modules - sys.modules[__modname] = __lazy_mod - __info('Plugin "%s" lazy-loaded as "%s"', __p.module_name, __modname) + __LM.import_ep(__p.name, __name__, __p) # ------------------------------------------------------------------------ -del (os, glob, __icon, icon_dir, pkg_resources, sys, __mod, __modname, __info) - +del (os, glob, __icon, icon_dir, pkg_resources, sys, __LM) From 965740d2bf9a74d4d67697562af0311343954ff4 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 7 May 2020 19:52:52 +0200 Subject: [PATCH 212/373] (WIP) Add unit test for lazymodule --- lib/taurus/core/util/test/test_lazymodule.py | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lib/taurus/core/util/test/test_lazymodule.py diff --git a/lib/taurus/core/util/test/test_lazymodule.py b/lib/taurus/core/util/test/test_lazymodule.py new file mode 100644 index 000000000..5b56a5623 --- /dev/null +++ b/lib/taurus/core/util/test/test_lazymodule.py @@ -0,0 +1,31 @@ +import sys +import pkg_resources +from types import ModuleType +from taurus.core.util.lazymodule import LazyModule + + +def test_lazy_import_entry_point(): + modname = "foobartest" + pkg_name = "taurus.core.util.test" + ep = list(pkg_resources.iter_entry_points('taurus.foobartest'))[0] + print(ep) + + LazyModule.import_ep(modname, pkg_name, ep) + + # check that creating the LazyModule does not have side-efects + import taurus.core.util.test + assert modname not in sys.modules + + # import the module and check that it is a LazyModule + import taurus.core.util.test.foobartest as lzm + assert isinstance(lzm, LazyModule) + + # ask for a member of the lazy module + assert lzm.foo == 1 + + # ...and check that any future import of lzm will import "real" module, + # not the lazy one + + import taurus.core.util.test.foobartest as lzm + assert not isinstance(lzm, LazyModule) + assert isinstance(lzm, ModuleType) From 435b6a4977975702415c8332ca327bc3cda5633b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 7 May 2020 21:14:07 +0200 Subject: [PATCH 213/373] Finish working version of test_lazymodule Define the entry point on-the-fly in order not to require an installed dummy package with defined entry pointsfor the lazymodule unit test --- lib/taurus/core/util/test/dummy.py | 1 + lib/taurus/core/util/test/test_lazymodule.py | 35 ++++++++++++-------- lib/taurus/test/resource.py | 2 +- 3 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 lib/taurus/core/util/test/dummy.py diff --git a/lib/taurus/core/util/test/dummy.py b/lib/taurus/core/util/test/dummy.py new file mode 100644 index 000000000..8073606b2 --- /dev/null +++ b/lib/taurus/core/util/test/dummy.py @@ -0,0 +1 @@ +foo = 1 \ No newline at end of file diff --git a/lib/taurus/core/util/test/test_lazymodule.py b/lib/taurus/core/util/test/test_lazymodule.py index 5b56a5623..730c5f47a 100644 --- a/lib/taurus/core/util/test/test_lazymodule.py +++ b/lib/taurus/core/util/test/test_lazymodule.py @@ -3,29 +3,38 @@ from types import ModuleType from taurus.core.util.lazymodule import LazyModule +from pkg_resources import EntryPoint, Requirement, WorkingSet -def test_lazy_import_entry_point(): - modname = "foobartest" - pkg_name = "taurus.core.util.test" - ep = list(pkg_resources.iter_entry_points('taurus.foobartest'))[0] - print(ep) - LazyModule.import_ep(modname, pkg_name, ep) +def test_LazyModule(): - # check that creating the LazyModule does not have side-efects - import taurus.core.util.test + # create an entry point for taurus.core.util.test.dumm + w = WorkingSet() + d = w.find(Requirement.parse('taurus')) + ep = EntryPoint.parse("dummy_mod = taurus.core.util.test.dummy", dist=d) + modname = ep.name + + # lazy load the ep module as taurus.fbt + LazyModule.import_ep(modname, "taurus", ep) + + # check that lazy-loading did not import the entry point modules assert modname not in sys.modules + assert ep.module_name not in sys.modules # import the module and check that it is a LazyModule - import taurus.core.util.test.foobartest as lzm + import taurus.dummy_mod as lzm + assert isinstance(lzm, LazyModule) + + # same again + import taurus.dummy_mod as lzm assert isinstance(lzm, LazyModule) - # ask for a member of the lazy module + # now access a member of the lazy module assert lzm.foo == 1 - # ...and check that any future import of lzm will import "real" module, - # not the lazy one + # ...and check that any subsequent import will return a "real" module, + # not a lazy one - import taurus.core.util.test.foobartest as lzm + import taurus.dummy_mod as lzm assert not isinstance(lzm, LazyModule) assert isinstance(lzm, ModuleType) diff --git a/lib/taurus/test/resource.py b/lib/taurus/test/resource.py index 2ad8fbb5c..541c92179 100644 --- a/lib/taurus/test/resource.py +++ b/lib/taurus/test/resource.py @@ -66,5 +66,5 @@ def getResourcePath(resmodule, fname=''): print(getResourcePath('taurus.test')) print(getResourcePath('taurus.test', 'resource.py')) # print getResourcePath('taurus.qt.qtgui.plot', 'taurusplot.py') - # print getResourcePath('taurus.test', 'kk.py') + # print getResourcePath('taurus.test', 'dummy.py') # print getResourcePath('taurus.kk', 'resource.py') From 101716fb5a40966d913c60fc6bdad17d67cabdf0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 7 May 2020 22:46:08 +0200 Subject: [PATCH 214/373] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 987e700f9..a4aa8cadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ develop branch) won't be reflected in this file. - Support for spyder v4 in `taurus.qt.qtgui.editor` (#1038) - Entry-point ("taurus.qt.formatters") for registering formatters via plugins (#1039) - New `worker_cls` argument for `taurus.core.util.ThreadPool` costructor (#1081) +- `taurus.core.util.lazymodule` for delayed entry-point loading of modules (#1090) ### Removed ### Changed @@ -23,6 +24,7 @@ develop branch) won't be reflected in this file. being now automatically tested by travis (#994) - TaurusForm provides more debugging info when failing to handle a model (#1049) - Improved GUI dialog for changing the formatter of a widget (#1039) +- Modules registered with `"taurus.qt.qtgui"` entry-point are now lazy-loaded (#1090) ### Deprecated - `TaurusBaseWidget.showFormatterDlg()` (#1039) From 4112f09a2a6de4a6dde750bb3c55e342805636ea Mon Sep 17 00:00:00 2001 From: zreszela Date: Thu, 7 May 2020 23:59:09 +0200 Subject: [PATCH 215/373] Add expiration_period arg to TangoAttribute.read() There is no way different then modifying the polling period to make the attribute readout return the value of the last event. Add expiration period concept to the attribute single readout. --- lib/taurus/core/tango/tangoattribute.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 20dcbdedd..5628053e0 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -515,7 +515,7 @@ def poll(self, single=True, value=None, time=None, error=None): self.__subscription_event.set() self.fireEvent(TaurusEventType.Periodic, self.__attr_value) - def read(self, cache=True): + def read(self, cache=True, expiration_period=None): """ Returns the current value of the attribute. if cache is set to True (default) or the attribute has events active then it will return the local cached value. Otherwise it will @@ -527,7 +527,9 @@ def read(self, cache=True): except AttributeError: attr_timestamp = 0 dt = (curr_time - attr_timestamp) * 1000 - if dt < self.getPollingPeriod(): + if expiration_period is None: + expiration_period = self.getPollingPeriod() + if dt < expiration_period: if self.__attr_value is not None: return self.__attr_value elif self.__attr_err is not None: From 3804fcef218bb364cb520ee7c7c8b28430a4ed87 Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 8 May 2020 00:22:31 +0200 Subject: [PATCH 216/373] Update docs of TangoAttribute.read() --- lib/taurus/core/tango/tangoattribute.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 5628053e0..d002c4206 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -517,9 +517,22 @@ def poll(self, single=True, value=None, time=None, error=None): def read(self, cache=True, expiration_period=None): """ Returns the current value of the attribute. - if cache is set to True (default) or the attribute has events - active then it will return the local cached value. Otherwise it will - read the attribute value from the tango device.""" + + If cache is set to True (default) or the attribute has events + active then it will return the local cached value. Otherwise it will + read the attribute value from the tango device. + + Cached value can expire if it is older then expiration_period, + or if this is not specified the *polling period*, what would result in + the attribute readout as if cache would be False. + + :param cache: use cache value or make readout + :type cache: :obj:`bool` + :param expiration_period: time period in seconds + :type expiration_period: :obj:`float` + :return: attribute value + :rtype: :obj:`~taurus.core.tango.TangoAttributeValue` + """ curr_time = time.time() if cache: try: From c4476f2065ea0c17949ce417abd30cf4f989cae5 Mon Sep 17 00:00:00 2001 From: zreszela Date: Mon, 11 May 2020 08:33:46 +0200 Subject: [PATCH 217/373] Pass expiration period with cache arg Simplify API to use only cache argument to achieve the same behavior. Change the expiration period unit to milliseconds to be coherent with the changePollingPeriod(). --- lib/taurus/core/tango/tangoattribute.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index d002c4206..d2f67211c 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -515,21 +515,20 @@ def poll(self, single=True, value=None, time=None, error=None): self.__subscription_event.set() self.fireEvent(TaurusEventType.Periodic, self.__attr_value) - def read(self, cache=True, expiration_period=None): + def read(self, cache=True): """ Returns the current value of the attribute. If cache is set to True (default) or the attribute has events active then it will return the local cached value. Otherwise it will read the attribute value from the tango device. - Cached value can expire if it is older then expiration_period, - or if this is not specified the *polling period*, what would result in - the attribute readout as if cache would be False. + Cached value expires if it is older then the *polling period*, what + results in the attribute readout as if cache would be False. You can + pass a different expiration period with the cache argument. - :param cache: use cache value or make readout - :type cache: :obj:`bool` - :param expiration_period: time period in seconds - :type expiration_period: :obj:`float` + :param cache: use cache value or make readout, eventually pass a + cache's expiration period in milliseconds + :type cache: :obj:`bool` or :obj:`float` :return: attribute value :rtype: :obj:`~taurus.core.tango.TangoAttributeValue` """ @@ -540,8 +539,10 @@ def read(self, cache=True, expiration_period=None): except AttributeError: attr_timestamp = 0 dt = (curr_time - attr_timestamp) * 1000 - if expiration_period is None: + if cache is True: # cache *is* a bool True expiration_period = self.getPollingPeriod() + else: # cache is a non-zero numeric value + expiration_period = cache if dt < expiration_period: if self.__attr_value is not None: return self.__attr_value From 77f0cab0bd273e59e98e66b311a94d93fcc83670 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 11 May 2020 09:58:03 +0200 Subject: [PATCH 218/373] (doc) Improve docstring of read() --- lib/taurus/core/tango/tangoattribute.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index d2f67211c..898585d04 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -518,13 +518,14 @@ def poll(self, single=True, value=None, time=None, error=None): def read(self, cache=True): """ Returns the current value of the attribute. - If cache is set to True (default) or the attribute has events - active then it will return the local cached value. Otherwise it will + If `cache` is not False, or expired, or the attribute has events + active, then it will return the local cached value. Otherwise it will read the attribute value from the tango device. - Cached value expires if it is older then the *polling period*, what - results in the attribute readout as if cache would be False. You can - pass a different expiration period with the cache argument. + The cached value expires if it is older than the value (im ms) passed + as the `cache` argument or the *polling period* if `cache==True` + (default). If the cache is expired a reading will be done just as if + cache was False. :param cache: use cache value or make readout, eventually pass a cache's expiration period in milliseconds From c7e713e587328f54e9401b08798be1f6796a15f8 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 12 May 2020 03:02:05 +0200 Subject: [PATCH 219/373] Replace customWidgetMap API with plugin TaurusForm.{get,set}CustomWidgetMap API is tango-centric and it requires taurus to keep knowledge of a different project (sardana) since it maintains T_FORM_CUSTOM_WIDGET_MAP. Fix this by switching to a plugin-based solution using entry points: - define an entry point group called "taurus.qt.taurusform.item_factories" - other packages (such as sardana) can then register "Taurus Value Factories" for this entrypoint group. - TaurusValue factories are functions that receive a TaurusModel as their only argument and return either a TaurusValue-like instance or None in case the factory does not handle the given model. The old CustomWidgetMap API is still kept for backwards compatibility but it is deprecated and will be removed eventually. --- lib/taurus/qt/qtgui/panel/taurusform.py | 80 ++++++++++++++++++++++--- lib/taurus/tauruscustomsettings.py | 55 ++++++++++------- 2 files changed, 104 insertions(+), 31 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 6b060d960..78170ae96 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -31,6 +31,7 @@ import click from datetime import datetime from functools import partial +import pkg_resources from future.utils import string_types, binary_type @@ -44,8 +45,11 @@ from taurus.qt.qtgui.container import TaurusWidget, TaurusScrollArea from taurus.qt.qtgui.button import QButtonBox, TaurusCommandButton from .taurusmodelchooser import TaurusModelChooser +from taurus.core.util.log import deprecation_decorator +from taurus import warning import taurus.cli.common + __all__ = ["TaurusAttrForm", "TaurusCommandsForm", "TaurusForm"] __docformat__ = 'restructuredtext' @@ -102,6 +106,8 @@ class TaurusForm(TaurusWidget): You can also see some code that exemplifies the use of TaurusForm in :ref:`Taurus coding examples ` ''' + _itemFactories = None + def __init__(self, parent=None, formWidget=None, buttons=None, @@ -114,10 +120,11 @@ def __init__(self, parent=None, if buttons is None: buttons = Qt.QDialogButtonBox.Apply | \ Qt.QDialogButtonBox.Reset - self._customWidgetMap = {} + self._customWidgetMap = {} # deprecated self._model = [] # self._children = [] self.setFormWidget(formWidget) + self._loadItemFactories(force=False) self.setLayout(Qt.QVBoxLayout()) @@ -180,6 +187,38 @@ def __len__(self): '''returns the number of items contained by the form''' return len(self.getItems()) + @classmethod + def _loadItemFactories(cls, force=False): + """ + Loads TaurusValue factories registered via the + 'taurus.qt.taurusform.item_factories' entry point group. + + TaurusValue factories are functions that receive a TaurusModel as + their only argument and return either a TaurusValue-like instance + or None in case the factory does not handle the given model. + + The result is cached at class level in order to avoid re-loading for + each new form. It can be force-reloaded by passing force=True + """ + if cls._itemFactories is not None and not force: + return cls._itemFactories + + cls._itemFactories = {} + for ep in pkg_resources.iter_entry_points( + 'taurus.qt.taurusform.item_factories' + ): + key = ep.name + # allow for more than one plugin registering with same name + i = 2 + while key in cls._itemFactories: + key = "{}({})".format(ep.name, i) + i += 1 + try: + # load the plugin + cls._itemFactories[key] = ep.load() + except: + warning('cannot load item factory "%s"', key) + def _splitModel(self, modelNames): '''convert str to list if needed (commas and whitespace are considered as separators)''' if isinstance(modelNames, binary_type): @@ -189,6 +228,7 @@ def _splitModel(self, modelNames): modelNames = modelNames.split() return modelNames + @deprecation_decorator(alt="TaurusValue factories", rel="4.6.5") def setCustomWidgetMap(self, cwmap): '''Sets a map map for custom widgets. @@ -199,6 +239,7 @@ def setCustomWidgetMap(self, cwmap): # TODO: tango-centric self._customWidgetMap = cwmap + @deprecation_decorator(alt="TaurusValue factories", rel="4.6.5") def getCustomWidgetMap(self): '''Returns the map used to create custom widgets. @@ -312,6 +353,7 @@ def resetModel(self): self.destroyChildren() self._model = [] + @deprecation_decorator(alt="TaurusValue factories", rel="4.6.5") def getFormWidget(self, model=None): '''Returns a tuple that can be used for creating a widget for a given model. @@ -482,11 +524,31 @@ def fillWithChildren(self): for i, model in enumerate(self.getModel()): if not model: continue + try: + model_obj = taurus.Object(model) + except: + self.warning('problem adding item "%s"', model) + self.traceback(level=taurus.Debug) + continue if parent_name: # @todo: Change this (it assumes tango model naming!) model = "%s/%s" % (parent_name, model) - klass, args, kwargs = self.getFormWidget(model=model) - widget = klass(frame, *args, **kwargs) + + widget = None + # check if some item factory handles this model + for n, f in sorted(self._itemFactories.items()): + widget = f(model_obj) + if widget is not None: + self.debug("widget for '%s' provided by '%s'", model, n) + break # todo: consider exploring all to detect conflicts + # bck-compat with old custom widget map + if self._customWidgetMap: + klass, args, kwargs = self.getFormWidget(model=model) + widget = klass(frame, *args, **kwargs) + # no factory handles the model and no custom widgets. Use default + if widget is None: + widget = self._defaultFormWidget() + # @todo UGLY... See if this can be done in other ways... (this causes trouble with widget that need more vertical space , like PoolMotorTV) widget.setMinimumHeight(20) @@ -496,8 +558,7 @@ def fillWithChildren(self): widget.setParent(frame) except: # raise - self.warning( - 'an error occurred while adding the child "%s". Skipping' % model) + self.warning('problem adding item "%s"', model) self.traceback(level=taurus.Debug) try: widget.setModifiableByUser(self.isModifiableByUser()) @@ -1013,7 +1074,7 @@ def test3(): def test4(): - '''tests customwidgetma in taurusforms''' + '''tests old customwidgetmap in taurusforms''' import sys from taurus.qt.qtgui.display import TaurusLabel from taurus.qt.qtgui.application import TaurusApplication @@ -1084,10 +1145,11 @@ def form_cmd(window_name, config_file, models): dialog.addActions( (saveConfigAction, loadConfigAction, quitApplicationAction)) - # set the default map for this installation + # backwards-compat: in case T_FORM_CUSTOM_WIDGET_MAP was manually edited from taurus import tauruscustomsettings - dialog.setCustomWidgetMap( - getattr(tauruscustomsettings, 'T_FORM_CUSTOM_WIDGET_MAP', {})) + cwmap = getattr(tauruscustomsettings, 'T_FORM_CUSTOM_WIDGET_MAP', {}) + if cwmap: + dialog.setCustomWidgetMap(cwmap) # set a model list from the command line or launch the chooser if config_file is not None: diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index 5f0ca8e55..e076b0138 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -30,22 +30,6 @@ aspects of Taurus. """ -#: A map for using custom widgets for certain devices in TaurusForms. It is a -#: dictionary with the following structure: -#: device_class_name:(classname_with_full_module_path, args, kwargs) -#: where the args and kwargs will be passed to the constructor of the class -T_FORM_CUSTOM_WIDGET_MAP = \ - {'SimuMotor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), - 'Motor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), - 'PseudoMotor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), - 'PseudoCounter': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'CTExpChannel': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'ZeroDExpChannel': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'OneDExpChannel': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'TwoDExpChannel': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'IORegister': ('sardana.taurus.qt.qtgui.extra_pool.PoolIORegisterTV', (), {}) - } - #: Compact mode for widgets #: True sets the preferred mode of TaurusForms to use "compact" widgets T_FORM_COMPACT = False @@ -72,17 +56,17 @@ FILTER_OLD_TANGO_EVENTS = True #: Extra Taurus schemes. You can add a list of modules to be loaded for -#: providing support to new schemes +#: providing support to new schemes #: (e.g. EXTRA_SCHEME_MODULES = ['myownschememodule'] EXTRA_SCHEME_MODULES = [] #: Custom formatter. Taurus widgets use a default formatter based on the #: attribute type, but sometimes a custom formatter is needed. #: IMPORTANT: setting this option in this file will affect ALL widgets -#: of ALL applications (which is probably **not** what you want, since it -#: may have unexpected effects in some applications). +#: of ALL applications (which is probably **not** what you want, since it +#: may have unexpected effects in some applications). #: Consider using the API for modifying this on a per-widget or per-class -#: basis at runtime, or using the related `--default-formatter` parameter +#: basis at runtime, or using the related `--default-formatter` parameter #: from TaurusApplication, e.g.: #: $ taurus form MODEL --default-formatter='{:2.3f}' #: The formatter can be a python format string or the name of a formatter @@ -144,8 +128,8 @@ QT_DESIGNER_PATH = None #: Custom organization logo. Set the absolute path to an image file to be used as your -#: organization logo. Qt registered paths can also be used. -#: If not set, it defaults to 'logos:taurus.png" +#: organization logo. Qt registered paths can also be used. +#: If not set, it defaults to 'logos:taurus.png" #: (note that "logos:" is a Qt a registered path for "/qt/qtgui/icon/logos/") ORGANIZATION_LOGO = "logos:taurus.png" @@ -170,3 +154,30 @@ #: be raised instead of logging the message (useful for finding obsolete code) _MAX_DEPRECATIONS_LOGGED = 1 +# ---------------------------------------------------------------------------- +# DEPRECATED SETTINGS +# ---------------------------------------------------------------------------- + +#: DEPRECATED. Use "taurus.qt.taurusform.item_factories" plugin group instead +#: A map for using custom widgets for certain devices in TaurusForms. It is a +#: dictionary with the following structure: +#: device_class_name:(classname_with_full_module_path, args, kwargs) +#: where the args and kwargs will be passed to the constructor of the class +T_FORM_CUSTOM_WIDGET_MAP = {} +OLD_T_FORM_CUSTOM_WIDGET_MAP = { + 'SimuMotor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), + 'Motor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), + 'PseudoMotor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), + 'PseudoCounter': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), + 'CTExpChannel': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), + 'ZeroDExpChannel': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), + 'OneDExpChannel': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), + 'TwoDExpChannel': ('sardana.taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), + 'IORegister': ('sardana.taurus.qt.qtgui.extra_pool.PoolIORegisterTV', (), {}), +} +try: # just for backwards compatibility. This will be removed. + import sardana + if sardana.release.version < '3': + T_FORM_CUSTOM_WIDGET_MAP = OLD_T_FORM_CUSTOM_WIDGET_MAP +except: + pass From 438d122c9b832a20928611226139bd0d6c72393e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 12 May 2020 17:46:42 +0200 Subject: [PATCH 220/373] rename deprecated variable as internal rename OLD_T_FORM_CUSTOM_WIDGET_MAP to _OLD_T_FORM_CUSTOM_WIDGET_MAP in order to make it clear that it should not be used --- lib/taurus/tauruscustomsettings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index e076b0138..3b0e5c98b 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -164,7 +164,7 @@ #: device_class_name:(classname_with_full_module_path, args, kwargs) #: where the args and kwargs will be passed to the constructor of the class T_FORM_CUSTOM_WIDGET_MAP = {} -OLD_T_FORM_CUSTOM_WIDGET_MAP = { +_OLD_T_FORM_CUSTOM_WIDGET_MAP = { 'SimuMotor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), 'Motor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), 'PseudoMotor': ('sardana.taurus.qt.qtgui.extra_pool.PoolMotorTV', (), {}), @@ -178,6 +178,6 @@ try: # just for backwards compatibility. This will be removed. import sardana if sardana.release.version < '3': - T_FORM_CUSTOM_WIDGET_MAP = OLD_T_FORM_CUSTOM_WIDGET_MAP + T_FORM_CUSTOM_WIDGET_MAP = _OLD_T_FORM_CUSTOM_WIDGET_MAP except: pass From 28209bb7d4d0e736ed9d633fc0a09969a4074b8f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 12 May 2020 17:54:41 +0200 Subject: [PATCH 221/373] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4aa8cadd..f79526985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ develop branch) won't be reflected in this file. - Entry-point ("taurus.qt.formatters") for registering formatters via plugins (#1039) - New `worker_cls` argument for `taurus.core.util.ThreadPool` costructor (#1081) - `taurus.core.util.lazymodule` for delayed entry-point loading of modules (#1090) +- `'taurus.qt.taurusform.item_factories'` entry-point (#1108) ### Removed ### Changed @@ -25,9 +26,13 @@ develop branch) won't be reflected in this file. - TaurusForm provides more debugging info when failing to handle a model (#1049) - Improved GUI dialog for changing the formatter of a widget (#1039) - Modules registered with `"taurus.qt.qtgui"` entry-point are now lazy-loaded (#1090) +- The control over which custom widgets are to be used in a TaurusForm is now + done by registering factories to the`'taurus.qt.taurusform.item_factories'` + entry-point (#1108) ### Deprecated - `TaurusBaseWidget.showFormatterDlg()` (#1039) +- TaurusForm custom widget map API (#1108) ### Fixed - Several issues in TaurusWheelEdit (#1010, #1021) From c81a48c8434716bf766e24883ef7e72e80380af1 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 13 May 2020 20:15:48 +0200 Subject: [PATCH 222/373] Use unique docker-compose project names per build When travis launches the tango docker services, the name is the same for all jobs. This might be an issue: https://github.com/taurus-org/taurus/issues/1042#issuecomment-628090032 Use a unique docker project name per job to test if this helps with #1042 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3fdd4a859..350967337 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,7 @@ before_install: - if [[ "$TOXENV" == "flake8" ]]; then echo "Skipping tango initialization"; else - docker-compose -f ci/tango_docker-compose.yml up -d; + docker-compose -p taurus_$TRAVIS_JOB_ID -f ci/tango_docker-compose.yml up -d; echo 'TANGO_HOST=localhost:10000' >> $HOME/.tangorc; fi From 94de61e3b1a1e67357bf2b7179499c72d86ce3fe Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 13 May 2020 23:33:28 +0200 Subject: [PATCH 223/373] Add docker-compose dir from ska-docker Copy docker-compose configurations directory from https://github.com/ska-telescope/ska-docker into taurus/ci/ to use it for CI. This allows us to start tango services (by default based on docker images from SKA) using a makefile. --- ci/docker-compose/.env | 2 + ci/docker-compose/Makefile | 122 +++++++++++++++++++++++++++++ ci/docker-compose/archiver.yml | 67 ++++++++++++++++ ci/docker-compose/astor.yml | 50 ++++++++++++ ci/docker-compose/hdbpp_viewer.yml | 40 ++++++++++ ci/docker-compose/itango.yml | 30 +++++++ ci/docker-compose/jive.yml | 30 +++++++ ci/docker-compose/logviewer.yml | 30 +++++++ ci/docker-compose/pogo.yml | 32 ++++++++ ci/docker-compose/rest.yml | 34 ++++++++ ci/docker-compose/tango.yml | 52 ++++++++++++ ci/docker-compose/tangotest.yml | 28 +++++++ 12 files changed, 517 insertions(+) create mode 100644 ci/docker-compose/.env create mode 100644 ci/docker-compose/Makefile create mode 100644 ci/docker-compose/archiver.yml create mode 100644 ci/docker-compose/astor.yml create mode 100644 ci/docker-compose/hdbpp_viewer.yml create mode 100644 ci/docker-compose/itango.yml create mode 100644 ci/docker-compose/jive.yml create mode 100644 ci/docker-compose/logviewer.yml create mode 100644 ci/docker-compose/pogo.yml create mode 100644 ci/docker-compose/rest.yml create mode 100644 ci/docker-compose/tango.yml create mode 100644 ci/docker-compose/tangotest.yml diff --git a/ci/docker-compose/.env b/ci/docker-compose/.env new file mode 100644 index 000000000..bf415f70a --- /dev/null +++ b/ci/docker-compose/.env @@ -0,0 +1,2 @@ +DOCKER_REGISTRY_HOST=nexus.engageska-portugal.pt +DOCKER_REGISTRY_USER=ska-docker diff --git a/ci/docker-compose/Makefile b/ci/docker-compose/Makefile new file mode 100644 index 000000000..fd09bfb4a --- /dev/null +++ b/ci/docker-compose/Makefile @@ -0,0 +1,122 @@ +# Set dir of Makefile to a variable to use later +MAKEPATH := $(abspath $(lastword $(MAKEFILE_LIST))) +BASEDIR := $(notdir $(patsubst %/,%,$(dir $(MAKEPATH)))) + +COMPOSE_FILES := $(wildcard *.yml) +COMPOSE_FILE_ARGS := $(foreach yml,$(COMPOSE_FILES),-f $(yml)) + +ATTACH_COMPOSE_FILE_ARGS := $(foreach yml,$(filter-out tango.yml,$(COMPOSE_FILES)),-f $(yml)) + +# If the first make argument is "start" or "stop"... +ifeq (start,$(firstword $(MAKECMDGOALS))) + SERVICE_TARGET = true +else ifeq (stop,$(firstword $(MAKECMDGOALS))) + SERVICE_TARGET = true +else ifeq (attach,$(firstword $(MAKECMDGOALS))) + SERVICE_TARGET = true +ifndef NETWORK_MODE +$(error NETWORK_MODE must specify the network to attach to, e.g., make NETWORK_MODE=tangonet-powersupply ...) +endif +ifndef TANGO_HOST +$(error TANGO_HOST must specify the Tango database device, e.g., make TANGO_HOST=powersupply-databaseds:100000 ...) +endif +endif +ifdef SERVICE_TARGET + # .. then use the rest as arguments for the make target + SERVICE := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + # ...and turn them into do-nothing targets + $(eval $(SERVICE):;@:) +endif + +# +# Never use the network=host mode when running CI jobs, and add extra +# distinguishing identifiers to the network name and container names to +# prevent collisions with jobs from the same project running at the same +# time. +# +ifneq ($(CI_JOB_ID),) +NETWORK_MODE := tangonet-$(CI_JOB_ID) +CONTAINER_NAME_PREFIX := $(CI_JOB_ID)- +else +CONTAINER_NAME_PREFIX := +$(info Network mode cannot be host for the archiver! It won't work unless you set the env var CI_JOB_ID=local) +endif + +ifeq ($(OS),Windows_NT) + $(error Sorry, Windows is not supported yet) +else + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Linux) + DISPLAY ?= :0.0 + NETWORK_MODE ?= host + XAUTHORITY_MOUNT := /tmp/.X11-unix:/tmp/.X11-unix + XAUTHORITY ?= /hosthome/.Xauthority + # /bin/sh (=dash) does not evaluate 'docker network' conditionals correctly + SHELL := /bin/bash + endif + ifeq ($(UNAME_S),Darwin) + IF_INTERFACE := $(shell scutil --nwi | grep 'Network interfaces:' | cut -d' ' -f3) + DISPLAY := $(shell scutil --nwi | grep 'address' | cut -d':' -f2 | tr -d ' ' | head -n1):0 + # network_mode = host doesn't work on MacOS, so fix to the internal network + NETWORK_MODE ?= tangonet + XAUTHORITY_MOUNT := $(HOME)/.Xauthority:/hosthome/.Xauthority:ro + XAUTHORITY := /hosthome/.Xauthority + endif +endif + +# +# When running in network=host mode, point devices at a port on the host +# machine rather than at the container. +# +ifeq ($(NETWORK_MODE),host) + TANGO_HOST := $(shell hostname):10000 + MYSQL_HOST := $(shell hostname):3306 +else + TANGO_HOST := $(CONTAINER_NAME_PREFIX)databaseds:10000 + MYSQL_HOST := $(CONTAINER_NAME_PREFIX)tangodb:3306 +endif + +DOCKER_COMPOSE_ARGS := DISPLAY=$(DISPLAY) XAUTHORITY=$(XAUTHORITY) TANGO_HOST=$(TANGO_HOST) \ + NETWORK_MODE=$(NETWORK_MODE) XAUTHORITY_MOUNT=$(XAUTHORITY_MOUNT) MYSQL_HOST=$(MYSQL_HOST) \ + CONTAINER_NAME_PREFIX=$(CONTAINER_NAME_PREFIX) COMPOSE_IGNORE_ORPHANS=true + + +.PHONY: up down minimal start stop status clean pull help +.DEFAULT_GOAL := help + +pull: ## pull the images from the Docker hub + $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) pull + +up: minimal ## start the base TANGO system and prepare all services + $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) up --no-start + +down: ## stop all services and tear down the system + $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) down +ifneq ($(NETWORK_MODE),host) + docker network inspect $(NETWORK_MODE) &> /dev/null && ([ $$? -eq 0 ] && docker network rm $(NETWORK_MODE)) || true +endif + +minimal: ## start the base TANGO system +ifneq ($(NETWORK_MODE),host) + docker network inspect $(NETWORK_MODE) &> /dev/null || ([ $$? -ne 0 ] && docker network create $(NETWORK_MODE)) +endif + $(DOCKER_COMPOSE_ARGS) docker-compose -f tango.yml up -d + +start: up ## start a service (usage: make start ) + $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) start $(SERVICE) + +stop: ## stop a service (usage: make stop ) + $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) stop $(SERVICE) + +attach: ## attach a service to an existing Tango network + $(DOCKER_COMPOSE_ARGS) docker-compose $(ATTACH_COMPOSE_FILE_ARGS) up -d $(SERVICE) + +status: ## show the container status + $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) ps + +clean: down ## clear all TANGO database entries + docker volume rm $(BASEDIR)_tangodb + +help: ## show this help. + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + diff --git a/ci/docker-compose/archiver.yml b/ci/docker-compose/archiver.yml new file mode 100644 index 000000000..f1097b93e --- /dev/null +++ b/ci/docker-compose/archiver.yml @@ -0,0 +1,67 @@ +version: '2' + +services: + maria-db: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/mariadb_hdbpp:latest + container_name: archiver-maria-db + network_mode: ${NETWORK_MODE} + depends_on: + - databaseds + environment: + - MYSQL_ROOT_PASSWORD=secret + - MYSQL_DATABASE=hdbpp + - MYSQL_HOST=archiver-maria-db:3306 + - MYSQL_USER=tango + - MYSQL_PASSWORD=tango + - TANGO_HOST=${TANGO_HOST} + restart: on-failure + + hdbpp-es: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-archiver:latest + network_mode: ${NETWORK_MODE} + container_name: hdbpp-es + depends_on: + - databaseds + - dsconfig + - maria-db + environment: + - TANGO_HOST=${TANGO_HOST} + - HdbManager=archiving/hdbpp/confmanager01 + command: > + /bin/bash -c " + wait-for-it.sh archiver-maria-db:3306 --timeout=30 --strict -- + wait-for-it.sh ${TANGO_HOST} --timeout=30 --strict -- + hdbppes-srv 01" + + hdbpp-cm: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-archiver:latest + network_mode: ${NETWORK_MODE} + container_name: hdbpp-cm + depends_on: + - databaseds + - dsconfig + - maria-db + environment: + - TANGO_HOST=${TANGO_HOST} + - HdbManager=archiving/hdbpp/confmanager01 + command: > + /bin/bash -c " + wait-for-it.sh archiver-maria-db:3306 --timeout=30 --strict -- + wait-for-it.sh ${TANGO_HOST} --timeout=30 --strict -- + hdbppcm-srv 01" + + dsconfig: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-dsconfig:latest + container_name: dsconfig + network_mode: ${NETWORK_MODE} + depends_on: + - databaseds + environment: + - TANGO_HOST=${TANGO_HOST} + command: > + sh -c "wait-for-it.sh ${TANGO_HOST} --timeout=30 --strict -- + json2tango -w -a -u /tango-archiver/data/archiver-devices.json && + sleep infinity" + volumes: + - ../docker/tango/tango-archiver:/tango-archiver + diff --git a/ci/docker-compose/astor.yml b/ci/docker-compose/astor.yml new file mode 100644 index 000000000..7911b66f9 --- /dev/null +++ b/ci/docker-compose/astor.yml @@ -0,0 +1,50 @@ +# +# Docker compose file that launches Astor, sending the display to a remote X11 +# display. +# +# Defines: +# - astor: service that runs Astor in a container +# - container1: example container running Starter device +# +# Requires: +# - tango.yml +# +version: '2' + +services: + astor: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-java:latest + container_name: ${CONTAINER_NAME_PREFIX}astor + network_mode: ${NETWORK_MODE} + volumes: + - ${XAUTHORITY_MOUNT} + environment: + - XAUTHORITY=${XAUTHORITY} + - DISPLAY=${DISPLAY} + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - /usr/local/bin/astor + + starter-example: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-starter:latest + container_name: ${CONTAINER_NAME_PREFIX}starter-example + network_mode: ${NETWORK_MODE} + # set the hostname, otherwise duplicate device registrations result every + # time the hostname changes as the container is restarted. + hostname: starter-example + environment: + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - /usr/bin/supervisord + - --configuration + - /etc/supervisor/supervisord.conf diff --git a/ci/docker-compose/hdbpp_viewer.yml b/ci/docker-compose/hdbpp_viewer.yml new file mode 100644 index 000000000..cf65548b2 --- /dev/null +++ b/ci/docker-compose/hdbpp_viewer.yml @@ -0,0 +1,40 @@ +# +# Docker compose file that launches HdbViewer, sending the display to a remote X11 +# display. +# +# +# Requires: +# - tango.yml +# +version: '2' + +services: + hdbpp-viewer: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/hdbpp_viewer:latest + container_name: ${CONTAINER_NAME_PREFIX}hdbpp-viewer + network_mode: ${NETWORK_MODE} + depends_on: + - databaseds + - dsconfig + - maria-db + - hdbpp-es + - hdbpp-cm + volumes: + - ${XAUTHORITY_MOUNT} + environment: + - XAUTHORITY=${XAUTHORITY} + - DISPLAY=${DISPLAY} + - TANGO_HOST=${TANGO_HOST} + - HDB_TYPE=mysql + - HDB_MYSQL_HOST=archiver-maria-db + - HDB_MYSQL_PORT=3306 + - HDB_USER=tango + - HDB_PASSWORD=tango + - HDB_NAME=hdbpp + - CLASSPATH=JTango.jar:ATKCore.jar:ATKWidget.jar:jhdbviewer.jar:HDBPP.jar:jython.jar:jcalendar.jar + entrypoint: + - wait-for-it.sh + - ${TANGO_HOST} + - --strict + - -- + - ./hdbpp_viewer/hdbpp_viewer_script diff --git a/ci/docker-compose/itango.yml b/ci/docker-compose/itango.yml new file mode 100644 index 000000000..89df69cce --- /dev/null +++ b/ci/docker-compose/itango.yml @@ -0,0 +1,30 @@ +# +# Docker compose file that launches an interactive iTango session. +# +# Connect to the interactive session with 'docker attach itango'. +# Disconnect with the Docker deattach sequence: +

+ +# +# Defines: +# - itango: iTango interactive session +# +# Requires: +# - tango.yml +# +version: '2' + +services: + itango: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-itango:latest + container_name: ${CONTAINER_NAME_PREFIX}itango + network_mode: ${NETWORK_MODE} + environment: + - TANGO_HOST=${TANGO_HOST} + stdin_open: true + tty: true + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - /venv/bin/itango3 diff --git a/ci/docker-compose/jive.yml b/ci/docker-compose/jive.yml new file mode 100644 index 000000000..4a5788209 --- /dev/null +++ b/ci/docker-compose/jive.yml @@ -0,0 +1,30 @@ +# +# Docker compose file that launches Jive, sending output to a remote X11 +# display. +# +# Defines: +# - jive: container running Jive +# +# Requires: +# - tango.yml +# +version: '2' + +services: + jive: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-java:latest + container_name: ${CONTAINER_NAME_PREFIX}jive + network_mode: ${NETWORK_MODE} + volumes: + - ${XAUTHORITY_MOUNT} + environment: + - XAUTHORITY=${XAUTHORITY} + - DISPLAY=${DISPLAY} + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - /usr/local/bin/jive diff --git a/ci/docker-compose/logviewer.yml b/ci/docker-compose/logviewer.yml new file mode 100644 index 000000000..ad1d975ec --- /dev/null +++ b/ci/docker-compose/logviewer.yml @@ -0,0 +1,30 @@ +# +# Docker compose file that launches LogViewer, sending output to a remote X11 +# display. +# +# Defines: +# - logviewer: container running LogViewer +# +# Requires: +# - tango.yml +# +version: '2' + +services: + logviewer: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-java:latest + container_name: ${CONTAINER_NAME_PREFIX}logviewer + network_mode: ${NETWORK_MODE} + volumes: + - ${XAUTHORITY_MOUNT} + environment: + - XAUTHORITY=${XAUTHORITY} + - DISPLAY=${DISPLAY} + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - /usr/local/bin/logviewer diff --git a/ci/docker-compose/pogo.yml b/ci/docker-compose/pogo.yml new file mode 100644 index 000000000..157fab05f --- /dev/null +++ b/ci/docker-compose/pogo.yml @@ -0,0 +1,32 @@ +# +# Docker compose file that launches Pogo, sending the display to a remote X11 +# display. Pogo output can be persisted by writing to either: +# 1. the /home/tango folder, which is a Docker volume persisted between +# container runs; +# 2. the /hosthome folder, which is a r/w mount of your home folder. +# +# Defines: +# - pogo (service): service running Pogo +# - pogo (volume): persistent volume for writing Pogo outut +# +# Requires: +# - N/A +# +version: '2' + +# Create a volume so that Pogo preferences and Pogo output can be persisted +volumes: + pogo: {} + +services: + pogo: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-pogo:latest + container_name: ${CONTAINER_NAME_PREFIX}pogo + network_mode: ${NETWORK_MODE} + volumes: + - pogo:/home/tango + - ${HOME}:/hosthome:rw + - ${XAUTHORITY_MOUNT} + environment: + - XAUTHORITY=${XAUTHORITY} + - DISPLAY=${DISPLAY} diff --git a/ci/docker-compose/rest.yml b/ci/docker-compose/rest.yml new file mode 100644 index 000000000..dfbd154b2 --- /dev/null +++ b/ci/docker-compose/rest.yml @@ -0,0 +1,34 @@ +# +# Docker compose file that launches Astor, sending the display to a remote X11 +# display. +# +# Defines: +# - astor: service that runs Astor in a container +# - container1: example container running Starter device +# +# Requires: +# - tango.yml +# +version: '2' + +services: + rest: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-rest:latest + container_name: ${CONTAINER_NAME_PREFIX}tango-rest + network_mode: ${NETWORK_MODE} + # set the hostname, otherwise duplicate device registrations result every + # time the hostname changes as the container is restarted. + hostname: tango-rest + environment: + - TANGO_HOST=${TANGO_HOST} + ports: + - 8080:8080 + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - /usr/bin/supervisord + - --configuration + - /etc/supervisor/supervisord.conf diff --git a/ci/docker-compose/tango.yml b/ci/docker-compose/tango.yml new file mode 100644 index 000000000..f87788b9c --- /dev/null +++ b/ci/docker-compose/tango.yml @@ -0,0 +1,52 @@ +# +# Docker compose file for TANGO database and database device server +# +# Defines: +# - tangodb: MariaDB database with TANGO schema +# - databaseds: TANGO database device server +# +# Requires: +# - None +# +version: '2' + +volumes: + tangodb: {} + +services: + tangodb: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-db:latest + container_name: ${CONTAINER_NAME_PREFIX}tangodb + network_mode: ${NETWORK_MODE} + environment: + - MYSQL_ROOT_PASSWORD=secret + - MYSQL_DATABASE=tango + - MYSQL_USER=tango + - MYSQL_PASSWORD=tango + volumes: + - tangodb:/var/lib/mysql + restart: on-failure + + databaseds: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-cpp:latest + container_name: ${CONTAINER_NAME_PREFIX}databaseds + network_mode: ${NETWORK_MODE} + depends_on: + - tangodb + environment: + - MYSQL_HOST=${MYSQL_HOST} + - MYSQL_DATABASE=tango + - MYSQL_USER=tango + - MYSQL_PASSWORD=tango + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${MYSQL_HOST} + - --timeout=30 + - --strict + - -- + - /usr/local/bin/DataBaseds + - "2" + - -ORBendPoint + - giop:tcp::10000 + restart: on-failure diff --git a/ci/docker-compose/tangotest.yml b/ci/docker-compose/tangotest.yml new file mode 100644 index 000000000..c2be72477 --- /dev/null +++ b/ci/docker-compose/tangotest.yml @@ -0,0 +1,28 @@ +# +# Docker compose file for TANGO test device server. +# +# Defines: +# - tangotest: TANGO test device server +# +# Requires: +# - tango.yml +# +version: '2' + +services: + tangotest: + image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-java:latest + container_name: ${CONTAINER_NAME_PREFIX}tangotest + network_mode: ${NETWORK_MODE} + environment: + - TANGO_HOST=${TANGO_HOST} + entrypoint: + - /usr/local/bin/wait-for-it.sh + - ${TANGO_HOST} + - --timeout=30 + - --strict + - -- + - /usr/local/bin/TangoTest + - test + restart: on-failure + From 50a7dccf82d799330f7c78865b818caeac52fd7a Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 13 May 2020 23:42:44 +0200 Subject: [PATCH 224/373] Map databaseds port 10000 to host port 10000 Modify tango docker-compose configuration so that the tangods:10000 is mapped as the host's port 10000 This allows CI to use TANGO_HOST=localhost:10000 --- ci/docker-compose/tango.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/docker-compose/tango.yml b/ci/docker-compose/tango.yml index f87788b9c..c3706e8c8 100644 --- a/ci/docker-compose/tango.yml +++ b/ci/docker-compose/tango.yml @@ -31,6 +31,8 @@ services: image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-cpp:latest container_name: ${CONTAINER_NAME_PREFIX}databaseds network_mode: ${NETWORK_MODE} + ports: + - "10000:10000" depends_on: - tangodb environment: From 3cae5ad25f8d3691df04cf4fb7231a40bbb6858d Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 13 May 2020 23:59:14 +0200 Subject: [PATCH 225/373] make Makefile aware of Travis job id Allow usage of TRAVIS_JOB_ID for CI_JOB_ID --- ci/docker-compose/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/docker-compose/Makefile b/ci/docker-compose/Makefile index fd09bfb4a..b086d4e01 100644 --- a/ci/docker-compose/Makefile +++ b/ci/docker-compose/Makefile @@ -34,6 +34,7 @@ endif # prevent collisions with jobs from the same project running at the same # time. # +CI_JOB_ID ?= $(TRAVIS_JOB_ID) ifneq ($(CI_JOB_ID),) NETWORK_MODE := tangonet-$(CI_JOB_ID) CONTAINER_NAME_PREFIX := $(CI_JOB_ID)- From 4b1c8d008cc96cd7ef88915d6c84974c9c549095 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 14 May 2020 00:03:47 +0200 Subject: [PATCH 226/373] Switch to SKA's tango docker images for CI Use SKA's docker-compose configurations instead of the tango-cs docker one to spawn the tango services --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 350967337..c60196b4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,7 @@ before_install: - if [[ "$TOXENV" == "flake8" ]]; then echo "Skipping tango initialization"; else - docker-compose -p taurus_$TRAVIS_JOB_ID -f ci/tango_docker-compose.yml up -d; + make -C ci/docker-compose start tangotest echo 'TANGO_HOST=localhost:10000' >> $HOME/.tangorc; fi From 4b5c81f6790de8a0bcf052d6752586d47719bb5c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 14 May 2020 00:28:02 +0200 Subject: [PATCH 227/373] Remove unneeded docker-compose configs The SKA make file will try to pull all images even if they are not used. Remove the .yml docker-compose files that are not needed for CI. Note: I leave the jive one because it may be useful for for non-CI usage and it shares image with tangotest (thus not increasing the download size). --- ci/docker-compose/archiver.yml | 67 ------------------------------ ci/docker-compose/astor.yml | 50 ---------------------- ci/docker-compose/hdbpp_viewer.yml | 40 ------------------ ci/docker-compose/itango.yml | 30 ------------- ci/docker-compose/logviewer.yml | 30 ------------- ci/docker-compose/pogo.yml | 32 -------------- ci/docker-compose/rest.yml | 34 --------------- 7 files changed, 283 deletions(-) delete mode 100644 ci/docker-compose/archiver.yml delete mode 100644 ci/docker-compose/astor.yml delete mode 100644 ci/docker-compose/hdbpp_viewer.yml delete mode 100644 ci/docker-compose/itango.yml delete mode 100644 ci/docker-compose/logviewer.yml delete mode 100644 ci/docker-compose/pogo.yml delete mode 100644 ci/docker-compose/rest.yml diff --git a/ci/docker-compose/archiver.yml b/ci/docker-compose/archiver.yml deleted file mode 100644 index f1097b93e..000000000 --- a/ci/docker-compose/archiver.yml +++ /dev/null @@ -1,67 +0,0 @@ -version: '2' - -services: - maria-db: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/mariadb_hdbpp:latest - container_name: archiver-maria-db - network_mode: ${NETWORK_MODE} - depends_on: - - databaseds - environment: - - MYSQL_ROOT_PASSWORD=secret - - MYSQL_DATABASE=hdbpp - - MYSQL_HOST=archiver-maria-db:3306 - - MYSQL_USER=tango - - MYSQL_PASSWORD=tango - - TANGO_HOST=${TANGO_HOST} - restart: on-failure - - hdbpp-es: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-archiver:latest - network_mode: ${NETWORK_MODE} - container_name: hdbpp-es - depends_on: - - databaseds - - dsconfig - - maria-db - environment: - - TANGO_HOST=${TANGO_HOST} - - HdbManager=archiving/hdbpp/confmanager01 - command: > - /bin/bash -c " - wait-for-it.sh archiver-maria-db:3306 --timeout=30 --strict -- - wait-for-it.sh ${TANGO_HOST} --timeout=30 --strict -- - hdbppes-srv 01" - - hdbpp-cm: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-archiver:latest - network_mode: ${NETWORK_MODE} - container_name: hdbpp-cm - depends_on: - - databaseds - - dsconfig - - maria-db - environment: - - TANGO_HOST=${TANGO_HOST} - - HdbManager=archiving/hdbpp/confmanager01 - command: > - /bin/bash -c " - wait-for-it.sh archiver-maria-db:3306 --timeout=30 --strict -- - wait-for-it.sh ${TANGO_HOST} --timeout=30 --strict -- - hdbppcm-srv 01" - - dsconfig: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-dsconfig:latest - container_name: dsconfig - network_mode: ${NETWORK_MODE} - depends_on: - - databaseds - environment: - - TANGO_HOST=${TANGO_HOST} - command: > - sh -c "wait-for-it.sh ${TANGO_HOST} --timeout=30 --strict -- - json2tango -w -a -u /tango-archiver/data/archiver-devices.json && - sleep infinity" - volumes: - - ../docker/tango/tango-archiver:/tango-archiver - diff --git a/ci/docker-compose/astor.yml b/ci/docker-compose/astor.yml deleted file mode 100644 index 7911b66f9..000000000 --- a/ci/docker-compose/astor.yml +++ /dev/null @@ -1,50 +0,0 @@ -# -# Docker compose file that launches Astor, sending the display to a remote X11 -# display. -# -# Defines: -# - astor: service that runs Astor in a container -# - container1: example container running Starter device -# -# Requires: -# - tango.yml -# -version: '2' - -services: - astor: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-java:latest - container_name: ${CONTAINER_NAME_PREFIX}astor - network_mode: ${NETWORK_MODE} - volumes: - - ${XAUTHORITY_MOUNT} - environment: - - XAUTHORITY=${XAUTHORITY} - - DISPLAY=${DISPLAY} - - TANGO_HOST=${TANGO_HOST} - entrypoint: - - /usr/local/bin/wait-for-it.sh - - ${TANGO_HOST} - - --timeout=30 - - --strict - - -- - - /usr/local/bin/astor - - starter-example: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-starter:latest - container_name: ${CONTAINER_NAME_PREFIX}starter-example - network_mode: ${NETWORK_MODE} - # set the hostname, otherwise duplicate device registrations result every - # time the hostname changes as the container is restarted. - hostname: starter-example - environment: - - TANGO_HOST=${TANGO_HOST} - entrypoint: - - /usr/local/bin/wait-for-it.sh - - ${TANGO_HOST} - - --timeout=30 - - --strict - - -- - - /usr/bin/supervisord - - --configuration - - /etc/supervisor/supervisord.conf diff --git a/ci/docker-compose/hdbpp_viewer.yml b/ci/docker-compose/hdbpp_viewer.yml deleted file mode 100644 index cf65548b2..000000000 --- a/ci/docker-compose/hdbpp_viewer.yml +++ /dev/null @@ -1,40 +0,0 @@ -# -# Docker compose file that launches HdbViewer, sending the display to a remote X11 -# display. -# -# -# Requires: -# - tango.yml -# -version: '2' - -services: - hdbpp-viewer: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/hdbpp_viewer:latest - container_name: ${CONTAINER_NAME_PREFIX}hdbpp-viewer - network_mode: ${NETWORK_MODE} - depends_on: - - databaseds - - dsconfig - - maria-db - - hdbpp-es - - hdbpp-cm - volumes: - - ${XAUTHORITY_MOUNT} - environment: - - XAUTHORITY=${XAUTHORITY} - - DISPLAY=${DISPLAY} - - TANGO_HOST=${TANGO_HOST} - - HDB_TYPE=mysql - - HDB_MYSQL_HOST=archiver-maria-db - - HDB_MYSQL_PORT=3306 - - HDB_USER=tango - - HDB_PASSWORD=tango - - HDB_NAME=hdbpp - - CLASSPATH=JTango.jar:ATKCore.jar:ATKWidget.jar:jhdbviewer.jar:HDBPP.jar:jython.jar:jcalendar.jar - entrypoint: - - wait-for-it.sh - - ${TANGO_HOST} - - --strict - - -- - - ./hdbpp_viewer/hdbpp_viewer_script diff --git a/ci/docker-compose/itango.yml b/ci/docker-compose/itango.yml deleted file mode 100644 index 89df69cce..000000000 --- a/ci/docker-compose/itango.yml +++ /dev/null @@ -1,30 +0,0 @@ -# -# Docker compose file that launches an interactive iTango session. -# -# Connect to the interactive session with 'docker attach itango'. -# Disconnect with the Docker deattach sequence: +

+ -# -# Defines: -# - itango: iTango interactive session -# -# Requires: -# - tango.yml -# -version: '2' - -services: - itango: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-itango:latest - container_name: ${CONTAINER_NAME_PREFIX}itango - network_mode: ${NETWORK_MODE} - environment: - - TANGO_HOST=${TANGO_HOST} - stdin_open: true - tty: true - entrypoint: - - /usr/local/bin/wait-for-it.sh - - ${TANGO_HOST} - - --timeout=30 - - --strict - - -- - - /venv/bin/itango3 diff --git a/ci/docker-compose/logviewer.yml b/ci/docker-compose/logviewer.yml deleted file mode 100644 index ad1d975ec..000000000 --- a/ci/docker-compose/logviewer.yml +++ /dev/null @@ -1,30 +0,0 @@ -# -# Docker compose file that launches LogViewer, sending output to a remote X11 -# display. -# -# Defines: -# - logviewer: container running LogViewer -# -# Requires: -# - tango.yml -# -version: '2' - -services: - logviewer: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-java:latest - container_name: ${CONTAINER_NAME_PREFIX}logviewer - network_mode: ${NETWORK_MODE} - volumes: - - ${XAUTHORITY_MOUNT} - environment: - - XAUTHORITY=${XAUTHORITY} - - DISPLAY=${DISPLAY} - - TANGO_HOST=${TANGO_HOST} - entrypoint: - - /usr/local/bin/wait-for-it.sh - - ${TANGO_HOST} - - --timeout=30 - - --strict - - -- - - /usr/local/bin/logviewer diff --git a/ci/docker-compose/pogo.yml b/ci/docker-compose/pogo.yml deleted file mode 100644 index 157fab05f..000000000 --- a/ci/docker-compose/pogo.yml +++ /dev/null @@ -1,32 +0,0 @@ -# -# Docker compose file that launches Pogo, sending the display to a remote X11 -# display. Pogo output can be persisted by writing to either: -# 1. the /home/tango folder, which is a Docker volume persisted between -# container runs; -# 2. the /hosthome folder, which is a r/w mount of your home folder. -# -# Defines: -# - pogo (service): service running Pogo -# - pogo (volume): persistent volume for writing Pogo outut -# -# Requires: -# - N/A -# -version: '2' - -# Create a volume so that Pogo preferences and Pogo output can be persisted -volumes: - pogo: {} - -services: - pogo: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-pogo:latest - container_name: ${CONTAINER_NAME_PREFIX}pogo - network_mode: ${NETWORK_MODE} - volumes: - - pogo:/home/tango - - ${HOME}:/hosthome:rw - - ${XAUTHORITY_MOUNT} - environment: - - XAUTHORITY=${XAUTHORITY} - - DISPLAY=${DISPLAY} diff --git a/ci/docker-compose/rest.yml b/ci/docker-compose/rest.yml deleted file mode 100644 index dfbd154b2..000000000 --- a/ci/docker-compose/rest.yml +++ /dev/null @@ -1,34 +0,0 @@ -# -# Docker compose file that launches Astor, sending the display to a remote X11 -# display. -# -# Defines: -# - astor: service that runs Astor in a container -# - container1: example container running Starter device -# -# Requires: -# - tango.yml -# -version: '2' - -services: - rest: - image: ${DOCKER_REGISTRY_HOST}/${DOCKER_REGISTRY_USER}/tango-rest:latest - container_name: ${CONTAINER_NAME_PREFIX}tango-rest - network_mode: ${NETWORK_MODE} - # set the hostname, otherwise duplicate device registrations result every - # time the hostname changes as the container is restarted. - hostname: tango-rest - environment: - - TANGO_HOST=${TANGO_HOST} - ports: - - 8080:8080 - entrypoint: - - /usr/local/bin/wait-for-it.sh - - ${TANGO_HOST} - - --timeout=30 - - --strict - - -- - - /usr/bin/supervisord - - --configuration - - /etc/supervisor/supervisord.conf From 2969fe41cabb436cfb2e9eb5369a64021d8d2acc Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 14 May 2020 00:34:27 +0200 Subject: [PATCH 228/373] Fix typo in travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c60196b4a..7818ad1ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,7 @@ before_install: - if [[ "$TOXENV" == "flake8" ]]; then echo "Skipping tango initialization"; else - make -C ci/docker-compose start tangotest + make -C ci/docker-compose start tangotest; echo 'TANGO_HOST=localhost:10000' >> $HOME/.tangorc; fi From 2cf274e4510ed42e4b93f89cb26244cc98131332 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 14 May 2020 23:36:24 +0200 Subject: [PATCH 229/373] Avoid using tango for TaurusValueTest.test_texts - Split TaurusValueTest into TaurusValueTest and TaurusValueTestTango - Keep the labelCaseSensitivity tests in the Tango class - Move the test_texts to the non-tango testcase, and use a writable eval model instead of the tango one --- .../qt/qtgui/panel/test/test_taurusvalue.py | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py index 3854a5bae..45c80b37e 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py @@ -36,12 +36,8 @@ DEV_NAME = TangoSchemeTestLauncher.DEV_NAME -@insertTest(helper_name="texts", - model="tango:" + DEV_NAME + "/double_scalar", - expected=("double_scalar", "1.23", "0.00", "mm") - ) -class TaurusValueTest(TangoSchemeTestLauncher, BaseWidgetTestCase, - unittest.TestCase): +class TaurusValueTestTango(TangoSchemeTestLauncher, BaseWidgetTestCase, + unittest.TestCase): """ Specific tests for TaurusValue """ @@ -61,23 +57,6 @@ def test_bug126(self): self.assertEqual(label, shownLabel, msg) self.assertMaxDeprecations(1) - @pytest.mark.flaky - def texts(self, model=None, expected=None, fgRole=None, maxdepr=0): - """Checks the texts for scalar attributes""" - self._widget.setModel(model) - if fgRole is not None: - self._widget.setFgRole(fgRole) - self.processEvents(repetitions=10, sleep=.1) - got = (str(self._widget.labelWidget().text()), - str(self._widget.readWidget().text()), - str(self._widget.writeWidget().displayText()), - str(self._widget.unitsWidget().text()), - ) - msg = ('wrong text for "%s":\n expected: %s\n got: %s' % - (model, expected, got)) - self.assertEqual(got, expected, msg) - self.assertMaxDeprecations(maxdepr) - @pytest.mark.flaky def test_labelCaseSensitivity(self): """Verify that case is respected of in the label widget""" @@ -97,5 +76,35 @@ def tearDown(self): TangoSchemeTestLauncher.tearDown(self) unittest.TestCase.tearDown(self) + +@insertTest( + helper_name="texts", + model='eval:@a=taurus.core.evaluation.test.res.mymod.MyClass()/a.foo', + expected=("a.foo", "123", "123", "m") +) +class TaurusValueTest(BaseWidgetTestCase, unittest.TestCase): + """ + Specific tests for TaurusValue + """ + _klass = TaurusValue + + def texts(self, model=None, expected=None, fgRole=None, maxdepr=0): + """Checks the texts for scalar attributes""" + self._widget.setModel(model) + if fgRole is not None: + self._widget.setFgRole(fgRole) + # wait until there is some text in the read widget + self.processEvents(repetitions=20, sleep=.05) + got = (str(self._widget.labelWidget().text()), + str(self._widget.readWidget().text()), + str(self._widget.writeWidget().displayText()), + str(self._widget.unitsWidget().text()), + ) + msg = ('wrong text for "%s":\n expected: %s\n got: %s' % + (model, expected, got)) + self.assertEqual(got, expected, msg) + self.assertMaxDeprecations(maxdepr) + + if __name__ == "__main__": pass From 548bfccfb343fa9cbe5b113fc8568be0c75b92c9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 14 May 2020 23:50:43 +0200 Subject: [PATCH 230/373] Allow failures in py2 tests Quarantine the py2 tests. We are seeing them finishing the test suite ok but showing problems when *exiting* the pytest process (either hanging or crashing) when run on Travis. When run locally the crashes are a lot less frequent. This needs investigation (but we quarantine for now to get some value from the rest of the CI). Note: given that py2 support may be dropped soon, this quarantine may be effective until removal. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7818ad1ce..a388833a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,8 @@ matrix: - env: TOXENV=py37-ps2 - env: TOXENV=py37-qt5-docs allow_failures: + - env: TOXENV=py27-qt4 + - env: TOXENV=py27-qt5 - env: TOXENV=flake8 before_install: From 7712f37c79e6450676317157ef67739e785f57f3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 01:00:22 +0200 Subject: [PATCH 231/373] Remove old docker-compose config Remove docker-compose configuration that used the tangocs-docker images --- ci/tango_docker-compose.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 ci/tango_docker-compose.yml diff --git a/ci/tango_docker-compose.yml b/ci/tango_docker-compose.yml deleted file mode 100644 index 313924f5c..000000000 --- a/ci/tango_docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: '2' -services: - tango-db: - image: tangocs/mysql:9.2.2 - ports: - - "9999:3306" - environment: - - MYSQL_ROOT_PASSWORD=root - tango-cs: - image: tangocs/tango-cs:9.3.2-alpha.1-no-tango-test - ports: - - "10000:10000" - environment: - - TANGO_HOST=localhost:10000 - - MYSQL_HOST=tango-db:3306 - - MYSQL_USER=tango - - MYSQL_PASSWORD=tango - - MYSQL_DATABASE=tango - links: - - "tango-db:localhost" - depends_on: - - tango-db - tango-test: - image: tangocs/tango-test:9.3.3-rc1 - environment: - - TANGO_HOST=tango-cs:10000 - links: - - "tango-cs:localhost" - depends_on: - - tango-cs From 78b2495654a4720b02ddf9d1f7eef4d1ccc1d3a2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 01:00:44 +0200 Subject: [PATCH 232/373] Update Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4aa8cadd..6de0fdbba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ develop branch) won't be reflected in this file. - Exception in DelayedSubscriber (#1030) - Compatibility issue in deprecated TangoAttribute's `isScalar()` and `isSpectrum()` (#1034) - Some issues in taurus v3 to v4 migration support (#1059) -- Some CI test issues (#1075, #1069) +- Some CI test issues (#1075, #1069, #1109) ## [4.6.1] - 2019-08-19 Hotfix for auto-deployment in PyPI with Travis. No other difference from 4.6.0. From b17a994950f7d5ab8ddbf8507219360f9bc1fc60 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 15:44:33 +0200 Subject: [PATCH 233/373] Refactor form item factories API Add TaurusForm.{select,get}ItemFactories() and delay loading of factory entry points --- lib/taurus/core/util/test/test_lazymodule.py | 1 - lib/taurus/qt/qtgui/panel/taurusform.py | 83 ++++++++++++++++---- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/lib/taurus/core/util/test/test_lazymodule.py b/lib/taurus/core/util/test/test_lazymodule.py index 730c5f47a..a8e1e2d28 100644 --- a/lib/taurus/core/util/test/test_lazymodule.py +++ b/lib/taurus/core/util/test/test_lazymodule.py @@ -1,5 +1,4 @@ import sys -import pkg_resources from types import ModuleType from taurus.core.util.lazymodule import LazyModule diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 78170ae96..fa283830f 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -32,6 +32,7 @@ from datetime import datetime from functools import partial import pkg_resources +import re from future.utils import string_types, binary_type @@ -106,7 +107,7 @@ class TaurusForm(TaurusWidget): You can also see some code that exemplifies the use of TaurusForm in :ref:`Taurus coding examples ` ''' - _itemFactories = None + _allItemFactories = None def __init__(self, parent=None, formWidget=None, @@ -124,7 +125,9 @@ def __init__(self, parent=None, self._model = [] # self._children = [] self.setFormWidget(formWidget) - self._loadItemFactories(force=False) + + self._itemFactories = [] + self.selectItemFactories() self.setLayout(Qt.QVBoxLayout()) @@ -188,7 +191,7 @@ def __len__(self): return len(self.getItems()) @classmethod - def _loadItemFactories(cls, force=False): + def _getAllItemFactories(cls, force=False): """ Loads TaurusValue factories registered via the 'taurus.qt.taurusform.item_factories' entry point group. @@ -200,24 +203,69 @@ def _loadItemFactories(cls, force=False): The result is cached at class level in order to avoid re-loading for each new form. It can be force-reloaded by passing force=True """ - if cls._itemFactories is not None and not force: - return cls._itemFactories + if cls._allItemFactories is not None and not force: + return cls._allItemFactories - cls._itemFactories = {} + cls._allItemFactories = {} for ep in pkg_resources.iter_entry_points( 'taurus.qt.taurusform.item_factories' ): key = ep.name - # allow for more than one plugin registering with same name + # allow (but warn about) duplicated registered names i = 2 - while key in cls._itemFactories: + while key in cls._allItemFactories: key = "{}({})".format(ep.name, i) i += 1 - try: - # load the plugin - cls._itemFactories[key] = ep.load() - except: - warning('cannot load item factory "%s"', key) + warning("Duplicated form item factory name --> '%s'", key) + cls._allItemFactories[key] = ep + + def selectItemFactories(self, included=('.*',), excluded=()): + """ + Select which factories will be used to crete the form's items. + The factories are selected using the names registered in the + 'taurus.qt.taurusform.item_factories' entry point group. + + The selection is done by regex matching on the names. First the names + that match any of the patterns in `excluded` are discarded. Then + each pattern in `included` is used to select from the names. + + In this way, the factories will be selected in the order dictated by + the `included` pattern list. If a pattern matches several names, these + will be sorted alphabetically. + + For example, if there are the following registered factory names: + - names = ['foo1', 'foo2', 'baz1', 'baz2, 'bar1', 'bar2'] + And we use `exclude=("foo2",)` and `include=("bar2", "b.*1", "f.*")` , + then the selection will be: ["bar2","bar1","baz1","foo1"] + + :param included: iterable of regexps patterns (str or re.Pattern) + :param excluded: iterable of regexps patterns (str or re.Pattern) + """ + self._itemFactories = [] + all = self._getAllItemFactories() + + # filter out the factory keys matching a exclude pattern + remaining = all.keys() + for p in excluded: + remaining = [k for k in remaining if not re.match(p, k)] + + #sort the remaining keys alphabetically + remaining.sort() + + # fill the _itemFactories list with the selected entry points, + # sorting according to the order in included (and then alphabetically) + for p in included: + keys = remaining + remaining = [] + for k in keys: + if re.match(p, k): + self._itemFactories.append(all[k]) + else: + remaining.append(k) + + def getItemFactories(self): + """returns the list of item factories entry points currently in use""" + return self._itemFactories def _splitModel(self, modelNames): '''convert str to list if needed (commas and whitespace are considered as separators)''' @@ -536,7 +584,14 @@ def fillWithChildren(self): widget = None # check if some item factory handles this model - for n, f in sorted(self._itemFactories.items()): + for n, ep in sorted(self._itemFactories.items()): + try: + # load the plugin + f = ep.load() + except: + warning('cannot load item factory "%s"', ep.name) + continue + widget = f(model_obj) if widget is not None: self.debug("widget for '%s' provided by '%s'", model, n) From c44a530ccc8a781a4f47a05f66ee63682915fe8e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 17:45:19 +0200 Subject: [PATCH 234/373] m, Add missing license to lazymodule.py --- lib/taurus/core/util/test/test_lazymodule.py | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/taurus/core/util/test/test_lazymodule.py b/lib/taurus/core/util/test/test_lazymodule.py index a8e1e2d28..60c8f2a98 100644 --- a/lib/taurus/core/util/test/test_lazymodule.py +++ b/lib/taurus/core/util/test/test_lazymodule.py @@ -1,3 +1,28 @@ + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + + import sys from types import ModuleType from taurus.core.util.lazymodule import LazyModule From 4362de368036d55a2423c7aea014bdf5f1027236 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 19:38:35 +0200 Subject: [PATCH 235/373] Refactor the item factories API (again) Simplify the TaurusForm item factory selection API and add a generic plugin selection function: taurus.core.util.plugin.selectEntryPoints --- lib/taurus/core/util/plugin.py | 85 ++++++++++++++++++++++++ lib/taurus/qt/qtgui/panel/taurusform.py | 87 +++++++------------------ 2 files changed, 108 insertions(+), 64 deletions(-) create mode 100644 lib/taurus/core/util/plugin.py diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py new file mode 100644 index 000000000..a376ac36c --- /dev/null +++ b/lib/taurus/core/util/plugin.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +""" +Utilities for plugin loading and selection +""" + +import re +import pkg_resources + +def selectEntryPoints(group=None, include=('.*',), exclude=()): + """ + Selects and prioritizes entry points from an entry point group. + + The entry points are selected using their name. + + The selection is done by regex matching on the names: first the entry + points whose name matches any of the patterns in `excluded` are discarded; + then each pattern in `included` is used to select from the names of the + remaining entry points (from highest to lowest priority). + + In this way, the entry points are selected in the order dictated by + the `included` pattern list. If a pattern matches several names, these + will be sorted alphabetically. If a name is matched by several patterns, + it is only selected byt the first pattern. + + For example, if there are the following registered entry point names: + ['foo1', 'foo2', 'baz1', 'baz2', 'bar1', 'bar2'] + And we use `exclude=("foo2",)` and `include=("bar2", "b.*1", "f.*")` , + then the selection will be: ["bar2","bar1","baz1","foo1"] + + :param: group: (`str`) entry point group name from which the entry points + are obtained. + :param include: (tuple of `str` or `re.Pattern`). Regexp patterns for + names to be included in the selection. Default is `(".*",)`, which matches + all registered names and the sort is purely alphabetical. + :param exclude: (tuple of `str` or `re.Pattern`). Regexp patterns for + names to be excluded. Default is `()`, so no entry point is excluded. + """ + ret = [] + + remaining = list(pkg_resources.iter_entry_points(group)) + + # filter out the entry points whose name matches a exclude pattern + for p in exclude: + remaining = [e for e in remaining if not re.match(p, e.name)] + + # sort the remaining entry points alphabetically + remaining.sort(key=lambda e: e.name) + + # fill the ret list with the entry points whose name matches a pattern in + # the `include` tuple. The inclusion order follows the order of the + # patterns in `include` (and alphabetically for a pattern that produces + # multiple matches) + for p in include: + tmp = remaining + remaining = [] + for e in tmp: + if re.match(p, e.name): + ret.append(e) + else: + remaining.append(e) + return ret diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index fa283830f..adcd3b6bd 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -47,6 +47,7 @@ from taurus.qt.qtgui.button import QButtonBox, TaurusCommandButton from .taurusmodelchooser import TaurusModelChooser from taurus.core.util.log import deprecation_decorator +from taurus.core.util.plugin import selectEntryPoints from taurus import warning import taurus.cli.common @@ -107,8 +108,6 @@ class TaurusForm(TaurusWidget): You can also see some code that exemplifies the use of TaurusForm in :ref:`Taurus coding examples ` ''' - _allItemFactories = None - def __init__(self, parent=None, formWidget=None, buttons=None, @@ -190,78 +189,38 @@ def __len__(self): '''returns the number of items contained by the form''' return len(self.getItems()) - @classmethod - def _getAllItemFactories(cls, force=False): + def selectItemFactories(self, include=('.*',), exclude=()): """ - Loads TaurusValue factories registered via the - 'taurus.qt.taurusform.item_factories' entry point group. + Selects and prioritizes the factories to be used to create the form's + items. TaurusValue factories are functions that receive a TaurusModel as their only argument and return either a TaurusValue-like instance or None in case the factory does not handle the given model. - The result is cached at class level in order to avoid re-loading for - each new form. It can be force-reloaded by passing force=True - """ - if cls._allItemFactories is not None and not force: - return cls._allItemFactories - - cls._allItemFactories = {} - for ep in pkg_resources.iter_entry_points( - 'taurus.qt.taurusform.item_factories' - ): - key = ep.name - # allow (but warn about) duplicated registered names - i = 2 - while key in cls._allItemFactories: - key = "{}({})".format(ep.name, i) - i += 1 - warning("Duplicated form item factory name --> '%s'", key) - cls._allItemFactories[key] = ep - - def selectItemFactories(self, included=('.*',), excluded=()): - """ - Select which factories will be used to crete the form's items. - The factories are selected using the names registered in the - 'taurus.qt.taurusform.item_factories' entry point group. - - The selection is done by regex matching on the names. First the names - that match any of the patterns in `excluded` are discarded. Then - each pattern in `included` is used to select from the names. + The factories are selected using their entry point names as registered + in the "taurus.qt.taurusform.item_factories" entry point group. - In this way, the factories will be selected in the order dictated by - the `included` pattern list. If a pattern matches several names, these - will be sorted alphabetically. + The factories entry point name is up to the registrar of the entry + point (typically a taurus plugin) and should be documented by the + registrar to allow for selection and prioritization. - For example, if there are the following registered factory names: - - names = ['foo1', 'foo2', 'baz1', 'baz2, 'bar1', 'bar2'] - And we use `exclude=("foo2",)` and `include=("bar2", "b.*1", "f.*")` , - then the selection will be: ["bar2","bar1","baz1","foo1"] + The selection and prioritization is done using + :function:`taurus.core.util.plugin.selectEntryPoints()`. See it for + more details. - :param included: iterable of regexps patterns (str or re.Pattern) - :param excluded: iterable of regexps patterns (str or re.Pattern) + :param include: (tuple of `str` or `re.Pattern`). Regexp patterns for + names to be included in the selection. Default is `(".*",)`, which + matches all registered names and the sort is purely alphabetical. + :param exclude: (tuple of `str` or `re.Pattern`). Regexp patterns for + names to be excluded. Default is `()`, so no entry point is exclude. """ - self._itemFactories = [] - all = self._getAllItemFactories() - - # filter out the factory keys matching a exclude pattern - remaining = all.keys() - for p in excluded: - remaining = [k for k in remaining if not re.match(p, k)] - - #sort the remaining keys alphabetically - remaining.sort() - - # fill the _itemFactories list with the selected entry points, - # sorting according to the order in included (and then alphabetically) - for p in included: - keys = remaining - remaining = [] - for k in keys: - if re.match(p, k): - self._itemFactories.append(all[k]) - else: - remaining.append(k) + + self._itemFactories = selectEntryPoints( + group='taurus.qt.taurusform.item_factories', + include=include, + exclude=exclude + ) def getItemFactories(self): """returns the list of item factories entry points currently in use""" From 59028bed0272ae27a007e6d100afb983745173ae Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 19:39:11 +0200 Subject: [PATCH 236/373] Add unit test for selectEntryPoints - add mock_entry_point utility - add test_Plugin unit test --- lib/taurus/core/util/test/test_plugin.py | 100 +++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 lib/taurus/core/util/test/test_plugin.py diff --git a/lib/taurus/core/util/test/test_plugin.py b/lib/taurus/core/util/test/test_plugin.py new file mode 100644 index 000000000..f983a8d6b --- /dev/null +++ b/lib/taurus/core/util/test/test_plugin.py @@ -0,0 +1,100 @@ +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +from taurus.core.util.plugin import selectEntryPoints + +import pkg_resources +import random +import string + + +def _random_string(length, chars=string.ascii_lowercase, prefix='', suffix=''): + """ + Return a random string with given length, prefix and suffix. + The random characters are chosen from the chars iterable (lowercase + letters by default) + """ + r = ''.join(random.choice(chars) for i in range(length)) + return prefix + r + suffix + + +def mock_entry_point(lines, group=None, dist_name=None): + """ + Registers a fake distribution that advertises a group of entry points + (defined by the `lines argument as a sequence of parseable strings). + The group name is give by the `group` argument (or generated + randomly if not given). The fake distribution name is given by the + `dist_name` argument (or generated randomly if not given). + + The function registers (so they are discoverable, e.g. with + :method:`pkg_resources.iter_entry_points`) and also returns the mapping + dictionary. + """ + if group is None: + group = _random_string(8, prefix='dummygroup_') + if dist_name is None: + dist_name = _random_string(8, prefix='dummydist_') + + # create a dummy pkg_resources distribution based on a clone of taurus + w = pkg_resources.working_set + d = w.find(pkg_resources.Requirement.parse('taurus')) + d = d.clone(project_name=dist_name) + w.add(d, dist_name) + + # create a fake entry point mapping (with group name testgroup)) + mapping = pkg_resources.EntryPoint.parse_map({group: lines}, dist=d) + + # add mapping to the fake distro so that it can be discovered + d._ep_map = mapping + + # return the mapping + return mapping + + +def test_Plugin(): + + # create a fake entry point mapping (with group name testgroup)) + group = _random_string(8, prefix='dummygroup_') + names = ['foo1', 'foo2', 'baz1', 'baz2', 'bar1', 'bar2'] + mock_entry_point( + lines=["{0} = obj_{0}".format(n) for n in names], + group=group + ) + + # check the selectEntryPoints function + all = selectEntryPoints(group) + assert isinstance(all[0], pkg_resources.EntryPoint) + assert [ep.name for ep in all] == sorted(names) + + none_included = selectEntryPoints(group, include=()) + assert len(none_included) == 0 + + all_excluded = selectEntryPoints(group, exclude=('.*',)) + assert len(all_excluded) == 0 + + some = selectEntryPoints( + group, + exclude=("foo2",), + include=("bar2", "b.*1", "f.*") + ) + assert [ep.name for ep in some] == ["bar2", "bar1", "baz1", "foo1"] From 695da9ca703578079fdd9673c3ba1f092faff688 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 23:38:58 +0200 Subject: [PATCH 237/373] Add tests for TaurusForm's itemFactory - Add unit test for for the itemFactory selection API - Add functional test for the itemFactory usage --- .../qt/qtgui/panel/test/test_taurusform.py | 91 ++++++++++++++++--- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index 510ec58e0..0a7f23c21 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -26,16 +26,16 @@ import unittest from taurus.qt.qtgui.test import GenericWidgetTestCase -from taurus.qt.qtgui.panel import TaurusForm, TaurusAttrForm +from taurus.qt.qtgui.panel import TaurusForm, TaurusAttrForm, TaurusValue +from taurus.core.util.test.test_plugin import mock_entry_point class TaurusFormTest(GenericWidgetTestCase, unittest.TestCase): - - ''' + """ Generic tests for TaurusForm widget. .. seealso: :class:`taurus.qt.qtgui.test.base.GenericWidgetTestCase` - ''' + """ _klass = TaurusForm modelnames = [['sys/tg_test/1'], ['sys/tg_test/1/wave'], @@ -55,17 +55,86 @@ class TaurusFormTest(GenericWidgetTestCase, unittest.TestCase): class TaurusAttrFormTest(GenericWidgetTestCase, unittest.TestCase): - - ''' + """ Generic tests for TaurusAttrForm widget. .. seealso: :class:`taurus.qt.qtgui.test.base.GenericWidgetTestCase` - ''' + """ _klass = TaurusAttrForm modelnames = ['sys/tg_test/1', None] -# if __name__ == "__main__": -# unittest.main() -# suite = unittest.defaultTestLoader.loadTestsFromTestCase(TaurusFormTest) -# unittest.TextTestRunner(verbosity=2).run(suite) +class _DummyTV(TaurusValue): + pass + + +def _DummyItemFactory(m): + """ + A dummy item factory that returns _DummyTV instance for one specific + attribute: "eval://localhost/@dummy/'test_itemfactory'" + """ + if m.fullname == "eval://localhost/@dummy/'test_itemfactory'": + return _DummyTV() + + +def test_form_itemFactory(): + """Checks that the TaurusForm itemFactory API works""" + lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] + group = "taurus.qt.taurusform.item_factories" + mock_entry_point(lines, group=group) + + from taurus.qt.qtgui.application import TaurusApplication + app = TaurusApplication.instance() + if app is None: + _ = TaurusApplication([], cmd_line_parser=None) + + w = TaurusForm() + w.setModel( + [ + "eval://localhost/@dummy/'test_itemfactory'", + "eval://localhost/@dummy/'test_itemfactory2'", + ] + ) + # The first item should get a customized _DummyTV widget + assert type(w[0]) is _DummyTV + # The second item shoud get the default form widget + assert type(w[1]) is w._defaultFormWidget + + +def test_form_itemFactory_selection(): + """Checks that the TaurusForm itemFactory selection API works""" + lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] + group = "taurus.qt.taurusform.item_factories" + mapping = mock_entry_point(lines, group=group) + ep1 = mapping[group]["test_Form_ItemFactory"] + + from taurus.qt.qtgui.application import TaurusApplication + app = TaurusApplication.instance() + if app is None: + _ = TaurusApplication([], cmd_line_parser=None) + + w = TaurusForm() + + # the test_Form_ItemFactory should be in the default factories + default_factories = w.getItemFactories() + assert ep1 in default_factories + + # Check that we can deselect all factories + no_factories = w.selectItemFactories(include=[]) + assert no_factories == [] + + # Check that we can exclude everything except test_Form_ItemFactory + select1 = w.selectItemFactories(exclude=[r"(?!.*test_Form_ItemFactory).*"]) + assert select1 == [ep1] + + # Check that we can include only test_Form_ItemFactory + select2 = w.selectItemFactories(include=["test_Form_ItemFactory"]) + assert select2 == [ep1] + + # Check that the selected test_Form_ItemFactory is an entry point + from pkg_resources import EntryPoint + assert type(select2[0]) == EntryPoint + + # Check that the selected entry point loads _DummyItemFactory + assert select2[0].load() is _DummyItemFactory + From 8e6d92ac4ed76fc55134ea6ef035fd295e30edec Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 23:41:34 +0200 Subject: [PATCH 238/373] Make selectItemFactories return the selection TaurusForm.selectItemFactories() just sets the selection but does not return it. Make it return the selection for convenience. --- lib/taurus/qt/qtgui/panel/taurusform.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index adcd3b6bd..8c07c80c5 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -209,11 +209,14 @@ def selectItemFactories(self, include=('.*',), exclude=()): :function:`taurus.core.util.plugin.selectEntryPoints()`. See it for more details. + The selected list is updated in the form, and returned. + :param include: (tuple of `str` or `re.Pattern`). Regexp patterns for names to be included in the selection. Default is `(".*",)`, which matches all registered names and the sort is purely alphabetical. :param exclude: (tuple of `str` or `re.Pattern`). Regexp patterns for names to be excluded. Default is `()`, so no entry point is exclude. + :return: (list) selected item factories entry points """ self._itemFactories = selectEntryPoints( @@ -221,6 +224,7 @@ def selectItemFactories(self, include=('.*',), exclude=()): include=include, exclude=exclude ) + return self._itemFactories def getItemFactories(self): """returns the list of item factories entry points currently in use""" From 95ea960ef9a3e0ea8a2b15a37643b6fcd6b708fb Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 15 May 2020 23:43:28 +0200 Subject: [PATCH 239/373] Fix bug in TaurusForm.fillWithChildren The last refactoring left a couple of issues (detected in the tests). Fix them. --- lib/taurus/qt/qtgui/panel/taurusform.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 8c07c80c5..f1b85e417 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -547,7 +547,7 @@ def fillWithChildren(self): widget = None # check if some item factory handles this model - for n, ep in sorted(self._itemFactories.items()): + for ep in self._itemFactories: try: # load the plugin f = ep.load() @@ -557,7 +557,11 @@ def fillWithChildren(self): widget = f(model_obj) if widget is not None: - self.debug("widget for '%s' provided by '%s'", model, n) + self.debug( + "widget for '%s' provided by '%s'", + model, + ep.name + ) break # todo: consider exploring all to detect conflicts # bck-compat with old custom widget map if self._customWidgetMap: From 9cb669a6cecfaba1468df7bed6616cf8e6e17c76 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 12:50:35 +0200 Subject: [PATCH 240/373] Require full match for patterns in selectEntryPoints The pattern matching in selectEntryPoints used re.match, which matches if the beginning of the name matches. Instead we want a match only if the whole name matches. Therefore replace the usage of re.match() by re.fullmatch() --- lib/taurus/core/util/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py index a376ac36c..cbffd018d 100644 --- a/lib/taurus/core/util/plugin.py +++ b/lib/taurus/core/util/plugin.py @@ -78,7 +78,7 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): tmp = remaining remaining = [] for e in tmp: - if re.match(p, e.name): + if re.fullmatch(p, e.name): ret.append(e) else: remaining.append(e) From 153bcd14374bcdbf5d057ad01ea07a9242708e77 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 15:15:10 +0200 Subject: [PATCH 241/373] Use unique name for plugin test test_form_itemFactory_selection registers the same name as test_form_itemFactory for its resgistration of a dummy item factory. This, together with the fact that these tests do not cleanup (because I found no way of safely reset the global pkg_resources working set), produces failures when both tests are run in the same process. Work around by using a different name --- lib/taurus/qt/qtgui/panel/test/test_taurusform.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index 0a7f23c21..05ab7ffc4 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -103,10 +103,10 @@ def test_form_itemFactory(): def test_form_itemFactory_selection(): """Checks that the TaurusForm itemFactory selection API works""" - lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] + lines = ["test_Form_ItemFactorySel={}:_DummyItemFactory".format(__name__)] group = "taurus.qt.taurusform.item_factories" mapping = mock_entry_point(lines, group=group) - ep1 = mapping[group]["test_Form_ItemFactory"] + ep1 = mapping[group]["test_Form_ItemFactorySel"] from taurus.qt.qtgui.application import TaurusApplication app = TaurusApplication.instance() @@ -124,11 +124,13 @@ def test_form_itemFactory_selection(): assert no_factories == [] # Check that we can exclude everything except test_Form_ItemFactory - select1 = w.selectItemFactories(exclude=[r"(?!.*test_Form_ItemFactory).*"]) + select1 = w.selectItemFactories( + exclude=[r"(?!.*test_Form_ItemFactorySel).*"] + ) assert select1 == [ep1] # Check that we can include only test_Form_ItemFactory - select2 = w.selectItemFactories(include=["test_Form_ItemFactory"]) + select2 = w.selectItemFactories(include=["test_Form_ItemFactorySel"]) assert select2 == [ep1] # Check that the selected test_Form_ItemFactory is an entry point @@ -137,4 +139,3 @@ def test_form_itemFactory_selection(): # Check that the selected entry point loads _DummyItemFactory assert select2[0].load() is _DummyItemFactory - From 07235d9e41c251c77d496a6d6414192966805006 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 17:41:33 +0200 Subject: [PATCH 242/373] m, doc, Fix typo in docstring --- lib/taurus/core/util/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py index cbffd018d..2f228bc74 100644 --- a/lib/taurus/core/util/plugin.py +++ b/lib/taurus/core/util/plugin.py @@ -51,12 +51,12 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): And we use `exclude=("foo2",)` and `include=("bar2", "b.*1", "f.*")` , then the selection will be: ["bar2","bar1","baz1","foo1"] - :param: group: (`str`) entry point group name from which the entry points + :param group: (`str`) entry point group name from which the entry points are obtained. - :param include: (tuple of `str` or `re.Pattern`). Regexp patterns for + :param include: (`tuple` of `str` or `re.Pattern`). Regexp patterns for names to be included in the selection. Default is `(".*",)`, which matches all registered names and the sort is purely alphabetical. - :param exclude: (tuple of `str` or `re.Pattern`). Regexp patterns for + :param exclude: (`tuple` of `str` or `re.Pattern`). Regexp patterns for names to be excluded. Default is `()`, so no entry point is excluded. """ ret = [] From 6d01bd8306a056dd0c4582c5cedcbd287cad2611 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 18:59:02 +0200 Subject: [PATCH 243/373] m, doc, Fix typo in docstring --- lib/taurus/core/util/plugin.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py index 2f228bc74..158bcd9b2 100644 --- a/lib/taurus/core/util/plugin.py +++ b/lib/taurus/core/util/plugin.py @@ -27,9 +27,12 @@ Utilities for plugin loading and selection """ +__all__ = ["selectEntryPoints"] + import re import pkg_resources + def selectEntryPoints(group=None, include=('.*',), exclude=()): """ Selects and prioritizes entry points from an entry point group. @@ -52,12 +55,12 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): then the selection will be: ["bar2","bar1","baz1","foo1"] :param group: (`str`) entry point group name from which the entry points - are obtained. + are obtained. :param include: (`tuple` of `str` or `re.Pattern`). Regexp patterns for - names to be included in the selection. Default is `(".*",)`, which matches - all registered names and the sort is purely alphabetical. + names to be included in the selection. Default is `(".*",)`, which + matches all registered names and the sort is purely alphabetical. :param exclude: (`tuple` of `str` or `re.Pattern`). Regexp patterns for - names to be excluded. Default is `()`, so no entry point is excluded. + names to be excluded. Default is `()`, so no entry point is excluded. """ ret = [] From 5f1c65781bba8d8ef862a8fc18201dd91e43c885 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 19:17:23 +0200 Subject: [PATCH 244/373] Allow any case in CLI --log option The log_level parameter to the taurus --log option was case-insensitive before we introduced click-based CLI, and changed to case-sensitive since then. Make it insensitive again. --- lib/taurus/cli/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index a4e88be10..b0c3fe32c 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -88,7 +88,7 @@ def cmd2(model, serial_mode, poll_period, default_formatter): log_level = click.option( '--log-level', 'log_level', type=click.Choice(['Critical', 'Error', 'Warning', 'Info', - 'Debug', 'Trace']), + 'Debug', 'Trace'], case_sensitive=False), default='Info', show_default=True, help='Show only logs with priority LEVEL or above', ) From e43b20cfe81c4e9b0e405fd37248e80bffa59c0e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 19:37:18 +0200 Subject: [PATCH 245/373] m, doc, Fix typo in docstring --- lib/taurus/core/util/plugin.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py index 158bcd9b2..abe7140cc 100644 --- a/lib/taurus/core/util/plugin.py +++ b/lib/taurus/core/util/plugin.py @@ -54,13 +54,15 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): And we use `exclude=("foo2",)` and `include=("bar2", "b.*1", "f.*")` , then the selection will be: ["bar2","bar1","baz1","foo1"] - :param group: (`str`) entry point group name from which the entry points - are obtained. + :param group: (str) entry point group name from which the entry points + are obtained. :param include: (`tuple` of `str` or `re.Pattern`). Regexp patterns for - names to be included in the selection. Default is `(".*",)`, which - matches all registered names and the sort is purely alphabetical. + names to be included in the selection. Default is + `(".*",)`, which matches all registered names and the sort + is purely alphabetical. :param exclude: (`tuple` of `str` or `re.Pattern`). Regexp patterns for - names to be excluded. Default is `()`, so no entry point is excluded. + names to be excluded. Default is `()`, so no entry point + is excluded. """ ret = [] From db4c8876e376714e008adf801c54bd20cdbf9266 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 20:01:14 +0200 Subject: [PATCH 246/373] m, doc, Fix typo in docstring --- lib/taurus/qt/qtgui/panel/taurusform.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index f1b85e417..dd0006dcc 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -206,16 +206,18 @@ def selectItemFactories(self, include=('.*',), exclude=()): registrar to allow for selection and prioritization. The selection and prioritization is done using - :function:`taurus.core.util.plugin.selectEntryPoints()`. See it for + :meth:`taurus.core.util.plugin.selectEntryPoints()`. See it for more details. The selected list is updated in the form, and returned. :param include: (tuple of `str` or `re.Pattern`). Regexp patterns for - names to be included in the selection. Default is `(".*",)`, which - matches all registered names and the sort is purely alphabetical. + names to be included in the selection. Default is + `(".*",)`, which matches all registered names and the + sort is purely alphabetical. :param exclude: (tuple of `str` or `re.Pattern`). Regexp patterns for - names to be excluded. Default is `()`, so no entry point is exclude. + names to be excluded. Default is `()`, so no entry + point is exclude. :return: (list) selected item factories entry points """ From d9464af8c1ac7275dd83ae51666b601f33e4069b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 23:16:10 +0200 Subject: [PATCH 247/373] Support arbitrary objects in selectEntryPoints Support the possibility of passing arbitrary objects in the include list argument of selectEntryPoints. If present, these objects get wrapped as EntryPoint-like objects and included in the corresponding position of the selection --- lib/taurus/core/util/plugin.py | 56 +++++++++++++++++++++--- lib/taurus/core/util/test/test_plugin.py | 13 +++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py index abe7140cc..fa16c1af3 100644 --- a/lib/taurus/core/util/plugin.py +++ b/lib/taurus/core/util/plugin.py @@ -27,12 +27,31 @@ Utilities for plugin loading and selection """ -__all__ = ["selectEntryPoints"] +__all__ = ["selectEntryPoints", "EntryPointAlike"] import re import pkg_resources +class EntryPointAlike(object): + """ + A dummy EntryPoint substitute to be used with :class:`selectEntryPoints` + for wrapping arbitrary objects and imitate the `name` and `load()` API of + a :class:`pkg_resources.EntryPoint` instance. + + Pass an object and optionally a name to the constructor to get the wrapped + `EntryPointAlike` instance. The `repr` of the object is used as the name + if `name` arg is not provided. + """ + def __init__(self, obj, name=None): + self._obj = obj + if name is None: + name = repr(obj) + self.name = name + def load(self): + return self._obj + + def selectEntryPoints(group=None, include=('.*',), exclude=()): """ Selects and prioritizes entry points from an entry point group. @@ -54,15 +73,31 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): And we use `exclude=("foo2",)` and `include=("bar2", "b.*1", "f.*")` , then the selection will be: ["bar2","bar1","baz1","foo1"] + Note: apart from regex patterns (strings or compiled) the `include` list + can also contain :class:`pkg_resources`EntryPoint`-like instances + (more specifically, an object having `.name` and `.load()` members), in + which case they are added directly to the selected list. If a member is + something other than a pattern or an EntryPoint-like object, it will be + wrapped in an :class:`EntryPointAlike` instance and also included in the + selection. + :param group: (str) entry point group name from which the entry points are obtained. - :param include: (`tuple` of `str` or `re.Pattern`). Regexp patterns for - names to be included in the selection. Default is - `(".*",)`, which matches all registered names and the sort - is purely alphabetical. + :param include: (`tuple`). The members of the tuple can either be + Regexp patterns (both in the form of strings or of regexp + compiled patterns), which will be matched against + registered names in group; or EntryPoint-like objects which + will be included as they are; or an arbitrary object which + will be wrapped as an EntryPoint-like object before being + included. Default is `(".*",)`, which matches all + registered names in group and the sort is purely + alphabetical. :param exclude: (`tuple` of `str` or `re.Pattern`). Regexp patterns for names to be excluded. Default is `()`, so no entry point is excluded. + + :return: (list of :class:`pkg_resources.EntryPoint`) the selected entry + points. """ ret = [] @@ -80,6 +115,17 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): # patterns in `include` (and alphabetically for a pattern that produces # multiple matches) for p in include: + try: + # check if it is a pattern string or pattern object + p = re.compile(p) + except TypeError: + # if p is not an entry point -like object, create one + if not hasattr(p, 'name') or not hasattr(p, 'load'): + p = EntryPointAlike(p) + # and add it directly + ret.append(p) + continue + # if it is a pattern, match it against remaining entry points tmp = remaining remaining = [] for e in tmp: diff --git a/lib/taurus/core/util/test/test_plugin.py b/lib/taurus/core/util/test/test_plugin.py index f983a8d6b..066119938 100644 --- a/lib/taurus/core/util/test/test_plugin.py +++ b/lib/taurus/core/util/test/test_plugin.py @@ -21,7 +21,7 @@ ## ############################################################################# -from taurus.core.util.plugin import selectEntryPoints +from taurus.core.util.plugin import selectEntryPoints, EntryPointAlike import pkg_resources import random @@ -71,7 +71,7 @@ def mock_entry_point(lines, group=None, dist_name=None): return mapping -def test_Plugin(): +def test_selectEntryPoints(): # create a fake entry point mapping (with group name testgroup)) group = _random_string(8, prefix='dummygroup_') @@ -98,3 +98,12 @@ def test_Plugin(): include=("bar2", "b.*1", "f.*") ) assert [ep.name for ep in some] == ["bar2", "bar1", "baz1", "foo1"] + + mixed = selectEntryPoints( + group, + exclude=("b.*",), + include=(123, "f.*", EntryPointAlike(321, name="boo")) + ) + assert [ep.name for ep in mixed] == ["123", "foo1", "foo2", "boo"] + assert mixed[0].load() == 123 + assert mixed[-1].load() == 321 From 5490e4eb41165352ab7790b76b53a98f71474fdf Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 18 May 2020 23:43:01 +0200 Subject: [PATCH 248/373] Replace selectItemFactories by setItemFactories The selectItemFactories method no allows to directly set a factory (not just selecting) and therefore it is better called setItemFactories. Rename, document and add specific tests for this case. --- lib/taurus/qt/qtgui/panel/taurusform.py | 19 ++++++++++------- .../qt/qtgui/panel/test/test_taurusform.py | 21 ++++++++++++++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index dd0006dcc..f04b1a458 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -126,7 +126,7 @@ def __init__(self, parent=None, self.setFormWidget(formWidget) self._itemFactories = [] - self.selectItemFactories() + self.setItemFactories() self.setLayout(Qt.QVBoxLayout()) @@ -189,7 +189,7 @@ def __len__(self): '''returns the number of items contained by the form''' return len(self.getItems()) - def selectItemFactories(self, include=('.*',), exclude=()): + def setItemFactories(self, include=('.*',), exclude=()): """ Selects and prioritizes the factories to be used to create the form's items. @@ -211,13 +211,16 @@ def selectItemFactories(self, include=('.*',), exclude=()): The selected list is updated in the form, and returned. - :param include: (tuple of `str` or `re.Pattern`). Regexp patterns for - names to be included in the selection. Default is - `(".*",)`, which matches all registered names and the - sort is purely alphabetical. + :param include: (tuple). The members in the tuple can be: Regexp + patterns (in string or compiled form) matching the + names to be included in the selection. They can also be + item factory functions (which then are wrapped in an + EntryPoint-like object and included in the selection). + The Default is `(".*",)`, which matches all registered + names and the sort is purely alphabetical. :param exclude: (tuple of `str` or `re.Pattern`). Regexp patterns for - names to be excluded. Default is `()`, so no entry - point is exclude. + registered names to be excluded. Default is `()`, so no + entry point is excluded. :return: (list) selected item factories entry points """ diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index 05ab7ffc4..0a25b0a40 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -120,17 +120,17 @@ def test_form_itemFactory_selection(): assert ep1 in default_factories # Check that we can deselect all factories - no_factories = w.selectItemFactories(include=[]) + no_factories = w.setItemFactories(include=[]) assert no_factories == [] # Check that we can exclude everything except test_Form_ItemFactory - select1 = w.selectItemFactories( + select1 = w.setItemFactories( exclude=[r"(?!.*test_Form_ItemFactorySel).*"] ) assert select1 == [ep1] # Check that we can include only test_Form_ItemFactory - select2 = w.selectItemFactories(include=["test_Form_ItemFactorySel"]) + select2 = w.setItemFactories(include=["test_Form_ItemFactorySel"]) assert select2 == [ep1] # Check that the selected test_Form_ItemFactory is an entry point @@ -139,3 +139,18 @@ def test_form_itemFactory_selection(): # Check that the selected entry point loads _DummyItemFactory assert select2[0].load() is _DummyItemFactory + + # Check that we can include a factory instance + select3 = w.setItemFactories(include=[_DummyItemFactory]) + + # Check that the selected test_Form_ItemFactory is an entry point-alike + from taurus.core.util.plugin import EntryPointAlike + assert type(select3[0]) == EntryPointAlike + + # Check that the selected entry point loads _DummyItemFactory + assert select3[0].load() is _DummyItemFactory + + # Check that the selected entry point has the given name + assert select3[0].name == repr(_DummyItemFactory) + + From cee9c66062b21ea5ca41e49d222f7746ccfde18f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 19 May 2020 02:26:23 +0200 Subject: [PATCH 249/373] Use full match for exclude patterns in selectEntryPoints The pattern matching in selectEntryPoints was changed to fullmatch in the case of the enable patterns but (by mistake) not for the disable ones. Change it now. --- lib/taurus/core/util/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py index fa16c1af3..ffcedcee3 100644 --- a/lib/taurus/core/util/plugin.py +++ b/lib/taurus/core/util/plugin.py @@ -105,7 +105,7 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): # filter out the entry points whose name matches a exclude pattern for p in exclude: - remaining = [e for e in remaining if not re.match(p, e.name)] + remaining = [e for e in remaining if not re.fullmatch(p, e.name)] # sort the remaining entry points alphabetically remaining.sort(key=lambda e: e.name) From 6c1a6e74413b87e9b877a7700e756b2e998f945b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 19 May 2020 02:54:24 +0200 Subject: [PATCH 250/373] Add return_excluded arg to getItemFactories Make TaurusForm.getItemFactories optionally return the disabled factories as well as the selected ones. --- lib/taurus/qt/qtgui/panel/taurusform.py | 18 +++++++++++++++--- .../qt/qtgui/panel/test/test_taurusform.py | 4 ++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index f04b1a458..8de8bcff5 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -231,9 +231,21 @@ def setItemFactories(self, include=('.*',), exclude=()): ) return self._itemFactories - def getItemFactories(self): - """returns the list of item factories entry points currently in use""" - return self._itemFactories + def getItemFactories(self, return_disabled=False): + """ + returns the list of item factories entry points currently in use + + :param return_disabled: If False (default), it returns only a list of + the enabled factories. If True, it returns a + tuple containing two lists: the enabled and + the available but disabled factories. + """ + enabled = self._itemFactories + if return_disabled: + all_ = selectEntryPoints('taurus.qt.taurusform.item_factories') + return enabled, [f for f in all_ if f not in enabled] + else: + return enabled def _splitModel(self, modelNames): '''convert str to list if needed (commas and whitespace are considered as separators)''' diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index 0a25b0a40..30acf4b30 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -119,6 +119,10 @@ def test_form_itemFactory_selection(): default_factories = w.getItemFactories() assert ep1 in default_factories + # no factories should be excluded by default + inc, exc = w.getItemFactories(return_disabled=True) + assert exc == [] + # Check that we can deselect all factories no_factories = w.setItemFactories(include=[]) assert no_factories == [] From 2f04248589ac4fd685ffaaaf433f71626b8cf968 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 19 May 2020 02:56:51 +0200 Subject: [PATCH 251/373] Add item factories related options to CLI Add --ls-fact, --add-fact and --exc-fact to the form CLI (to list, enable and exclude item factories, respectively) --- lib/taurus/qt/qtgui/panel/taurusform.py | 39 ++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 8de8bcff5..a2546b1ed 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -1152,13 +1152,50 @@ def setModel(self, model): @click.command('form') @taurus.cli.common.window_name("TaurusForm") @taurus.cli.common.config_file +@click.option( + '--add-fact', + multiple=True, + metavar="INC", + default=(".*",), + type=click.STRING, + help=("Enable item factories matching INC pattern" + + " (can be passed multiple times)") +) +@click.option( + '--exc-fact', + multiple=True, + metavar="EXC", + default="", + type=click.STRING, + help=("Disable item factories matching EXC pattern" + + " (can be passed multiple times)") +) +@click.option( + '--ls-fact', + is_flag=True, + help="List the available item factories" +) @taurus.cli.common.models -def form_cmd(window_name, config_file, models): +def form_cmd(window_name, config_file, add_fact, exc_fact, ls_fact, models): """Shows a Taurus form populated with the given model names""" from taurus.qt.qtgui.application import TaurusApplication import sys app = TaurusApplication(cmd_line_parser=None) dialog = TaurusForm() + + dialog.setItemFactories(include=add_fact, exclude=exc_fact) + + if ls_fact: + inc, exc = dialog.getItemFactories(return_disabled=True) + msg = "\nItem Factories in {}:\n".format(window_name) + msg += "\n".join([" [*] " + e.name for e in inc] + + [" [ ] " + e.name for e in exc]) + msg += "\nPatterns used for item factory selection:\n" + msg += " A: {}\n".format(add_fact) + msg += " E: {}\n".format(exc_fact) + print(msg) + click.get_current_context().exit(0) + dialog.setModifiableByUser(True) dialog.setModelInConfig(True) From 4d436da0b7c4c2a2aebe5bbbe45fe15a78feb700 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 19 May 2020 18:21:50 +0200 Subject: [PATCH 252/373] Add config for default item factory selection Use tauruscustomsettings.T_FORM_ITEM_FACTORIES for the default patterns for setting taurus form item factories --- lib/taurus/qt/qtgui/panel/taurusform.py | 33 +++++++++++++++---------- lib/taurus/tauruscustomsettings.py | 5 ++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index a2546b1ed..34e432ba7 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -36,6 +36,7 @@ from future.utils import string_types, binary_type +from taurus import tauruscustomsettings as _ts from taurus.external.qt import Qt import taurus.core @@ -189,7 +190,7 @@ def __len__(self): '''returns the number of items contained by the form''' return len(self.getItems()) - def setItemFactories(self, include=('.*',), exclude=()): + def setItemFactories(self, include=None, exclude=None): """ Selects and prioritizes the factories to be used to create the form's items. @@ -211,19 +212,23 @@ def setItemFactories(self, include=('.*',), exclude=()): The selected list is updated in the form, and returned. + The default values for the include and exclude arguments are defined + in `tauruscustomsettings.T_FORM_ITEM_FACTORIES` + :param include: (tuple). The members in the tuple can be: Regexp patterns (in string or compiled form) matching the names to be included in the selection. They can also be item factory functions (which then are wrapped in an EntryPoint-like object and included in the selection). - The Default is `(".*",)`, which matches all registered - names and the sort is purely alphabetical. :param exclude: (tuple of `str` or `re.Pattern`). Regexp patterns for - registered names to be excluded. Default is `()`, so no - entry point is excluded. + registered names to be excluded. :return: (list) selected item factories entry points """ - + patterns = getattr(_ts, "T_FORM_ITEM_FACTORIES", {}) + if include is None: + include = patterns.get("include", ('.*',)) + if exclude is None: + include = patterns.get("exclude", ()) self._itemFactories = selectEntryPoints( group='taurus.qt.taurusform.item_factories', include=include, @@ -1153,10 +1158,11 @@ def setModel(self, model): @taurus.cli.common.window_name("TaurusForm") @taurus.cli.common.config_file @click.option( - '--add-fact', + '--inc-fact', multiple=True, metavar="INC", - default=(".*",), + default=getattr(_ts, 'T_FORM_ITEM_FACTORIES', {}).get('include', ('.*',)), + show_default=True, type=click.STRING, help=("Enable item factories matching INC pattern" + " (can be passed multiple times)") @@ -1165,7 +1171,8 @@ def setModel(self, model): '--exc-fact', multiple=True, metavar="EXC", - default="", + default=getattr(_ts, 'T_FORM_ITEM_FACTORIES', {}).get('exclude', ()), + show_default=True, type=click.STRING, help=("Disable item factories matching EXC pattern" + " (can be passed multiple times)") @@ -1176,14 +1183,14 @@ def setModel(self, model): help="List the available item factories" ) @taurus.cli.common.models -def form_cmd(window_name, config_file, add_fact, exc_fact, ls_fact, models): +def form_cmd(window_name, config_file, inc_fact, exc_fact, ls_fact, models): """Shows a Taurus form populated with the given model names""" from taurus.qt.qtgui.application import TaurusApplication import sys app = TaurusApplication(cmd_line_parser=None) dialog = TaurusForm() - dialog.setItemFactories(include=add_fact, exclude=exc_fact) + dialog.setItemFactories(include=inc_fact, exclude=exc_fact) if ls_fact: inc, exc = dialog.getItemFactories(return_disabled=True) @@ -1191,8 +1198,8 @@ def form_cmd(window_name, config_file, add_fact, exc_fact, ls_fact, models): msg += "\n".join([" [*] " + e.name for e in inc] + [" [ ] " + e.name for e in exc]) msg += "\nPatterns used for item factory selection:\n" - msg += " A: {}\n".format(add_fact) - msg += " E: {}\n".format(exc_fact) + msg += " INC: {}\n".format(inc_fact) + msg += " EXC: {}\n".format(exc_fact) print(msg) click.get_current_context().exit(0) diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index 3b0e5c98b..ff787ddbe 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -30,6 +30,11 @@ aspects of Taurus. """ +#: Default include and exclude patterns for TaurusForm item factories +#: See `TaurusForm.setItemFactories` docs. By default, all available +#: factories are enabled (and tried alphabetically) +T_FORM_ITEM_FACTORIES = {"include": (".*",), "exclude": ()} + #: Compact mode for widgets #: True sets the preferred mode of TaurusForms to use "compact" widgets T_FORM_COMPACT = False From 040c8173e14bc4a3cc3dbe3e5d302c92f2076ce1 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 19 May 2020 18:22:25 +0200 Subject: [PATCH 253/373] Clean unneeded imports --- lib/taurus/qt/qtgui/panel/taurusform.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 34e432ba7..b403784b6 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -31,8 +31,6 @@ import click from datetime import datetime from functools import partial -import pkg_resources -import re from future.utils import string_types, binary_type @@ -40,7 +38,7 @@ from taurus.external.qt import Qt import taurus.core -from taurus.core import TaurusDevState, DisplayLevel +from taurus.core import DisplayLevel from taurus.qt.qtcore.mimetypes import (TAURUS_ATTR_MIME_TYPE, TAURUS_DEV_MIME_TYPE, TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_MODEL_MIME_TYPE) From 448bc2c53771b0736821aefbdea2c12f6983dda9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 19 May 2020 19:41:17 +0200 Subject: [PATCH 254/373] m, replace doc references to "TaurusValue factories" Use "item factories" instead --- doc/source/users/ui/forms.rst | 37 +++++++++++-------------- lib/taurus/qt/qtgui/panel/taurusform.py | 8 +++--- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/doc/source/users/ui/forms.rst b/doc/source/users/ui/forms.rst index 5b9b2a475..1d2556b51 100644 --- a/doc/source/users/ui/forms.rst +++ b/doc/source/users/ui/forms.rst @@ -54,14 +54,13 @@ Run the following command for more details:: taurus form --help The model list is optional and is a space-separated list of models for -TaurusForm. Valid models are: attribute names, device names or alias. See -:class:`TaurusForm` API for more information about valid models. +TaurusForm. -The widgets used for different types of attributes and devices ---------------------------------------------------------------- +The widgets used for different types of models +---------------------------------------------- By default, TaurusForm tries to use the most appropriate Taurus widget for -representing its attributes and/or widgets. +representing its models. .. figure:: /_static/forms03.png :align: center @@ -73,22 +72,18 @@ representing its attributes and/or widgets. sys/tg_test/1/float_image` -For the attributes, TaurusForm checks the type of attribute (whether it is a -scalar or an array, whether it is a number or a string or a boolean, whether it -is writable or read-only, etc.). For certain attributes, more than one widget -may be adequate, and the form allows the user to switch between them (See the -`Changing the contents of a form`_ section). - -For Tango devices, the Tango Class of the device is searched in the -`T_FORM_CUSTOM_WIDGET_MAP` map defined in -:ref:`tauruscustomsettings` and the given widget is used if there is a -match. Otherwise, the default device representation is used, which shows a -button that launches an :class:`AttributeForm` showing *all* the attributes for -that device. - -As an example, Sardana_ makes heavy use of `T_FORM_CUSTOM_WIDGET_MAP` in order -to display specific widgets for its motor and channel devices. - +TaurusForm uses "item factories" to obtain the widget with which each model +is represented. These item factories can be provided by taurus itself or by +external plugins (e.g. sardana provides a factory to display its pool devices). +The available factories can be listed, enabled and excluded using CLI arguments +(see `taurus form --help`). If a given model is not handled by any enabled +item factory, a default generic widget will be used which introspects the model +(e.g., if it is an attribute, it checks whether it is a scalar or an array, +whether it is a number or a string or a boolean, whether it is writable or +read-only, etc.) and uses adequate subwidgets to display it. In certain cases +this generic widget may be further customized by the user (e.g. by swithing +among a set of possible subwidgets). See the `Changing the contents of a form`_ +section). Changing the contents of a form ------------------------------- diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index b403784b6..67d232ab9 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -193,7 +193,7 @@ def setItemFactories(self, include=None, exclude=None): Selects and prioritizes the factories to be used to create the form's items. - TaurusValue factories are functions that receive a TaurusModel as + TaurusForm item factories are functions that receive a TaurusModel as their only argument and return either a TaurusValue-like instance or None in case the factory does not handle the given model. @@ -259,7 +259,7 @@ def _splitModel(self, modelNames): modelNames = modelNames.split() return modelNames - @deprecation_decorator(alt="TaurusValue factories", rel="4.6.5") + @deprecation_decorator(alt="item factories", rel="4.6.5") def setCustomWidgetMap(self, cwmap): '''Sets a map map for custom widgets. @@ -270,7 +270,7 @@ def setCustomWidgetMap(self, cwmap): # TODO: tango-centric self._customWidgetMap = cwmap - @deprecation_decorator(alt="TaurusValue factories", rel="4.6.5") + @deprecation_decorator(alt="item factories", rel="4.6.5") def getCustomWidgetMap(self): '''Returns the map used to create custom widgets. @@ -384,7 +384,7 @@ def resetModel(self): self.destroyChildren() self._model = [] - @deprecation_decorator(alt="TaurusValue factories", rel="4.6.5") + @deprecation_decorator(alt="item factories", rel="4.6.5") def getFormWidget(self, model=None): '''Returns a tuple that can be used for creating a widget for a given model. From 23e011f10a87d37957a1895f2514e385cc3e50d1 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 01:30:29 +0200 Subject: [PATCH 255/373] Fix copy-paste error in setItemFactories --- lib/taurus/qt/qtgui/panel/taurusform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 67d232ab9..0bc0744a9 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -226,7 +226,7 @@ def setItemFactories(self, include=None, exclude=None): if include is None: include = patterns.get("include", ('.*',)) if exclude is None: - include = patterns.get("exclude", ()) + exclude = patterns.get("exclude", ()) self._itemFactories = selectEntryPoints( group='taurus.qt.taurusform.item_factories', include=include, From fdccff78e46b3dcde0b987b92579e6e388d49c0c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 01:37:39 +0200 Subject: [PATCH 256/373] Simplify custom widget bck-compat Implement the backwards compatibility for the customWidgetMap API using a factory in order to avoid using the deprecated and convoluted logic of TaurusForm.getFormWidget. Also add corresponding tests and remove obsolete demo code. --- lib/taurus/qt/qtgui/panel/taurusform.py | 67 +++++++------------ .../qt/qtgui/panel/test/test_taurusform.py | 32 +++++++++ 2 files changed, 57 insertions(+), 42 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 0bc0744a9..63b332247 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -250,6 +250,25 @@ def getItemFactories(self, return_disabled=False): else: return enabled + def _customWidgetFactory(self, model): + """ + Taurus Value Factory to provide backwards-compatibility for code + relaying on deprecated customWidGetMap API + + :param model: taurus model object + + :return: custom TaurusValue class + """ + try: + key = model.getDeviceProxy().info().dev_class + name, args, kwargs = self._customWidgetMap[key] + pkgname, klassname = name.rsplit('.', 1) + pkg = __import__(pkgname, fromlist=[klassname]) + klass = getattr(pkg, klassname) + except Exception: + return None + return klass(*args, **kwargs) + def _splitModel(self, modelNames): '''convert str to list if needed (commas and whitespace are considered as separators)''' if isinstance(modelNames, binary_type): @@ -582,11 +601,12 @@ def fillWithChildren(self): model, ep.name ) - break # todo: consider exploring all to detect conflicts - # bck-compat with old custom widget map - if self._customWidgetMap: - klass, args, kwargs = self.getFormWidget(model=model) - widget = klass(frame, *args, **kwargs) + break + + # backwards-compat with deprecated custom widget map API + if widget is None and self._customWidgetMap: + widget = self._customWidgetFactory(model_obj) + # no factory handles the model and no custom widgets. Use default if widget is None: widget = self._defaultFormWidget() @@ -1115,43 +1135,6 @@ def test3(): sys.exit(app.exec_()) -def test4(): - '''tests old customwidgetmap in taurusforms''' - import sys - from taurus.qt.qtgui.display import TaurusLabel - from taurus.qt.qtgui.application import TaurusApplication - - app = TaurusApplication(sys.argv, cmd_line_parser=None) - - from taurus.qt.qtgui.panel import TaurusValue - - class DummyCW(TaurusValue): - - def setModel(self, model): - print("!!!!! IN DUMMYCW.SETMODEL", model) - TaurusValue.setModel(self, model + '/double_scalar') - - models = ['sys/database/2', 'sys/tg_test/1', 'sys/tg_test/1/short_spectrum', - 'sys/tg_test/1/state', 'sys/tg_test/1/short_scalar_ro'] - models.append('tango://controls02:10000/expchan/bl97_simucotictrl_1/1') - map = { - # taurusvalue-like classes given as strings - 'PseudoCounter': ('taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'CTExpChannel': ('taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'ZeroDExpChannel': ('taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'OneDExpChannel': ('taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - 'TwoDExpChannel': ('taurus.qt.qtgui.extra_pool.PoolChannelTV', (), {}), - # a TaurusValue-like class given as a class (old way) - 'TangoTest': DummyCW, - 'DataBase': TaurusLabel} # a non-TaurusValue-like class given as a class (old way) - - dialog = TaurusForm() - dialog.setCustomWidgetMap(map) - dialog.setModel(models) - dialog.show() - sys.exit(app.exec_()) - - @click.command('form') @taurus.cli.common.window_name("TaurusForm") @taurus.cli.common.config_file diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index 30acf4b30..3534c135d 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -158,3 +158,35 @@ def test_form_itemFactory_selection(): assert select3[0].name == repr(_DummyItemFactory) +def test_form_cwidget_bck_compat(): + """check that the cusomWidgetMap bck-compat works""" + + from taurus.qt.qtgui.application import TaurusApplication + app = TaurusApplication.instance() + if app is None: + _ = TaurusApplication([], cmd_line_parser=None) + + w = TaurusForm() + + # check that custom widget map is empty by default + assert w.getCustomWidgetMap() == {} + + w.setItemFactories(include=()) + + # check that an explicit call to setCustomWidgetMap works + dummy = ("taurus.qt.qtgui.panel.test.test_taurusform._DummyTV", (), {}) + w.setCustomWidgetMap({"DataBase": dummy}) + w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) + assert type(w[0]) == _DummyTV + assert type(w[1]) == TaurusValue + assert w.getCustomWidgetMap() == {"DataBase": dummy} + + # check that the custom widget map can be restored + w.setCustomWidgetMap({}) + w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) + assert type(w[0]) == TaurusValue + assert type(w[1]) == TaurusValue + assert w.getCustomWidgetMap() == {} + + + From ccd36c1d012a4635abe58ebe8b772465bebf966b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 02:48:53 +0200 Subject: [PATCH 257/373] Deprecate {re,}setFormWidget() getFormWidget is already deprecated. Refactor TaurusForm to avoid using also the setter and the getter. deprecate them as well as the formWidget argument to the constructor. --- lib/taurus/qt/qtgui/panel/taurusform.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 63b332247..639a924fe 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -108,7 +108,7 @@ class TaurusForm(TaurusWidget): coding examples ` ''' def __init__(self, parent=None, - formWidget=None, + formWidget=None, # deprecated buttons=None, withButtons=True, designMode=False): @@ -121,8 +121,14 @@ def __init__(self, parent=None, Qt.QDialogButtonBox.Reset self._customWidgetMap = {} # deprecated self._model = [] - # self._children = [] - self.setFormWidget(formWidget) + + if formWidget is None: + from taurus.qt.qtgui.panel import TaurusValue + formWidget = TaurusValue + else: + self.deprecated( + dep="formWidget argument", alt="item factories", rel="4.6.5") + self._defaultFormWidget = formWidget self._itemFactories = [] self.setItemFactories() @@ -459,16 +465,14 @@ def getFormWidget(self, model=None): klass = self._defaultFormWidget return klass, args, kwargs + @deprecation_decorator(alt="item factories", rel="4.6.5") def setFormWidget(self, formWidget): if formWidget is None: from taurus.qt.qtgui.panel import TaurusValue - self._defaultFormWidget = TaurusValue - elif issubclass(formWidget, Qt.QWidget): - self._defaultFormWidget = formWidget - else: - raise TypeError( - 'formWidget must be one of None, QWidget. %s passed' % repr(type(formWidget))) + formWidget = TaurusValue + self._defaultFormWidget = formWidget + @deprecation_decorator(alt="item factories", rel="4.6.5") def resetFormWidget(self): self.setFormWidget(self, None) From 6da501c3f8fdb6a8cb9461c9cb3f00791e8c5e1a Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 02:49:43 +0200 Subject: [PATCH 258/373] Protect against bad item factories Ensure that TaurusForm.fillWithChildren is robust against item factories raising exceptions and unloadable entry points. --- lib/taurus/qt/qtgui/panel/taurusform.py | 7 ++- .../qt/qtgui/panel/test/test_taurusform.py | 49 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 639a924fe..7b3c75dac 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -597,8 +597,11 @@ def fillWithChildren(self): except: warning('cannot load item factory "%s"', ep.name) continue - - widget = f(model_obj) + try: + widget = f(model_obj) + except Exception as e: + warning('factory "%s" raised "%r" for "%s". ' + + 'Tip: consider disabling it', ep.name, e, model) if widget is not None: self.debug( "widget for '%s' provided by '%s'", diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index 3534c135d..29a6f7d7d 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -77,6 +77,24 @@ def _DummyItemFactory(m): return _DummyTV() +def _BadFactory(m): + """ + A dummy item factory that fails when called with the attribute + "eval://localhost/@dummy/'test_badfactory'" and returns _DummyTV otherwise. + """ + if m.fullname == "eval://localhost/@dummy/'test_badfactory'": + return _DummyTV() + raise RuntimeError("_BadFactory is doomed to fail") + + + +class _BadEntryPoint(object): + """A dummy entry point -like class that fails when loaded (for testing)""" + name = '_BadEntryPoint' + def load(self): + raise RuntimeError("_BadEntryPoint is doomed to fail") + + def test_form_itemFactory(): """Checks that the TaurusForm itemFactory API works""" lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] @@ -189,4 +207,35 @@ def test_form_cwidget_bck_compat(): assert w.getCustomWidgetMap() == {} +def test_form_itemFactory_loading(): + """ + check that the factory loading is robust against unloadable plugins + and badly-implemented item factories + """ + + from taurus.qt.qtgui.application import TaurusApplication + app = TaurusApplication.instance() + if app is None: + _ = TaurusApplication([], cmd_line_parser=None) + + w = TaurusForm() + + w.setItemFactories( + include=(_BadEntryPoint, _BadFactory, _DummyItemFactory) + ) + w.setModel( + ["eval://localhost/@dummy/'test_itemfactory'", + "eval://localhost/@dummy/'test_badfactory'", + "eval:1", + ] + ) + + # handled by _DummyItemFactory + assert type(w[0]) == _DummyTV + # handled in _BadFactory (even if with worng return value) + assert type(w[1]) == _DummyTV + # errored in _BadFactory, ignored by _DummyItemFactory + assert type(w[2]) == TaurusValue + + From 06a2bf8b6f387f933dc6a5ec6aaaf1fc7b3996c0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 14:09:15 +0200 Subject: [PATCH 259/373] m, doc Update TaurusForm Docstring --- lib/taurus/qt/qtgui/panel/taurusform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 7b3c75dac..c265e282c 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -91,8 +91,8 @@ class TaurusForm(TaurusWidget): which are vertically aligned with their counterparts from other items. By default a :class:`TaurusValue` object is used for each item, but this - can be changed and specific mappings can be defined using the - :meth:`setCustomWidgetMap` method. + can be changed and customizations can be provided by enabling/disabling + item factories with :meth:`setItemFactories`. Item objects can be accessed by index using a list-like notation:: From d3cda4a2f51e66c22b85cf40add4241dbced0e30 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 15:57:42 +0200 Subject: [PATCH 260/373] Deprecate TaurusGui.{g,s}etCustomWidgetMap Deprecate the customWidgetMap API in TaurusGUI. Avoid calling customWidgetMap API of other widgets unless explicitly required (for bck-compat). --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 34 +++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index c7f911e67..296a8af98 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -51,6 +51,7 @@ AppletDescription) from taurus.qt.qtgui.util.ui import UILoadable from taurus.qt.qtgui.taurusgui.utils import ExternalAppAction +from taurus.core.util.log import deprecation_decorator __all__ = ["DockWidgetPanel", "TaurusGui"] @@ -168,9 +169,14 @@ def setWidgetFromClassName(self, classname, modulename=None): except Exception as e: raise RuntimeError( 'Cannot create widget from classname "%s". Reason: %s' % (classname, repr(e))) - # set customwidgetmap if necessary - if hasattr(w, 'setCustomWidgetMap'): - w.setCustomWidgetMap(self._mainwindow.getCustomWidgetMap()) + + # ---------------------------------------------------------------- + # Backwards-compat. Remove when removing CW map support + gui_cwmap = self._mainwindow._customWidgetMap + if gui_cwmap and hasattr(w, 'setCustomWidgetMap'): + w.setCustomWidgetMap(gui_cwmap) + # ---------------------------------------------------------------- + self.setWidget(w) wname = "%s-%s" % (str(self.objectName()), str(classname)) w.setObjectName(wname) @@ -314,9 +320,10 @@ def __init__(self, parent=None, confname=None, configRecursionDepth=None, self.registerConfigProperty(self.getAllInstrumentAssociations, self.setAllInstrumentAssociations, 'instrumentAssociation') + # backwards-compat from taurus import tauruscustomsettings - self.setCustomWidgetMap( - getattr(tauruscustomsettings, 'T_FORM_CUSTOM_WIDGET_MAP', {})) + cwmap = getattr(tauruscustomsettings, 'T_FORM_CUSTOM_WIDGET_MAP', {}) + self._customWidgetMap = cwmap # deprecated # Create a global SharedDataManager Qt.qApp.SDM = SharedDataManager(self) @@ -564,6 +571,7 @@ def removeExternalApp(self, name=None): self.deleteExternalAppLauncher(action) self.debug('External application "%s" removed' % name) + @deprecation_decorator(alt="item factories", rel="4.6.5") def setCustomWidgetMap(self, map): ''' Sets the widget map that is used application-wide. This widget map will @@ -577,6 +585,7 @@ def setCustomWidgetMap(self, map): ''' self._customWidgetMap = map + @deprecation_decorator(alt="item factories", rel="4.6.5") def getCustomWidgetMap(self): ''' Returns the default map used to create custom widgets by the TaurusForms @@ -836,8 +845,11 @@ def createCustomPanel(self, paneldesc=None): if not ok: return w = paneldesc.getWidget(sdm=Qt.qApp.SDM, setModel=False) - if hasattr(w, 'setCustomWidgetMap'): - w.setCustomWidgetMap(self.getCustomWidgetMap()) + # ---------------------------------------------------------------- + # Backwards-compat. Remove when removing CW map support + if self._customWidgetMap and hasattr(w, 'setCustomWidgetMap'): + w.setCustomWidgetMap(self._customWidgetMap) + # ---------------------------------------------------------------- if paneldesc.model is not None: w.setModel(paneldesc.model) @@ -1311,9 +1323,11 @@ def _loadCustomPanels(self, conf, xmlroot, poolinstruments=None): ) if result == Qt.QMessageBox.Abort: sys.exit() - - if hasattr(w, "setCustomWidgetMap"): - w.setCustomWidgetMap(self.getCustomWidgetMap()) + # ------------------------------------------------------------- + # Backwards-compat. Remove when removing CW map support + if self._customWidgetMap and hasattr(w, 'setCustomWidgetMap'): + w.setCustomWidgetMap(self._customWidgetMap) + # ------------------------------------------------------------- if p.model is not None: w.setModel(p.model) if p.instrumentkey is None: From c80f6f32a2f646af790c352481a3fe0e7cb4a724 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 19:38:29 +0200 Subject: [PATCH 261/373] Deprecate TaurusValue Custom Widget API Deprecate all the Custom Widget API: - customWidgetMap arg to TaurusValue Constructor - .customWidget() - .getDefaultCustomWidgetClass() - .{s,g}etCustomWidgetMap - .{s,g,res}etCustomWidgetClass - .customWidgetClassFactory() - .updateCustomWidget() - .addCustomWidgetToLayout() Also reimplement some parts to avoid triggering deprecation warnings while maintaining backwards compatibility. Note: backwards compatibility is maintained except for the {create,apply}Config methods, which now ignore the customWidget config. --- lib/taurus/qt/qtgui/panel/taurusvalue.py | 154 ++++++++++++----------- 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index 0067fff45..a8d437f9e 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -56,6 +56,7 @@ from taurus.qt.qtgui.button import TaurusLauncherButton from taurus.qt.qtgui.util import TaurusWidgetFactory, ConfigurationMenu from taurus.qt.qtgui.compact import TaurusReadWriteSwitcher +from taurus.core.util.log import deprecation_decorator class DefaultTaurusValueCheckBox(TaurusValueCheckBox): @@ -344,18 +345,25 @@ def __init__(self, parent=None, designMode=False, customWidgetMap=None): self._readWidget = None self._writeWidget = None self._unitsWidget = None - self._customWidget = None + self._customWidget = None # deprecated self._extraWidget = None if customWidgetMap is None: customWidgetMap = {} - self.setCustomWidgetMap(customWidgetMap) + else: + self.deprecated( + dep="customWidgetMap arg", + alt="Form item factories", + rel="4.6.5" + ) + self._customWidgetMap = customWidgetMap # deprecated + self.labelWidgetClassID = 'Auto' self.readWidgetClassID = 'Auto' self.writeWidgetClassID = 'Auto' self.unitsWidgetClassID = 'Auto' - self.customWidgetClassID = 'Auto' + self.customWidgetClassID = 'Auto' # deprecated self.extraWidgetClassID = 'Auto' self.setPreferredRow(-1) self._row = None @@ -373,7 +381,8 @@ def __init__(self, parent=None, designMode=False, customWidgetMap=None): self.registerConfigProperty(self.isCompact, self.setCompact, 'compact') def setVisible(self, visible): - for w in (self.labelWidget(), self.readWidget(), self.writeWidget(), self.unitsWidget(), self.customWidget(), self.extraWidget()): + for w in (self.labelWidget(), self.readWidget(), self.writeWidget(), + self.unitsWidget(), self._customWidget, self.extraWidget()): if w is not None: w.setVisible(visible) Qt.QWidget.setVisible(self, visible) @@ -405,6 +414,7 @@ def unitsWidget(self): '''Returns the units widget''' return self._unitsWidget + @deprecation_decorator(alt="item factories", rel="4.6.5") def customWidget(self): '''Returns the custom widget''' return self._customWidget @@ -442,8 +452,6 @@ def setParent(self, parent): self.updateUnitsWidget() self.updateExtraWidget() -# self.updateCustomWidget() - # do the base class stuff too Qt.QWidget.setParent(self, parent) @@ -631,19 +639,28 @@ def getDefaultUnitsWidgetClass(self): # return DefaultUnitsWidget return DefaultUnitsWidget + @deprecation_decorator(alt="item factories", rel="4.6.5") def getDefaultCustomWidgetClass(self): + self.__getDefaultCustomWidgetClass() + + def __getDefaultCustomWidgetClass(self): + """ + renamed from __getDefaultCustomWidgetClass to avoid deprecation + warnings. To be removed. + """ modelclass = self.getModelClass() if modelclass and modelclass.getTaurusElementType() != TaurusElementType.Device: return None try: - key = self.getModelObj().getDeviceProxy().info().dev_class # TODO: Tango-centric + key = self.getModelObj().getDeviceProxy().info().dev_class except: return None - return self.getCustomWidgetMap().get(key, None) + return self._customWidgetMap.get(key, None) def getDefaultExtraWidgetClass(self): return None + @deprecation_decorator(alt="item factories", rel="4.6.5") def setCustomWidgetMap(self, cwmap): '''Sets a map map for custom widgets. @@ -653,6 +670,7 @@ class strings (see :class:`PyTango.DeviceInfo`) and ''' self._customWidgetMap = cwmap + @deprecation_decorator(alt="item factories", rel="4.6.5") def getCustomWidgetMap(self): '''Returns the map used to create custom widgets. @@ -798,14 +816,22 @@ def unitsWidgetClassFactory(self, classID): else: return TaurusWidgetFactory().getWidgetClass(classID) + @deprecation_decorator(alt="item factories", rel="4.6.5") def customWidgetClassFactory(self, classID): + return self.__customWidgetClassFactory(self, classID) + + def __customWidgetClassFactory(self, classID): + """ + renamed from customWidgetClassFactory to avoid deprecation warnings. + To be removed. + """ if classID is None or classID == 'None': return None classID = globals().get(classID, classID) if isinstance(classID, type): return classID elif str(classID) == 'Auto': - return self.getDefaultCustomWidgetClass() + return self.__getDefaultCustomWidgetClass() else: return TaurusWidgetFactory().getWidgetClass(classID) @@ -924,9 +950,13 @@ def updateUnitsWidget(self): if hasattr(self._unitsWidget, 'setModel'): self._unitsWidget.setModel(self.getFullModelName()) + @deprecation_decorator(alt="item factories", rel="4.6.5") def updateCustomWidget(self): + self.__updateCustomWidget() + + def __updateCustomWidget(self): # get the class for the widget and replace it if necessary - klass = self.customWidgetClassFactory(self.customWidgetClassID) + klass = self.__customWidgetClassFactory(self.customWidgetClassID) self._customWidget = self._newSubwidget(self._customWidget, klass) # take care of the layout @@ -986,7 +1016,15 @@ def addUnitsWidgetToLayout(self): self.parent().layout().addWidget(self._unitsWidget, self._row, 4, 1, 1, alignment) + @deprecation_decorator(alt="item factories", rel="4.6.5") def addCustomWidgetToLayout(self): + self.__addCustomWidgetToLayout() + + def __addCustomWidgetToLayout(self): + """ + Renamed from addCustomWidgetToLayout to avoid deprecation warnings. + To be removed. + """ if self._customWidget is not None and self.parent() is not None: alignment = getattr(self._customWidget, 'layoutAlignment', Qt.Qt.AlignmentFlag(0)) @@ -1014,7 +1052,7 @@ def parentModelChanged(self, parentmodel_name): """ TaurusBaseWidget.parentModelChanged(self, parentmodel_name) if not self._designMode: # in design mode, no subwidgets are created - self.updateCustomWidget() + self.__updateCustomWidget() self.updateLabelWidget() self.updateReadWidget() self.updateWriteWidget() @@ -1073,16 +1111,19 @@ def getUnitsWidgetClass(self): def resetUnitsWidgetClass(self): self.unitsWidgetClassID = 'Auto' + @deprecation_decorator(alt="item factories", rel="4.6.5") @Qt.pyqtSlot('QString', name='setCustomWidget') def setCustomWidgetClass(self, classID): '''substitutes the current widget by a new one. classID can be one of: None, 'Auto', a TaurusWidget class name, or any class''' self.customWidgetClassID = classID - self.updateCustomWidget() + self.__updateCustomWidget() + @deprecation_decorator(alt="item factories", rel="4.6.5") def getCustomWidgetClass(self): return self.customWidgetClassID + @deprecation_decorator(alt="item factories", rel="4.6.5") def resetCustomWidgetClass(self): self.customWidgetClassID = 'Auto' @@ -1158,7 +1199,8 @@ def createConfig(self, allowUnpickable=False): configdict = TaurusBaseWidget.createConfig( self, allowUnpickable=allowUnpickable) # store the subwidgets classIDs and configs - for key in ('LabelWidget', 'ReadWidget', 'WriteWidget', 'UnitsWidget', 'CustomWidget', 'ExtraWidget'): + for key in ('LabelWidget', 'ReadWidget', 'WriteWidget', 'UnitsWidget', + 'ExtraWidget'): # calls self.getLabelWidgetClass, self.getReadWidgetClass,... classID = getattr(self, 'get%sClass' % key)() if isinstance(classID, type): @@ -1187,7 +1229,8 @@ def applyConfig(self, configdict, **kwargs): # first do the basic stuff... TaurusBaseWidget.applyConfig(self, configdict, **kwargs) # restore the subwidgets classIDs - for key in ('LabelWidget', 'ReadWidget', 'WriteWidget', 'UnitsWidget', 'CustomWidget', 'ExtraWidget'): + for key in ('LabelWidget', 'ReadWidget', 'WriteWidget', 'UnitsWidget', + 'ExtraWidget'): if key in configdict: widget_configdict = configdict[key] classID = widget_configdict.get('classid', None) @@ -1212,7 +1255,7 @@ def setModel(self, model): self.__modelClass = taurus.Manager().findObjectClass(model or '') TaurusBaseWidget.setModel(self, model) if not self._designMode: # in design mode, no subwidgets are created - self.updateCustomWidget() + self.__updateCustomWidget() self.updateLabelWidget() self.updateReadWidget() self.updateWriteWidget() @@ -1227,7 +1270,7 @@ def handleEvent(self, evt_src, evt_type, evt_value): return if self.__error or evt_type == TaurusEventType.Config: - self.updateCustomWidget() + self.__updateCustomWidget() self.updateLabelWidget() self.updateReadWidget() self.updateWriteWidget() @@ -1403,68 +1446,29 @@ def getQtDesignerPluginInfo(cls): model = Qt.pyqtProperty("QStringList", getModel, setModel, resetModel) -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! if __name__ == "__main__": - + from taurus.qt.qtgui.application import TaurusApplication + from taurus.qt.qtgui.panel import TaurusForm import sys + app = TaurusApplication(cmd_line_parser=None) - from taurus.qt.qtgui.application import TaurusApplication + w = TaurusForm() - app = TaurusApplication(sys.argv, cmd_line_parser=None) - form = Qt.QMainWindow() - # ly=Qt.QVBoxLayout(form) - # container=Qt.QWidget() -# container = TaurusValuesFrame() - from taurus.qt.qtgui.panel import TaurusForm - container = TaurusForm() - # ly.addWidget(container) - form.setCentralWidget(container) - - try: - from taurus.qt.qtgui.extra_pool import PoolMotorSlim, PoolChannel - container.setCustomWidgetMap({'SimuMotor': PoolMotorSlim, - 'Motor': PoolMotorSlim, - 'PseudoMotor': PoolMotorSlim, - 'PseudoCounter': PoolChannel, - 'CTExpChannel': PoolChannel, - 'ZeroDExpChannel': PoolChannel, - 'OneDExpChannel': PoolChannel, - 'TwoDExpChannel': PoolChannel}) - except: - pass - - # set a model list - if len(sys.argv) > 1: - models = sys.argv[1:] - container.setModel(models) - else: - models = [] - - # models assigned for debugging.... comment out when releasing - # models=['bl97/pc/dummy-01/Current','bl97/pysignalsimulator/1/value1','bl97/pc/dummy-02/RemoteMode','bl97/pc/dummy-02/CurrentSetpoint','bl97/pc/dummy-02/Current'] - # models=['bl97/pc/dummy-01/Current'] - # models=['bl97/pc/dummy-01/CurrentSetpoint','bl97/pc/dummy-02/Current','bl97/pc/dummy-02/RemoteMode','bl97/pysignalsimulator/1/value1'] - # models=['bl97/pc/dummy-01/CurrentSetpoint','bl97/pc/dummy-02/RemoteMode'] - # models=['sys/tg_test/1/state','sys/tg_test/1/status','sys/tg_test/1/short_scalar','sys/tg_test/1'] - #models = ['sys/tg_test/1']+['sys/tg_test/1/%s_scalar'%s for s in ('float','short','string','long','boolean') ] - # models = ['sys/tg_test/1/float_scalar','sys/tg#_test/1/double_scalar'] - - container.setModel(models) - - # container.getItemByIndex(0).writeWidget().setDangerMessage('BOOO') #uncomment to test the dangerous operation support - # container.getItemByIndex(0).readWidget().setShowState(True) - # container.getItemByIndex(0).setWriteWidgetClass(TaurusValueLineEdit) - # container[0].setWriteWidgetClass('None') - # container.setModel(models) - - container.setModifiableByUser(True) - form.show() - - # show an Attributechooser dialog if no model was given - if models == []: - from taurus.qt.qtgui.panel import TaurusModelChooser - modelChooser = TaurusModelChooser() - modelChooser.updateModels.connect(container.setModel) - modelChooser.show() + models = ["sys/tg_test/1/short_scalar"] * 4 + w.setModel(models) + + w[0].writeWidget().setDangerMessage('BOOO') + w[1].setWriteWidgetClass(TaurusValueLineEdit) + w[2].setWriteWidgetClass('None') + + class DummyCW(Qt.QLabel): + def setModel(self, name): + Qt.QLabel.setText(self, name) + + w[3].setCustomWidgetClass(DummyCW) + + w.setModifiableByUser(True) + w.show() sys.exit(app.exec_()) + From 4b5de679ba879dc9f5184c10c09c88680d4f80c3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 19:58:19 +0200 Subject: [PATCH 262/373] Support click 6.6 Fall back to old behaviour if click version does not support case insensitivity --- lib/taurus/cli/common.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index b0c3fe32c..04120dfa9 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -85,11 +85,16 @@ def cmd2(model, serial_mode, poll_period, default_formatter): import click +__levels = ['Critical', 'Error', 'Warning', 'Info', 'Debug', 'Trace'] +try: + __log_choice = click.Choice(__levels, case_sensitive=False) +except TypeError: # click v< 7 does not allow case_sensitive option + __log_choice = click.Choice(__levels) + + log_level = click.option( '--log-level', 'log_level', - type=click.Choice(['Critical', 'Error', 'Warning', 'Info', - 'Debug', 'Trace'], case_sensitive=False), - default='Info', show_default=True, + type=__log_choice, default='Info', show_default=True, help='Show only logs with priority LEVEL or above', ) From 12377dfdc6dbd361f8e6cfbe52d8dd0665f3a029 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 20:28:48 +0200 Subject: [PATCH 263/373] Rename item facories entry point group Rename the taurus.qt.taurusform.item_factories entry point group to taurus.form.item_factories --- CHANGELOG.md | 7 +++---- lib/taurus/qt/qtgui/panel/taurusform.py | 6 +++--- lib/taurus/qt/qtgui/panel/test/test_taurusform.py | 4 ++-- lib/taurus/tauruscustomsettings.py | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c854676d..b94dfed89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ develop branch) won't be reflected in this file. - Entry-point ("taurus.qt.formatters") for registering formatters via plugins (#1039) - New `worker_cls` argument for `taurus.core.util.ThreadPool` costructor (#1081) - `taurus.core.util.lazymodule` for delayed entry-point loading of modules (#1090) -- `'taurus.qt.taurusform.item_factories'` entry-point (#1108) +- `"taurus.form.item_factories"` entry-point (#1108) ### Removed ### Changed @@ -27,12 +27,11 @@ develop branch) won't be reflected in this file. - Improved GUI dialog for changing the formatter of a widget (#1039) - Modules registered with `"taurus.qt.qtgui"` entry-point are now lazy-loaded (#1090) - The control over which custom widgets are to be used in a TaurusForm is now - done by registering factories to the`'taurus.qt.taurusform.item_factories'` - entry-point (#1108) + done by registering factories to `"taurus.form.item_factories"` entry-point (#1108) ### Deprecated - `TaurusBaseWidget.showFormatterDlg()` (#1039) -- TaurusForm custom widget map API (#1108) +- Custom widget API in TaurusForm, TaurusValue and TaurusGui (#1108) ### Fixed - Several issues in TaurusWheelEdit (#1010, #1021) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index c265e282c..69bac4f4e 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -204,7 +204,7 @@ def setItemFactories(self, include=None, exclude=None): or None in case the factory does not handle the given model. The factories are selected using their entry point names as registered - in the "taurus.qt.taurusform.item_factories" entry point group. + in the "taurus.form.item_factories" entry point group. The factories entry point name is up to the registrar of the entry point (typically a taurus plugin) and should be documented by the @@ -234,7 +234,7 @@ def setItemFactories(self, include=None, exclude=None): if exclude is None: exclude = patterns.get("exclude", ()) self._itemFactories = selectEntryPoints( - group='taurus.qt.taurusform.item_factories', + group='taurus.form.item_factories', include=include, exclude=exclude ) @@ -251,7 +251,7 @@ def getItemFactories(self, return_disabled=False): """ enabled = self._itemFactories if return_disabled: - all_ = selectEntryPoints('taurus.qt.taurusform.item_factories') + all_ = selectEntryPoints('taurus.form.item_factories') return enabled, [f for f in all_ if f not in enabled] else: return enabled diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index 29a6f7d7d..17505bb73 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -98,7 +98,7 @@ def load(self): def test_form_itemFactory(): """Checks that the TaurusForm itemFactory API works""" lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] - group = "taurus.qt.taurusform.item_factories" + group = "taurus.form.item_factories" mock_entry_point(lines, group=group) from taurus.qt.qtgui.application import TaurusApplication @@ -122,7 +122,7 @@ def test_form_itemFactory(): def test_form_itemFactory_selection(): """Checks that the TaurusForm itemFactory selection API works""" lines = ["test_Form_ItemFactorySel={}:_DummyItemFactory".format(__name__)] - group = "taurus.qt.taurusform.item_factories" + group = "taurus.form.item_factories" mapping = mock_entry_point(lines, group=group) ep1 = mapping[group]["test_Form_ItemFactorySel"] diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index ff787ddbe..28b17991d 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -163,7 +163,7 @@ # DEPRECATED SETTINGS # ---------------------------------------------------------------------------- -#: DEPRECATED. Use "taurus.qt.taurusform.item_factories" plugin group instead +#: DEPRECATED. Use "taurus.form.item_factories" plugin group instead #: A map for using custom widgets for certain devices in TaurusForms. It is a #: dictionary with the following structure: #: device_class_name:(classname_with_full_module_path, args, kwargs) From 3b275150f7692f8807f19d03dad1402da37b0c26 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 20:39:18 +0200 Subject: [PATCH 264/373] Quarantine py35-qt5 tests (WORKAROUND!) This PR makes the py35-qt5 test to fail systematically in Travis. Allow failures to unblock this. TODO: an issue should becreated to fix this ASAP, since this target is very relevant for debian 9 support. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a388833a2..ebef9967f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,9 +23,10 @@ matrix: - env: TOXENV=py37-ps2 - env: TOXENV=py37-qt5-docs allow_failures: + - env: TOXENV=flake8 - env: TOXENV=py27-qt4 - env: TOXENV=py27-qt5 - - env: TOXENV=flake8 + - env: TOXENV=py35-qt5 before_install: # install conda From f2792e110c5089daedc7431c4e365ba82badb93f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 20 May 2020 21:09:15 +0200 Subject: [PATCH 265/373] Avoid addCustomWidgetToLayout calls Avoid deprecation warnings by calling internally to `__addCustomWidgetToLayout` instead. --- lib/taurus/qt/qtgui/panel/taurusvalue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusvalue.py b/lib/taurus/qt/qtgui/panel/taurusvalue.py index a8d437f9e..8de26e5dc 100644 --- a/lib/taurus/qt/qtgui/panel/taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/taurusvalue.py @@ -960,7 +960,7 @@ def __updateCustomWidget(self): self._customWidget = self._newSubwidget(self._customWidget, klass) # take care of the layout - self.addCustomWidgetToLayout() + self.__addCustomWidgetToLayout() if self._customWidget is not None: # set the model for the subwidget From 94e996ddb418ef4a73e2101891b9ea431948f718 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 25 May 2020 15:35:24 +0200 Subject: [PATCH 266/373] m, doc Fix some minor doc issues found during review --- CHANGELOG.md | 3 ++- lib/taurus/core/util/plugin.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b94dfed89..ee7053457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,8 @@ develop branch) won't be reflected in this file. ### Deprecated - `TaurusBaseWidget.showFormatterDlg()` (#1039) -- Custom widget API in TaurusForm, TaurusValue and TaurusGui (#1108) +- Custom widget API in TaurusForm, TaurusValue and TaurusGui (#1108) +- `tauruscustomsettings.T_FORM_CUSTOM_WIDGET_MAP` (#1108) ### Fixed - Several issues in TaurusWheelEdit (#1010, #1021) diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py index ffcedcee3..b91db3757 100644 --- a/lib/taurus/core/util/plugin.py +++ b/lib/taurus/core/util/plugin.py @@ -35,7 +35,7 @@ class EntryPointAlike(object): """ - A dummy EntryPoint substitute to be used with :class:`selectEntryPoints` + A dummy EntryPoint substitute to be used with :func:`selectEntryPoints` for wrapping arbitrary objects and imitate the `name` and `load()` API of a :class:`pkg_resources.EntryPoint` instance. @@ -48,7 +48,9 @@ def __init__(self, obj, name=None): if name is None: name = repr(obj) self.name = name + def load(self): + """Returns the wrapped object""" return self._obj @@ -66,7 +68,7 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): In this way, the entry points are selected in the order dictated by the `included` pattern list. If a pattern matches several names, these will be sorted alphabetically. If a name is matched by several patterns, - it is only selected byt the first pattern. + it is only selected by the first pattern. For example, if there are the following registered entry point names: ['foo1', 'foo2', 'baz1', 'baz2', 'bar1', 'bar2'] @@ -84,17 +86,17 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): :param group: (str) entry point group name from which the entry points are obtained. :param include: (`tuple`). The members of the tuple can either be - Regexp patterns (both in the form of strings or of regexp - compiled patterns), which will be matched against + patterns (both in the form of strings or of + :class:`re.Pattern` objects), which will be matched against registered names in group; or EntryPoint-like objects which will be included as they are; or an arbitrary object which will be wrapped as an EntryPoint-like object before being included. Default is `(".*",)`, which matches all registered names in group and the sort is purely alphabetical. - :param exclude: (`tuple` of `str` or `re.Pattern`). Regexp patterns for - names to be excluded. Default is `()`, so no entry point - is excluded. + :param exclude: (`tuple`). Regexp patterns (either `str` or + :class:`re.Pattern` objects) matching names to be excluded. + Default is `()`, so no entry point is excluded. :return: (list of :class:`pkg_resources.EntryPoint`) the selected entry points. From a72b4c7c0aaf296d7b713bd0ad842b1c662f8a4b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 25 May 2020 15:38:38 +0200 Subject: [PATCH 267/373] m, doc Fix some minor doc issues found during review --- lib/taurus/qt/qtgui/panel/taurusform.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusform.py b/lib/taurus/qt/qtgui/panel/taurusform.py index 69bac4f4e..8e2a2fca3 100644 --- a/lib/taurus/qt/qtgui/panel/taurusform.py +++ b/lib/taurus/qt/qtgui/panel/taurusform.py @@ -224,8 +224,9 @@ def setItemFactories(self, include=None, exclude=None): names to be included in the selection. They can also be item factory functions (which then are wrapped in an EntryPoint-like object and included in the selection). - :param exclude: (tuple of `str` or `re.Pattern`). Regexp patterns for - registered names to be excluded. + :param exclude: (tuple). Regexp patterns ( either `str` or + :class:`re.Pattern` objects) matching registered names + to be excluded. :return: (list) selected item factories entry points """ patterns = getattr(_ts, "T_FORM_ITEM_FACTORIES", {}) From 0be8624f56ace4b58af39e94ea992d263787169e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 21 May 2020 03:21:50 +0200 Subject: [PATCH 268/373] Add taurus_test_ds pytest fixture Add a fixture for launching TangoSchemeTest for a test --- lib/taurus/core/tango/test/__init__.py | 2 +- lib/taurus/core/tango/test/tgtestds.py | 41 +++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/taurus/core/tango/test/__init__.py b/lib/taurus/core/tango/test/__init__.py index 769f3e503..052e9e674 100644 --- a/lib/taurus/core/tango/test/__init__.py +++ b/lib/taurus/core/tango/test/__init__.py @@ -24,4 +24,4 @@ ############################################################################# from __future__ import absolute_import -from .tgtestds import TangoSchemeTestLauncher +from .tgtestds import TangoSchemeTestLauncher, taurus_test_ds diff --git a/lib/taurus/core/tango/test/tgtestds.py b/lib/taurus/core/tango/test/tgtestds.py index 3f9dab823..b4333c16e 100644 --- a/lib/taurus/core/tango/test/tgtestds.py +++ b/lib/taurus/core/tango/test/tgtestds.py @@ -29,12 +29,51 @@ import PyTango from taurus.core.tango.starter import ProcessStarter from taurus.test import getResourcePath +import pytest +from random import randint -__all__ = ['TangoSchemeTestLauncher'] + +__all__ = ['TangoSchemeTestLauncher', 'taurus_test_ds'] __docformat__ = 'restructuredtext' +@pytest.fixture(scope="module") +def taurus_test_ds(): + """ + A pytest fixture that launches TangoSchemeTest for the test + It provides the device name as the fixture value. + + Usage:: + from taurus.core.tango.test import taurus_test_ds + + def test_foo(taurus_test_ds): + import taurus + d = taurus.Device(taurus_test_ds) + assert d["string_scalar"].rvalue == "hello world" + + """ + ds_name = 'TangoSchemeTest/unittest/temp-{:08d}'.format( + randint(0, 99999999)) + + # get path to DS and executable + device = getResourcePath( + 'taurus.core.tango.test.res', 'TangoSchemeTest') + # create starter for the device server + _starter = ProcessStarter(device, 'TangoSchemeTest/unittest') + # register + _starter.addNewDevice(ds_name, klass='TangoSchemeTest') + # start device server + _starter.startDs() + + yield ds_name + d = PyTango.DeviceProxy(ds_name) + d.Reset() + _starter.stopDs(hard_kill=True) + # remove server + _starter.cleanDb(force=True) + + class TangoSchemeTestLauncher(object): """A base class for TestCase classes wishing to start a TangoSchemeTest. Use it as a mixin class""" From 806cb0f80d43a383314c572a2ef52b7e834ea2db Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 21 May 2020 04:29:09 +0200 Subject: [PATCH 269/373] Add taurus_widget_set_models generic test Parameterizable pytest test for setting models sequentially This is the Pytest equivalent to GenericWidgetTestCase.test10_SetModelsSequentially --- lib/taurus/qt/qtgui/test/base.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/taurus/qt/qtgui/test/base.py b/lib/taurus/qt/qtgui/test/base.py index 7e8b46452..71e811527 100644 --- a/lib/taurus/qt/qtgui/test/base.py +++ b/lib/taurus/qt/qtgui/test/base.py @@ -33,6 +33,25 @@ from taurus.qt.qtgui.application import TaurusApplication +def taurus_widget_set_models(qtbot, klass, models=[]): + """ + Generic test that checks that a widget can be instantiated and given + its setModel called sequentially. + + It can be parameterized or run with functools.partial + """ + w = klass() + qtbot.addWidget(w) + + for model in models: + if not model: + model_obj = None + else: + model_obj = taurus.Object(model) + w.setModel(model) + assert w.getModelObj() == model_obj + + class BaseWidgetTestCase(object): ''' From 565121283359ec6b2fdeed5a028b8bbc202c1517 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 25 May 2020 19:14:31 +0200 Subject: [PATCH 270/373] Add check_taurus_deprecations CM Implement a context manager to facilitate checking deprecation errors in pytest tests --- lib/taurus/qt/qtgui/test/base.py | 24 +++++++------- lib/taurus/test/pytest.py | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 lib/taurus/test/pytest.py diff --git a/lib/taurus/qt/qtgui/test/base.py b/lib/taurus/qt/qtgui/test/base.py index 71e811527..11ccc8de4 100644 --- a/lib/taurus/qt/qtgui/test/base.py +++ b/lib/taurus/qt/qtgui/test/base.py @@ -31,25 +31,27 @@ import taurus.core import unittest from taurus.qt.qtgui.application import TaurusApplication +from taurus.test.pytest import check_taurus_deprecations -def taurus_widget_set_models(qtbot, klass, models=[]): +def taurus_widget_set_models(klass, qtbot, caplog, depr=0, models=[]): """ Generic test that checks that a widget can be instantiated and given its setModel called sequentially. It can be parameterized or run with functools.partial """ - w = klass() - qtbot.addWidget(w) - - for model in models: - if not model: - model_obj = None - else: - model_obj = taurus.Object(model) - w.setModel(model) - assert w.getModelObj() == model_obj + with check_taurus_deprecations(caplog, expected=depr): + w = klass() + qtbot.addWidget(w) + + for model in models: + if not model: + model_obj = None + else: + model_obj = taurus.Object(model) + w.setModel(model) + assert w.getModelObj() == model_obj class BaseWidgetTestCase(object): diff --git a/lib/taurus/test/pytest.py b/lib/taurus/test/pytest.py new file mode 100644 index 000000000..e51aa8491 --- /dev/null +++ b/lib/taurus/test/pytest.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +"""Common tools and fixtures for using with taurus and pytest""" + +import contextlib +import functools + + +@contextlib.contextmanager +def check_taurus_deprecations(caplog, expected=0): + """context manager that checks the number of taurus deprecation warnings + logged within the context execution. + + :param expected: (int) Expected number of deprecations. Default is 0 + """ + from taurus import tauruscustomsettings + # disable deprecation silencing in this context + bck = getattr(tauruscustomsettings, "MAX_DEPRECATIONS_LOGGED", None) + tauruscustomsettings._MAX_DEPRECATIONS_LOGGED = None + caplog.clear() + try: + yield + finally: + tauruscustomsettings._MAX_DEPRECATIONS_LOGGED = bck + # check the deprecations after the context is run + deps = [r for r in caplog.records if "DeprecationWarning" in r.msg] + n = len(deps) + msg = "{} Deprecation Warnings ({} expected)".format(n, expected) + assert len(deps) == expected, msg + + From 6635f94f896e8b2770cbb0cd3008024eb1af651d Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 11:56:32 +0200 Subject: [PATCH 271/373] [TEMPORARY] Disable of some test for debugging Leave only some relevant tests for quicker debugging of py35-qt5 testing issues --- .travis.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index ebef9967f..b7c12f588 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,19 +14,19 @@ env: matrix: include: - - env: TOXENV=flake8 - - env: TOXENV=py27-qt4 + #- env: TOXENV=flake8 + #- env: TOXENV=py27-qt4 - env: TOXENV=py27-qt5 - env: TOXENV=py35-qt5 - - env: TOXENV=py36-qt5 + #- env: TOXENV=py36-qt5 - env: TOXENV=py37-qt5 - - env: TOXENV=py37-ps2 - - env: TOXENV=py37-qt5-docs + #- env: TOXENV=py37-ps2 + #- env: TOXENV=py37-qt5-docs allow_failures: - - env: TOXENV=flake8 - - env: TOXENV=py27-qt4 + #- env: TOXENV=flake8 + #- env: TOXENV=py27-qt4 - env: TOXENV=py27-qt5 - - env: TOXENV=py35-qt5 + #- env: TOXENV=py35-qt5 before_install: # install conda From 067977336167176be79e9b9198d1abc81bc5f741 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 12:05:09 +0200 Subject: [PATCH 272/373] Disable newly added pytest tests --- .../qt/qtgui/panel/test/test_taurusform.py | 348 +++++++++--------- 1 file changed, 174 insertions(+), 174 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index 17505bb73..e4f6dd7d5 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -64,178 +64,178 @@ class TaurusAttrFormTest(GenericWidgetTestCase, unittest.TestCase): modelnames = ['sys/tg_test/1', None] -class _DummyTV(TaurusValue): - pass - - -def _DummyItemFactory(m): - """ - A dummy item factory that returns _DummyTV instance for one specific - attribute: "eval://localhost/@dummy/'test_itemfactory'" - """ - if m.fullname == "eval://localhost/@dummy/'test_itemfactory'": - return _DummyTV() - - -def _BadFactory(m): - """ - A dummy item factory that fails when called with the attribute - "eval://localhost/@dummy/'test_badfactory'" and returns _DummyTV otherwise. - """ - if m.fullname == "eval://localhost/@dummy/'test_badfactory'": - return _DummyTV() - raise RuntimeError("_BadFactory is doomed to fail") - - - -class _BadEntryPoint(object): - """A dummy entry point -like class that fails when loaded (for testing)""" - name = '_BadEntryPoint' - def load(self): - raise RuntimeError("_BadEntryPoint is doomed to fail") - - -def test_form_itemFactory(): - """Checks that the TaurusForm itemFactory API works""" - lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] - group = "taurus.form.item_factories" - mock_entry_point(lines, group=group) - - from taurus.qt.qtgui.application import TaurusApplication - app = TaurusApplication.instance() - if app is None: - _ = TaurusApplication([], cmd_line_parser=None) - - w = TaurusForm() - w.setModel( - [ - "eval://localhost/@dummy/'test_itemfactory'", - "eval://localhost/@dummy/'test_itemfactory2'", - ] - ) - # The first item should get a customized _DummyTV widget - assert type(w[0]) is _DummyTV - # The second item shoud get the default form widget - assert type(w[1]) is w._defaultFormWidget - - -def test_form_itemFactory_selection(): - """Checks that the TaurusForm itemFactory selection API works""" - lines = ["test_Form_ItemFactorySel={}:_DummyItemFactory".format(__name__)] - group = "taurus.form.item_factories" - mapping = mock_entry_point(lines, group=group) - ep1 = mapping[group]["test_Form_ItemFactorySel"] - - from taurus.qt.qtgui.application import TaurusApplication - app = TaurusApplication.instance() - if app is None: - _ = TaurusApplication([], cmd_line_parser=None) - - w = TaurusForm() - - # the test_Form_ItemFactory should be in the default factories - default_factories = w.getItemFactories() - assert ep1 in default_factories - - # no factories should be excluded by default - inc, exc = w.getItemFactories(return_disabled=True) - assert exc == [] - - # Check that we can deselect all factories - no_factories = w.setItemFactories(include=[]) - assert no_factories == [] - - # Check that we can exclude everything except test_Form_ItemFactory - select1 = w.setItemFactories( - exclude=[r"(?!.*test_Form_ItemFactorySel).*"] - ) - assert select1 == [ep1] - - # Check that we can include only test_Form_ItemFactory - select2 = w.setItemFactories(include=["test_Form_ItemFactorySel"]) - assert select2 == [ep1] - - # Check that the selected test_Form_ItemFactory is an entry point - from pkg_resources import EntryPoint - assert type(select2[0]) == EntryPoint - - # Check that the selected entry point loads _DummyItemFactory - assert select2[0].load() is _DummyItemFactory - - # Check that we can include a factory instance - select3 = w.setItemFactories(include=[_DummyItemFactory]) - - # Check that the selected test_Form_ItemFactory is an entry point-alike - from taurus.core.util.plugin import EntryPointAlike - assert type(select3[0]) == EntryPointAlike - - # Check that the selected entry point loads _DummyItemFactory - assert select3[0].load() is _DummyItemFactory - - # Check that the selected entry point has the given name - assert select3[0].name == repr(_DummyItemFactory) - - -def test_form_cwidget_bck_compat(): - """check that the cusomWidgetMap bck-compat works""" - - from taurus.qt.qtgui.application import TaurusApplication - app = TaurusApplication.instance() - if app is None: - _ = TaurusApplication([], cmd_line_parser=None) - - w = TaurusForm() - - # check that custom widget map is empty by default - assert w.getCustomWidgetMap() == {} - - w.setItemFactories(include=()) - - # check that an explicit call to setCustomWidgetMap works - dummy = ("taurus.qt.qtgui.panel.test.test_taurusform._DummyTV", (), {}) - w.setCustomWidgetMap({"DataBase": dummy}) - w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) - assert type(w[0]) == _DummyTV - assert type(w[1]) == TaurusValue - assert w.getCustomWidgetMap() == {"DataBase": dummy} - - # check that the custom widget map can be restored - w.setCustomWidgetMap({}) - w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) - assert type(w[0]) == TaurusValue - assert type(w[1]) == TaurusValue - assert w.getCustomWidgetMap() == {} - - -def test_form_itemFactory_loading(): - """ - check that the factory loading is robust against unloadable plugins - and badly-implemented item factories - """ - - from taurus.qt.qtgui.application import TaurusApplication - app = TaurusApplication.instance() - if app is None: - _ = TaurusApplication([], cmd_line_parser=None) - - w = TaurusForm() - - w.setItemFactories( - include=(_BadEntryPoint, _BadFactory, _DummyItemFactory) - ) - w.setModel( - ["eval://localhost/@dummy/'test_itemfactory'", - "eval://localhost/@dummy/'test_badfactory'", - "eval:1", - ] - ) - - # handled by _DummyItemFactory - assert type(w[0]) == _DummyTV - # handled in _BadFactory (even if with worng return value) - assert type(w[1]) == _DummyTV - # errored in _BadFactory, ignored by _DummyItemFactory - assert type(w[2]) == TaurusValue - - +# class _DummyTV(TaurusValue): +# pass +# +# +# def _DummyItemFactory(m): +# """ +# A dummy item factory that returns _DummyTV instance for one specific +# attribute: "eval://localhost/@dummy/'test_itemfactory'" +# """ +# if m.fullname == "eval://localhost/@dummy/'test_itemfactory'": +# return _DummyTV() +# +# +# def _BadFactory(m): +# """ +# A dummy item factory that fails when called with the attribute +# "eval://localhost/@dummy/'test_badfactory'" and returns _DummyTV otherwise. +# """ +# if m.fullname == "eval://localhost/@dummy/'test_badfactory'": +# return _DummyTV() +# raise RuntimeError("_BadFactory is doomed to fail") +# +# +# +# class _BadEntryPoint(object): +# """A dummy entry point -like class that fails when loaded (for testing)""" +# name = '_BadEntryPoint' +# def load(self): +# raise RuntimeError("_BadEntryPoint is doomed to fail") +# +# +# def test_form_itemFactory(): +# """Checks that the TaurusForm itemFactory API works""" +# lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] +# group = "taurus.form.item_factories" +# mock_entry_point(lines, group=group) +# +# from taurus.qt.qtgui.application import TaurusApplication +# app = TaurusApplication.instance() +# if app is None: +# _ = TaurusApplication([], cmd_line_parser=None) +# +# w = TaurusForm() +# w.setModel( +# [ +# "eval://localhost/@dummy/'test_itemfactory'", +# "eval://localhost/@dummy/'test_itemfactory2'", +# ] +# ) +# # The first item should get a customized _DummyTV widget +# assert type(w[0]) is _DummyTV +# # The second item shoud get the default form widget +# assert type(w[1]) is w._defaultFormWidget +# +# +# def test_form_itemFactory_selection(): +# """Checks that the TaurusForm itemFactory selection API works""" +# lines = ["test_Form_ItemFactorySel={}:_DummyItemFactory".format(__name__)] +# group = "taurus.form.item_factories" +# mapping = mock_entry_point(lines, group=group) +# ep1 = mapping[group]["test_Form_ItemFactorySel"] +# +# from taurus.qt.qtgui.application import TaurusApplication +# app = TaurusApplication.instance() +# if app is None: +# _ = TaurusApplication([], cmd_line_parser=None) +# +# w = TaurusForm() +# +# # the test_Form_ItemFactory should be in the default factories +# default_factories = w.getItemFactories() +# assert ep1 in default_factories +# +# # no factories should be excluded by default +# inc, exc = w.getItemFactories(return_disabled=True) +# assert exc == [] +# +# # Check that we can deselect all factories +# no_factories = w.setItemFactories(include=[]) +# assert no_factories == [] +# +# # Check that we can exclude everything except test_Form_ItemFactory +# select1 = w.setItemFactories( +# exclude=[r"(?!.*test_Form_ItemFactorySel).*"] +# ) +# assert select1 == [ep1] +# +# # Check that we can include only test_Form_ItemFactory +# select2 = w.setItemFactories(include=["test_Form_ItemFactorySel"]) +# assert select2 == [ep1] +# +# # Check that the selected test_Form_ItemFactory is an entry point +# from pkg_resources import EntryPoint +# assert type(select2[0]) == EntryPoint +# +# # Check that the selected entry point loads _DummyItemFactory +# assert select2[0].load() is _DummyItemFactory +# +# # Check that we can include a factory instance +# select3 = w.setItemFactories(include=[_DummyItemFactory]) +# +# # Check that the selected test_Form_ItemFactory is an entry point-alike +# from taurus.core.util.plugin import EntryPointAlike +# assert type(select3[0]) == EntryPointAlike +# +# # Check that the selected entry point loads _DummyItemFactory +# assert select3[0].load() is _DummyItemFactory +# +# # Check that the selected entry point has the given name +# assert select3[0].name == repr(_DummyItemFactory) +# +# +# def test_form_cwidget_bck_compat(): +# """check that the cusomWidgetMap bck-compat works""" +# +# from taurus.qt.qtgui.application import TaurusApplication +# app = TaurusApplication.instance() +# if app is None: +# _ = TaurusApplication([], cmd_line_parser=None) +# +# w = TaurusForm() +# +# # check that custom widget map is empty by default +# assert w.getCustomWidgetMap() == {} +# +# w.setItemFactories(include=()) +# +# # check that an explicit call to setCustomWidgetMap works +# dummy = ("taurus.qt.qtgui.panel.test.test_taurusform._DummyTV", (), {}) +# w.setCustomWidgetMap({"DataBase": dummy}) +# w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) +# assert type(w[0]) == _DummyTV +# assert type(w[1]) == TaurusValue +# assert w.getCustomWidgetMap() == {"DataBase": dummy} +# +# # check that the custom widget map can be restored +# w.setCustomWidgetMap({}) +# w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) +# assert type(w[0]) == TaurusValue +# assert type(w[1]) == TaurusValue +# assert w.getCustomWidgetMap() == {} +# +# +# def test_form_itemFactory_loading(): +# """ +# check that the factory loading is robust against unloadable plugins +# and badly-implemented item factories +# """ +# +# from taurus.qt.qtgui.application import TaurusApplication +# app = TaurusApplication.instance() +# if app is None: +# _ = TaurusApplication([], cmd_line_parser=None) +# +# w = TaurusForm() +# +# w.setItemFactories( +# include=(_BadEntryPoint, _BadFactory, _DummyItemFactory) +# ) +# w.setModel( +# ["eval://localhost/@dummy/'test_itemfactory'", +# "eval://localhost/@dummy/'test_badfactory'", +# "eval:1", +# ] +# ) +# +# # handled by _DummyItemFactory +# assert type(w[0]) == _DummyTV +# # handled in _BadFactory (even if with worng return value) +# assert type(w[1]) == _DummyTV +# # errored in _BadFactory, ignored by _DummyItemFactory +# assert type(w[2]) == TaurusValue +# +# From bfc83057242cf65fe11235cbea907c80768fc228 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 12:11:43 +0200 Subject: [PATCH 273/373] Add pytest-qt to tox.ini dependencies --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index bcd8892ee..5662b8f28 100644 --- a/tox.ini +++ b/tox.ini @@ -37,6 +37,7 @@ conda_deps= spyder pytest >= 3.6.3 pytest-xvfb + pytest-qt flaky docs: sphinx docs: sphinx_rtd_theme From 17d7e6e7825b1979388bb2bd62c6f20f1ab4d4e8 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 12:23:17 +0200 Subject: [PATCH 274/373] Refactor and re-enable test_form_itemFactory Re-enable test_form_itemFactory after refactoring it to use the qtbot fixture from pytest-qt --- .../qt/qtgui/panel/test/test_taurusform.py | 105 +++++++++--------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index e4f6dd7d5..a372d6c30 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -64,60 +64,57 @@ class TaurusAttrFormTest(GenericWidgetTestCase, unittest.TestCase): modelnames = ['sys/tg_test/1', None] -# class _DummyTV(TaurusValue): -# pass -# -# -# def _DummyItemFactory(m): -# """ -# A dummy item factory that returns _DummyTV instance for one specific -# attribute: "eval://localhost/@dummy/'test_itemfactory'" -# """ -# if m.fullname == "eval://localhost/@dummy/'test_itemfactory'": -# return _DummyTV() -# -# -# def _BadFactory(m): -# """ -# A dummy item factory that fails when called with the attribute -# "eval://localhost/@dummy/'test_badfactory'" and returns _DummyTV otherwise. -# """ -# if m.fullname == "eval://localhost/@dummy/'test_badfactory'": -# return _DummyTV() -# raise RuntimeError("_BadFactory is doomed to fail") -# -# -# -# class _BadEntryPoint(object): -# """A dummy entry point -like class that fails when loaded (for testing)""" -# name = '_BadEntryPoint' -# def load(self): -# raise RuntimeError("_BadEntryPoint is doomed to fail") -# -# -# def test_form_itemFactory(): -# """Checks that the TaurusForm itemFactory API works""" -# lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] -# group = "taurus.form.item_factories" -# mock_entry_point(lines, group=group) -# -# from taurus.qt.qtgui.application import TaurusApplication -# app = TaurusApplication.instance() -# if app is None: -# _ = TaurusApplication([], cmd_line_parser=None) -# -# w = TaurusForm() -# w.setModel( -# [ -# "eval://localhost/@dummy/'test_itemfactory'", -# "eval://localhost/@dummy/'test_itemfactory2'", -# ] -# ) -# # The first item should get a customized _DummyTV widget -# assert type(w[0]) is _DummyTV -# # The second item shoud get the default form widget -# assert type(w[1]) is w._defaultFormWidget -# +class _DummyTV(TaurusValue): + pass + + +def _DummyItemFactory(m): + """ + A dummy item factory that returns _DummyTV instance for one specific + attribute: "eval://localhost/@dummy/'test_itemfactory'" + """ + if m.fullname == "eval://localhost/@dummy/'test_itemfactory'": + return _DummyTV() + + +def _BadFactory(m): + """ + A dummy item factory that fails when called with the attribute + "eval://localhost/@dummy/'test_badfactory'" and returns _DummyTV otherwise. + """ + if m.fullname == "eval://localhost/@dummy/'test_badfactory'": + return _DummyTV() + raise RuntimeError("_BadFactory is doomed to fail") + + +class _BadEntryPoint(object): + """A dummy entry point -like class that fails when loaded (for testing)""" + name = '_BadEntryPoint' + def load(self): + raise RuntimeError("_BadEntryPoint is doomed to fail") + + +def test_form_itemFactory(qtbot): + """Checks that the TaurusForm itemFactory API works""" + lines = ["test_Form_ItemFactory={}:_DummyItemFactory".format(__name__)] + group = "taurus.form.item_factories" + mock_entry_point(lines, group=group) + + w = TaurusForm() + qtbot.addWidget(w) + + w.setModel( + [ + "eval://localhost/@dummy/'test_itemfactory'", + "eval://localhost/@dummy/'test_itemfactory2'", + ] + ) + qtbot.wait_until(lambda: len(w) == 2, timeout=3200) + # The first item should get a customized _DummyTV widget + assert type(w[0]) is _DummyTV + # The second item shoud get the default form widget + assert type(w[1]) is w._defaultFormWidget + # # def test_form_itemFactory_selection(): # """Checks that the TaurusForm itemFactory selection API works""" From 45795160682914d8b757968a609e7de25ce075ac Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 13:07:59 +0200 Subject: [PATCH 275/373] Refactor and re-enable remaining form tests Refactor (to use qtbot) and re-enable the following: - test_form_itemFactory_selection - test_form_cwidget_bck_compat - test_form_itemFactory_loading --- .../qt/qtgui/panel/test/test_taurusform.py | 231 +++++++++--------- 1 file changed, 111 insertions(+), 120 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index a372d6c30..b5cd005fa 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -115,124 +115,115 @@ def test_form_itemFactory(qtbot): # The second item shoud get the default form widget assert type(w[1]) is w._defaultFormWidget -# -# def test_form_itemFactory_selection(): -# """Checks that the TaurusForm itemFactory selection API works""" -# lines = ["test_Form_ItemFactorySel={}:_DummyItemFactory".format(__name__)] -# group = "taurus.form.item_factories" -# mapping = mock_entry_point(lines, group=group) -# ep1 = mapping[group]["test_Form_ItemFactorySel"] -# -# from taurus.qt.qtgui.application import TaurusApplication -# app = TaurusApplication.instance() -# if app is None: -# _ = TaurusApplication([], cmd_line_parser=None) -# -# w = TaurusForm() -# -# # the test_Form_ItemFactory should be in the default factories -# default_factories = w.getItemFactories() -# assert ep1 in default_factories -# -# # no factories should be excluded by default -# inc, exc = w.getItemFactories(return_disabled=True) -# assert exc == [] -# -# # Check that we can deselect all factories -# no_factories = w.setItemFactories(include=[]) -# assert no_factories == [] -# -# # Check that we can exclude everything except test_Form_ItemFactory -# select1 = w.setItemFactories( -# exclude=[r"(?!.*test_Form_ItemFactorySel).*"] -# ) -# assert select1 == [ep1] -# -# # Check that we can include only test_Form_ItemFactory -# select2 = w.setItemFactories(include=["test_Form_ItemFactorySel"]) -# assert select2 == [ep1] -# -# # Check that the selected test_Form_ItemFactory is an entry point -# from pkg_resources import EntryPoint -# assert type(select2[0]) == EntryPoint -# -# # Check that the selected entry point loads _DummyItemFactory -# assert select2[0].load() is _DummyItemFactory -# -# # Check that we can include a factory instance -# select3 = w.setItemFactories(include=[_DummyItemFactory]) -# -# # Check that the selected test_Form_ItemFactory is an entry point-alike -# from taurus.core.util.plugin import EntryPointAlike -# assert type(select3[0]) == EntryPointAlike -# -# # Check that the selected entry point loads _DummyItemFactory -# assert select3[0].load() is _DummyItemFactory -# -# # Check that the selected entry point has the given name -# assert select3[0].name == repr(_DummyItemFactory) -# -# -# def test_form_cwidget_bck_compat(): -# """check that the cusomWidgetMap bck-compat works""" -# -# from taurus.qt.qtgui.application import TaurusApplication -# app = TaurusApplication.instance() -# if app is None: -# _ = TaurusApplication([], cmd_line_parser=None) -# -# w = TaurusForm() -# -# # check that custom widget map is empty by default -# assert w.getCustomWidgetMap() == {} -# -# w.setItemFactories(include=()) -# -# # check that an explicit call to setCustomWidgetMap works -# dummy = ("taurus.qt.qtgui.panel.test.test_taurusform._DummyTV", (), {}) -# w.setCustomWidgetMap({"DataBase": dummy}) -# w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) -# assert type(w[0]) == _DummyTV -# assert type(w[1]) == TaurusValue -# assert w.getCustomWidgetMap() == {"DataBase": dummy} -# -# # check that the custom widget map can be restored -# w.setCustomWidgetMap({}) -# w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) -# assert type(w[0]) == TaurusValue -# assert type(w[1]) == TaurusValue -# assert w.getCustomWidgetMap() == {} -# -# -# def test_form_itemFactory_loading(): -# """ -# check that the factory loading is robust against unloadable plugins -# and badly-implemented item factories -# """ -# -# from taurus.qt.qtgui.application import TaurusApplication -# app = TaurusApplication.instance() -# if app is None: -# _ = TaurusApplication([], cmd_line_parser=None) -# -# w = TaurusForm() -# -# w.setItemFactories( -# include=(_BadEntryPoint, _BadFactory, _DummyItemFactory) -# ) -# w.setModel( -# ["eval://localhost/@dummy/'test_itemfactory'", -# "eval://localhost/@dummy/'test_badfactory'", -# "eval:1", -# ] -# ) -# -# # handled by _DummyItemFactory -# assert type(w[0]) == _DummyTV -# # handled in _BadFactory (even if with worng return value) -# assert type(w[1]) == _DummyTV -# # errored in _BadFactory, ignored by _DummyItemFactory -# assert type(w[2]) == TaurusValue -# -# + +def test_form_itemFactory_selection(qtbot): + """Checks that the TaurusForm itemFactory selection API works""" + lines = ["test_Form_ItemFactorySel={}:_DummyItemFactory".format(__name__)] + group = "taurus.form.item_factories" + mapping = mock_entry_point(lines, group=group) + ep1 = mapping[group]["test_Form_ItemFactorySel"] + + w = TaurusForm() + qtbot.addWidget(w) + + # the test_Form_ItemFactory should be in the default factories + default_factories = w.getItemFactories() + assert ep1 in default_factories + + # no factories should be excluded by default + inc, exc = w.getItemFactories(return_disabled=True) + assert exc == [] + + # Check that we can deselect all factories + no_factories = w.setItemFactories(include=[]) + assert no_factories == [] + + # Check that we can exclude everything except test_Form_ItemFactory + select1 = w.setItemFactories( + exclude=[r"(?!.*test_Form_ItemFactorySel).*"] + ) + assert select1 == [ep1] + + # Check that we can include only test_Form_ItemFactory + select2 = w.setItemFactories(include=["test_Form_ItemFactorySel"]) + assert select2 == [ep1] + + # Check that the selected test_Form_ItemFactory is an entry point + from pkg_resources import EntryPoint + assert type(select2[0]) == EntryPoint + + # Check that the selected entry point loads _DummyItemFactory + assert select2[0].load() is _DummyItemFactory + + # Check that we can include a factory instance + select3 = w.setItemFactories(include=[_DummyItemFactory]) + + # Check that the selected test_Form_ItemFactory is an entry point-alike + from taurus.core.util.plugin import EntryPointAlike + assert type(select3[0]) == EntryPointAlike + + # Check that the selected entry point loads _DummyItemFactory + assert select3[0].load() is _DummyItemFactory + + # Check that the selected entry point has the given name + assert select3[0].name == repr(_DummyItemFactory) + + +def test_form_cwidget_bck_compat(qtbot): + """check that the cusomWidgetMap bck-compat works""" + + w = TaurusForm() + qtbot.addWidget(w) + + # check that custom widget map is empty by default + assert w.getCustomWidgetMap() == {} + + w.setItemFactories(include=()) + + # check that an explicit call to setCustomWidgetMap works + dummy = ("taurus.qt.qtgui.panel.test.test_taurusform._DummyTV", (), {}) + w.setCustomWidgetMap({"DataBase": dummy}) + w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) + qtbot.wait_until(lambda: len(w) == 2, timeout=3200) + assert type(w[0]) == _DummyTV + assert type(w[1]) == TaurusValue + assert w.getCustomWidgetMap() == {"DataBase": dummy} + + # check that the custom widget map can be restored + w.setCustomWidgetMap({}) + w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) + qtbot.wait_until(lambda: len(w) == 2, timeout=3200) + assert type(w[0]) == TaurusValue + assert type(w[1]) == TaurusValue + assert w.getCustomWidgetMap() == {} + + +def test_form_itemFactory_loading(qtbot): + """ + check that the factory loading is robust against unloadable plugins + and badly-implemented item factories + """ + + w = TaurusForm() + qtbot.addWidget(w) + + w.setItemFactories( + include=(_BadEntryPoint, _BadFactory, _DummyItemFactory) + ) + w.setModel( + ["eval://localhost/@dummy/'test_itemfactory'", + "eval://localhost/@dummy/'test_badfactory'", + "eval:1", + ] + ) + qtbot.wait_until(lambda: len(w) == 3, timeout=3200) + + # handled by _DummyItemFactory + assert type(w[0]) == _DummyTV + # handled in _BadFactory (even if with wrong return value) + assert type(w[1]) == _DummyTV + # errored in _BadFactory, ignored by _DummyItemFactory + assert type(w[2]) == TaurusValue + + From 26de611d1a74d06c6656c0d39ed3b85e7611d280 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 16:30:34 +0200 Subject: [PATCH 276/373] set model to None as a tear down Try to avoid problems with tango in atexit() by forcing a previous unsubscribe in the test itself --- .../qt/qtgui/panel/test/test_taurusform.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index b5cd005fa..a38d7211b 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -180,22 +180,27 @@ def test_form_cwidget_bck_compat(qtbot): w.setItemFactories(include=()) - # check that an explicit call to setCustomWidgetMap works - dummy = ("taurus.qt.qtgui.panel.test.test_taurusform._DummyTV", (), {}) - w.setCustomWidgetMap({"DataBase": dummy}) - w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) - qtbot.wait_until(lambda: len(w) == 2, timeout=3200) - assert type(w[0]) == _DummyTV - assert type(w[1]) == TaurusValue - assert w.getCustomWidgetMap() == {"DataBase": dummy} - - # check that the custom widget map can be restored - w.setCustomWidgetMap({}) - w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) - qtbot.wait_until(lambda: len(w) == 2, timeout=3200) - assert type(w[0]) == TaurusValue - assert type(w[1]) == TaurusValue - assert w.getCustomWidgetMap() == {} + try: + # check that an explicit call to setCustomWidgetMap works + dummy = ("taurus.qt.qtgui.panel.test.test_taurusform._DummyTV", (), {}) + w.setCustomWidgetMap({"DataBase": dummy}) + w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) + qtbot.wait_until(lambda: len(w) == 2, timeout=3200) + assert type(w[0]) == _DummyTV + assert type(w[1]) == TaurusValue + assert w.getCustomWidgetMap() == {"DataBase": dummy} + + # check that the custom widget map can be restored + w.setCustomWidgetMap({}) + w.setModel(["tango:sys/database/2", "tango:sys/tg_test/1"]) + qtbot.wait_until(lambda: len(w) == 2, timeout=3200) + assert type(w[0]) == TaurusValue + assert type(w[1]) == TaurusValue + assert w.getCustomWidgetMap() == {} + finally: + # set model to None as an attempt to avoid problems in atexit() + w.setModel(None) + qtbot.wait_until(lambda: len(w) == 0, timeout=3200) def test_form_itemFactory_loading(qtbot): From f6192026221ebbbe35258403b71a623447d63f9c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 16:38:16 +0200 Subject: [PATCH 277/373] TEMPORARY Add debugging prints --- lib/taurus/core/tango/tangoattribute.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 898585d04..007916627 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -679,7 +679,7 @@ def _process_event_exception(self, ex): def _subscribeChangeEvents(self): """ Enable subscription to the attribute events. If change events are not supported polling is activated """ - + print("!!!!! IN SUBSCRIBE_CHG", self.__chg_evt_id, self._full_name) if self.__chg_evt_id is not None: self.warning("chg events already subscribed (id=%s)" %self.__chg_evt_id) @@ -731,7 +731,7 @@ def _unsubscribeChangeEvents(self): # Careful in this method: This is intended to be executed in the cleanUp # so we should not access external objects from the factory, like the # parent object - + print("!!!!! IN UNSUBSCRIBE_CHG", self.__chg_evt_id, self._full_name) if self.__dev_hw_obj is not None and self.__chg_evt_id is not None: self.trace("Unsubscribing to change events (ID=%d)", self.__chg_evt_id) @@ -750,6 +750,7 @@ def _unsubscribeChangeEvents(self): self.__subscription_state = SubscriptionState.Unsubscribed def _subscribeConfEvents(self): + print("!!!!! IN SUBSCRIBE_CONF", self.__cfg_evt_id, self._full_name) """ Enable subscription to the attribute configuration events.""" self.trace("Subscribing to configuration events...") @@ -796,7 +797,7 @@ def _unsubscribeConfEvents(self): # Careful in this method: This is intended to be executed in the cleanUp # so we should not access external objects from the factory, like the # parent object - + print("!!!!! IN UNSUBSCRIBE_CONF", self.__cfg_evt_id, self._full_name) if self.__cfg_evt_id is not None and self.__dev_hw_obj is not None: self.trace("Unsubscribing to configuration events (ID=%s)", str(self.__cfg_evt_id)) From 718b2a7756e7d5f1d7115706dc5cda2ad339abcf Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 16:50:13 +0200 Subject: [PATCH 278/373] TEMPORARY Disable ouput capture in tests --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5662b8f28..f5bf21266 100644 --- a/tox.ini +++ b/tox.ini @@ -46,7 +46,7 @@ deps= py27: guiqwt py35: guiqwt commands= - python -m pytest lib/taurus --show-capture=no + python -m pytest lib/taurus -s [testenv:py37-qt5-docs] commands= From abdf6ee7e04cda68ef5ee707884b212de4362a8e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 27 May 2020 17:38:58 +0200 Subject: [PATCH 279/373] TEMPORARY: move debug print --- lib/taurus/core/tango/tangoattribute.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 007916627..cfe276d3f 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -679,7 +679,6 @@ def _process_event_exception(self, ex): def _subscribeChangeEvents(self): """ Enable subscription to the attribute events. If change events are not supported polling is activated """ - print("!!!!! IN SUBSCRIBE_CHG", self.__chg_evt_id, self._full_name) if self.__chg_evt_id is not None: self.warning("chg events already subscribed (id=%s)" %self.__chg_evt_id) @@ -724,7 +723,7 @@ def _call_dev_hw_subscribe_event(self, stateless=True): self.__chg_evt_id = self.__dev_hw_obj.subscribe_event( attr_name, PyTango.EventType.CHANGE_EVENT, _BoundMethodWeakrefWithCall(self.push_event), [], stateless) - + print("!!!!! IN DEV_HW SUBSCRIBE_CHG", self.__chg_evt_id, self._full_name, stateless) return self.__chg_evt_id def _unsubscribeChangeEvents(self): From ab6888bbba3910a68b040170f3aaf2523c7e051a Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 2 Jun 2020 12:56:50 +0200 Subject: [PATCH 280/373] Use pytango >= 9.3 for all tox targets - use cpascual conda forge (TEMPORARY) - set minimum version in tox.ini --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f5bf21266..8a5e99f3a 100644 --- a/tox.ini +++ b/tox.ini @@ -12,11 +12,12 @@ setenv= conda_channels= conda-forge tango-controls + cpascual conda_deps= qt4: pyqt=4 qt5: pyqt=5 ps2: pyside2 - pytango + pytango >= 9.3 pyepics guidata py27: cython From ef444c578a3486dfa4a7c51cca1d25e46722d68f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 13:19:23 +0200 Subject: [PATCH 281/373] re-enable some travis targets py35-qt5 py36-qt5 py37-qt5 py37-ps2 --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index b7c12f588..d9f0452d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,16 +16,16 @@ matrix: include: #- env: TOXENV=flake8 #- env: TOXENV=py27-qt4 - - env: TOXENV=py27-qt5 + #- env: TOXENV=py27-qt5 - env: TOXENV=py35-qt5 - #- env: TOXENV=py36-qt5 + - env: TOXENV=py36-qt5 - env: TOXENV=py37-qt5 - #- env: TOXENV=py37-ps2 + - env: TOXENV=py37-ps2 #- env: TOXENV=py37-qt5-docs allow_failures: #- env: TOXENV=flake8 #- env: TOXENV=py27-qt4 - - env: TOXENV=py27-qt5 + #- env: TOXENV=py27-qt5 #- env: TOXENV=py35-qt5 before_install: From 26a3102794b6d42692107ac47c07add4eef54dd9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 13:31:18 +0200 Subject: [PATCH 282/373] Revert "TEMPORARY: move debug print" This reverts commit abdf6ee7e04cda68ef5ee707884b212de4362a8e. --- lib/taurus/core/tango/tangoattribute.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index cfe276d3f..007916627 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -679,6 +679,7 @@ def _process_event_exception(self, ex): def _subscribeChangeEvents(self): """ Enable subscription to the attribute events. If change events are not supported polling is activated """ + print("!!!!! IN SUBSCRIBE_CHG", self.__chg_evt_id, self._full_name) if self.__chg_evt_id is not None: self.warning("chg events already subscribed (id=%s)" %self.__chg_evt_id) @@ -723,7 +724,7 @@ def _call_dev_hw_subscribe_event(self, stateless=True): self.__chg_evt_id = self.__dev_hw_obj.subscribe_event( attr_name, PyTango.EventType.CHANGE_EVENT, _BoundMethodWeakrefWithCall(self.push_event), [], stateless) - print("!!!!! IN DEV_HW SUBSCRIBE_CHG", self.__chg_evt_id, self._full_name, stateless) + return self.__chg_evt_id def _unsubscribeChangeEvents(self): From f96c18bbda78fd20eace2f6c2f0c8ce6f57e1e28 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 13:34:02 +0200 Subject: [PATCH 283/373] Revert "TEMPORARY Add debugging prints" This reverts commit f6192026221ebbbe35258403b71a623447d63f9c. --- lib/taurus/core/tango/tangoattribute.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 007916627..898585d04 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -679,7 +679,7 @@ def _process_event_exception(self, ex): def _subscribeChangeEvents(self): """ Enable subscription to the attribute events. If change events are not supported polling is activated """ - print("!!!!! IN SUBSCRIBE_CHG", self.__chg_evt_id, self._full_name) + if self.__chg_evt_id is not None: self.warning("chg events already subscribed (id=%s)" %self.__chg_evt_id) @@ -731,7 +731,7 @@ def _unsubscribeChangeEvents(self): # Careful in this method: This is intended to be executed in the cleanUp # so we should not access external objects from the factory, like the # parent object - print("!!!!! IN UNSUBSCRIBE_CHG", self.__chg_evt_id, self._full_name) + if self.__dev_hw_obj is not None and self.__chg_evt_id is not None: self.trace("Unsubscribing to change events (ID=%d)", self.__chg_evt_id) @@ -750,7 +750,6 @@ def _unsubscribeChangeEvents(self): self.__subscription_state = SubscriptionState.Unsubscribed def _subscribeConfEvents(self): - print("!!!!! IN SUBSCRIBE_CONF", self.__cfg_evt_id, self._full_name) """ Enable subscription to the attribute configuration events.""" self.trace("Subscribing to configuration events...") @@ -797,7 +796,7 @@ def _unsubscribeConfEvents(self): # Careful in this method: This is intended to be executed in the cleanUp # so we should not access external objects from the factory, like the # parent object - print("!!!!! IN UNSUBSCRIBE_CONF", self.__cfg_evt_id, self._full_name) + if self.__cfg_evt_id is not None and self.__dev_hw_obj is not None: self.trace("Unsubscribing to configuration events (ID=%s)", str(self.__cfg_evt_id)) From 48b84028cf19a71e21a4474c0255f9015ccaeecc Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 2 Jun 2020 19:21:01 +0200 Subject: [PATCH 284/373] Refactor TaurusCombobox tests Use pytest-qt and parameterize. Also avoid segfaults in pyside2 tests due to PYSIDE-683 bug --- .../qtgui/input/test/test_tauruscombobox.py | 115 ++++++++++-------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py b/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py index 94167ca2b..84a29fed9 100644 --- a/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py +++ b/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py @@ -1,63 +1,72 @@ from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.input import TaurusValueComboBox +from taurus.core.units import UR import taurus +import pytest +from taurus.external.qt import PYSIDE2 -def test_TaurusValueCombobox(): + +@pytest.mark.parametrize( + "model,names,value,expected", + [ + # test with quantities + ('eval:@taurus.core.evaluation.test.res.mymod.MyClass()/self.foo', + [("A", 1234), ("B", "123"), ("C", 123 * UR.mm), ("E", -123)], + 123 * UR.mm, + "C", + ), + # test with a boolean + ('sys/tg_test/1/boolean_scalar', + [("N", None), ("F", False), ("T", True)], + False, + "F", + ), + # test with a boolean spectrum + ('sys/tg_test/1/boolean_spectrum', + [ + ("A", False), + ("B", [False, False, False]), + ("C", [False, True, False]), + ], + [False, True, False], + "C", + ), + # test with a string + ('sys/tg_test/1/string_scalar', + [("A", "foobarbaz"), ("B", "FOOBAR"), ("C", "foobar")], + "foobar", + "C", + ), + # test non-match + ('sys/tg_test/1/string_scalar', + [("A", "foobarbaz"), ("B", "FOOBAR"), ("C", "foobar")], + "foo", + "", + ), + ] +) +def test_TaurusValueCombobox(qtbot, model, names, value, expected): """Check that the TaurusValueComboBox is started with the right display See https://github.com/taurus-org/taurus/pull/1032 """ - # TODO: Parameterize this test - app = TaurusApplication.instance() - if app is None: - app = TaurusApplication(cmd_line_parser=None) - - # test with a pint quantity - model = 'sys/tg_test/1/short_scalar' a = taurus.Attribute(model) - units = a.write(123).wvalue.units + a.write(value) w = TaurusValueComboBox() - names = [("A", 1234), ("B", "123"), ("C", 123 * units), ("E", -123)] + qtbot.addWidget(w) + # ---------------------------------- + # workaround: avoid PySide2 segfaults when adding quantity to combobox + # https://bugreports.qt.io/browse/PYSIDE-683 + if isinstance(value, UR.Quantity) and PYSIDE2: + pytest.skip("avoid segfault due to PYSIDE-683 bug") + # ---------------------------------- w.addValueNames(names) - w.setModel(model) - assert(w.currentText() == "C") - - # test with a boolean (using quantities) - model = 'sys/tg_test/1/boolean_scalar' - a = taurus.Attribute(model) - a.write(False) - w = TaurusValueComboBox() - w.addValueNames([("N", None), ("F", False), ("T", True)]) - w.setModel(model) - assert (w.currentText() == "F") - - # test with a spectrum - model = 'sys/tg_test/1/boolean_spectrum' - a = taurus.Attribute(model) - a.write([False, True, False]) - w = TaurusValueComboBox() - w.addValueNames([ - ("A", False), - ("B", [False, False, False]), - ("C", [False, True, False]), - ]) - w.setModel(model) - assert (w.currentText() == "C") - - # test with strings - model = 'sys/tg_test/1/string_scalar' - a = taurus.Attribute(model) - a.write("foobar") - w = TaurusValueComboBox() - w.addValueNames([("A", "foobarbaz"), ("B", "FOOBAR"), ("C", "foobar")]) - w.setModel(model) - assert (w.currentText() == "C") - - # test non-match - model = 'sys/tg_test/1/string_scalar' - a = taurus.Attribute(model) - a.write("foo") - w = TaurusValueComboBox() - w.addValueNames([("A", "foobarbaz"), ("B", "FOOBAR"), ("C", "foobar")]) - w.setModel(model) - assert (w.currentText() == "") - w.setModel(None) \ No newline at end of file + qtbot.wait_until(lambda: w.count() == len(names), timeout=3200) + try: + with qtbot.waitSignal(w.taurusEvent, timeout=3200): + w.setModel(model) + assert w.currentText() == expected + finally: + a = None + # set model to None as an attempt to avoid problems in atexit() + with qtbot.waitSignal(w.taurusEvent, timeout=3200): + w.setModel(None) From b5fdc75e028d6337bdc49fbb03f7b7494a783a16 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 2 Jun 2020 19:47:06 +0200 Subject: [PATCH 285/373] Try fixing failure in combobox test The last refactoring introduced a failure in the last paramterization of test_TaurusValueCombobox. Try to fix it --- lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py b/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py index 84a29fed9..c8d938b19 100644 --- a/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py +++ b/lib/taurus/qt/qtgui/input/test/test_tauruscombobox.py @@ -62,11 +62,11 @@ def test_TaurusValueCombobox(qtbot, model, names, value, expected): w.addValueNames(names) qtbot.wait_until(lambda: w.count() == len(names), timeout=3200) try: - with qtbot.waitSignal(w.taurusEvent, timeout=3200): + with qtbot.waitSignal(w.valueChangedSignal, timeout=3200): w.setModel(model) - assert w.currentText() == expected + assert w.currentText() == expected finally: - a = None + del a # set model to None as an attempt to avoid problems in atexit() - with qtbot.waitSignal(w.taurusEvent, timeout=3200): + with qtbot.waitSignal(w.valueChangedSignal, timeout=3200): w.setModel(None) From b2524c2057c4d114a9ae9e0f10c1db94d515562b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 2 Jun 2020 20:45:44 +0200 Subject: [PATCH 286/373] Fix failures in py37-pyside2 tests The introduction of pytest-qt triggered several hidden issues in the PySide2 tests. Fix (or avoid) them --- lib/taurus/external/qt/QtDesigner.py | 2 +- lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py | 12 +++++------- lib/taurus/test/test_import.py | 5 +++++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/taurus/external/qt/QtDesigner.py b/lib/taurus/external/qt/QtDesigner.py index 6e3e60e43..73d7ae22f 100644 --- a/lib/taurus/external/qt/QtDesigner.py +++ b/lib/taurus/external/qt/QtDesigner.py @@ -17,6 +17,6 @@ elif PYQT4: from PyQt4.QtDesigner import * else: - raise PythonQtError('No Qt bindings could be found') + raise PythonQtError('No compatible Qt bindings could be found') del PYQT4, PYQT5, PythonQtError diff --git a/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py index 4ed2309a0..20ee227ee 100644 --- a/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py @@ -1,9 +1,6 @@ -from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.taurusgui import TaurusGui, utils - -app = TaurusApplication.instance() -if app is None: - app = TaurusApplication([], cmd_line_parser=None) +from taurus.external.qt import PYSIDE2 +import pytest p1 = utils.PanelDescription( "testpanel1", @@ -15,10 +12,11 @@ } ) - -def test_paneldescription(): +@pytest.mark.xfail(PYSIDE2, reason="This test is known to fail with PySide2") +def test_paneldescription(qtbot): gui = TaurusGui(confname=__file__, configRecursionDepth=0) w1 = gui.getPanel('testpanel1').widget() + qtbot.addWidget(w1) assert w1.withButtons is False assert w1.isWithButtons() is False assert not hasattr(w1, "foobar") diff --git a/lib/taurus/test/test_import.py b/lib/taurus/test/test_import.py index 34fccb381..4173d8326 100644 --- a/lib/taurus/test/test_import.py +++ b/lib/taurus/test/test_import.py @@ -64,6 +64,11 @@ def testImportSubmodules(self): except ImportError: exclude_patterns.append(r'taurus\.core\.epics') + from taurus.external.qt import PYSIDE2 + if PYSIDE2: + exclude_patterns.append(r'taurus\.external\.qt\.QtDesigner') + exclude_patterns.append(r'taurus\.qt\.qtdesigner') + moduleinfo, wrn = self.explore('taurus', verbose=False, exclude_patterns=exclude_patterns) msg = None From 2de66a20d948370422eb73853453594ada448d73 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 05:21:39 +0200 Subject: [PATCH 287/373] Refactor test_jdraw_parser - Use pytest-qt - Skip if using PySide2 (to avoid segfault) --- .../qtgui/graphic/jdraw/test/test_jdraw_parser.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/taurus/qt/qtgui/graphic/jdraw/test/test_jdraw_parser.py b/lib/taurus/qt/qtgui/graphic/jdraw/test/test_jdraw_parser.py index 0abf30b3a..1ad202699 100644 --- a/lib/taurus/qt/qtgui/graphic/jdraw/test/test_jdraw_parser.py +++ b/lib/taurus/qt/qtgui/graphic/jdraw/test/test_jdraw_parser.py @@ -1,5 +1,8 @@ import os -from taurus.qt.qtgui.application import TaurusApplication +from taurus.external.qt.QtCore import PYSIDE2 +import pytest + + from taurus.qt.qtgui.graphic.jdraw import ( TaurusJDrawGraphicsFactory, jdraw_parser @@ -7,12 +10,8 @@ from taurus.qt.qtgui.graphic import TaurusGraphicsScene -app = TaurusApplication.instance() -if app is None: - app = TaurusApplication([], cmd_line_parser=None) - - -def test_jdraw_parser(): +@pytest.mark.skipif(PYSIDE2, reason="Avoid segfault when using PySide2") +def test_jdraw_parser(qtbot): """Check that jdraw_parser does not break with JDBar elements""" fname = os.path.join(os.path.dirname(__file__), "res", "bug1077.jdw") factory = TaurusJDrawGraphicsFactory(None) From 8efbe87516d93823220455adc23c7a7907779466 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 05:19:56 +0200 Subject: [PATCH 288/373] Implement safer pyqtProperty for PySide The taurus.external.qt.QtCore shim simply aliases pyqtProperty to Property when using the PySide2 binding. This produces segfaults during exit of tests if Property is called with the doc kwarg. Workaround by implementing pyqtProperty as a wrapper of Property that avoids passing the doc kwarg. --- lib/taurus/external/qt/QtCore.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/taurus/external/qt/QtCore.py b/lib/taurus/external/qt/QtCore.py index 1a902f7ea..f7fa16a2b 100644 --- a/lib/taurus/external/qt/QtCore.py +++ b/lib/taurus/external/qt/QtCore.py @@ -50,7 +50,17 @@ def to_qvariant(pyobj=None): from PySide2.QtCore import * from PySide2.QtCore import Signal as pyqtSignal from PySide2.QtCore import Slot as pyqtSlot - from PySide2.QtCore import Property as pyqtProperty + + # ------------------------------------------------ + # Calling Property with doc="" produces segfaults in the tests. + # As a workaround, just remove the doc kwarg. Note this is an ad-hoc + # workaround: I could not find the API definition for + # PySide.QtCore.Property in order to do a more complete mock + def pyqtProperty(*args, **kwargs): + kwargs.pop('doc', None) + return Property(*args, **kwargs) + # ------------------------------------------------- + try: # may be limited to PySide-5.11a1 only from PySide2.QtGui import QStringListModel except: From 9685e47181494e17c972a040658ab0b57966fbf1 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 17:30:04 +0200 Subject: [PATCH 289/373] re-enable doc travis target --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d9f0452d9..708029f6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ matrix: - env: TOXENV=py36-qt5 - env: TOXENV=py37-qt5 - env: TOXENV=py37-ps2 - #- env: TOXENV=py37-qt5-docs + - env: TOXENV=py37-qt5-docs allow_failures: #- env: TOXENV=flake8 #- env: TOXENV=py27-qt4 From 3024abcc34ab3e4b94099d48af27bc9c15b5445f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 18:15:12 +0200 Subject: [PATCH 290/373] re-enable output capturing in pytest --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8a5e99f3a..a98dbf062 100644 --- a/tox.ini +++ b/tox.ini @@ -47,7 +47,7 @@ deps= py27: guiqwt py35: guiqwt commands= - python -m pytest lib/taurus -s + python -m pytest lib/taurus [testenv:py37-qt5-docs] commands= From 14d94b8dc12a19b3ba5bfc28d9b23fcc176c6bea Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 19:14:59 +0200 Subject: [PATCH 291/373] Do not assume TaurusApplication in taurusgui.py TaurusGui.loadConfiguration fails if the QApplication is not a TaurusApplication. This breaks the taurusgui test. Protect against this case --- lib/taurus/qt/qtgui/taurusgui/taurusgui.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py index 296a8af98..b51b2296f 100644 --- a/lib/taurus/qt/qtgui/taurusgui/taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/taurusgui.py @@ -1060,7 +1060,12 @@ def loadConfiguration(self, confname): self._loadAppName(conf, confname, xmlroot) self._loadOrgName(conf, xmlroot) self._loadCustomLogo(conf, xmlroot) - Qt.QApplication.instance().basicConfig() + + # do some extra config if we have a TaurusApplication + _app = Qt.QApplication.instance() + if hasattr(_app, 'basicConfig'): + _app.basicConfig() + self._loadOrgLogo(conf, xmlroot) self._loadSingleInstance(conf, xmlroot) From ae184363e2cbdcdd85b96172338539ca204dfeb0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 19:15:55 +0200 Subject: [PATCH 292/373] Register gui widget in test_paneldescription --- lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py b/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py index 20ee227ee..996ddad48 100644 --- a/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py +++ b/lib/taurus/qt/qtgui/taurusgui/test/test_taurusgui.py @@ -16,6 +16,7 @@ def test_paneldescription(qtbot): gui = TaurusGui(confname=__file__, configRecursionDepth=0) w1 = gui.getPanel('testpanel1').widget() + qtbot.addWidget(gui) qtbot.addWidget(w1) assert w1.withButtons is False assert w1.isWithButtons() is False From a2af7bcb0c6794cc0aaf9e27226202d01a878ef3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 4 Jun 2020 15:28:44 +0200 Subject: [PATCH 293/373] Replace TimeOut DS by TangoSchemeTest DS - Add Sleep command to TangoSchemeTest DS - Deprecate TimeOut DS --- lib/taurus/core/tango/starter.py | 9 +++++---- lib/taurus/core/tango/test/res/TangoSchemeTest | 5 +++++ lib/taurus/qt/qtgui/button/test/res/Timeout | 8 ++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/taurus/core/tango/starter.py b/lib/taurus/core/tango/starter.py index c5ec6489f..86f195e94 100644 --- a/lib/taurus/core/tango/starter.py +++ b/lib/taurus/core/tango/starter.py @@ -231,10 +231,10 @@ def hardKill(self): if __name__ == '__main__': from taurus.test.resource import getResourcePath - timeoutExec = getResourcePath('taurus.qt.qtgui.button.test.res', 'Timeout') - s = ProcessStarter(timeoutExec, 'Timeout/test_removeme') - devname = 'testing/timeout/temp-1' - s.addNewDevice(devname, klass='Timeout') + exe = getResourcePath('taurus.core.tango.test.res', 'TangoSchemeTest') + s = ProcessStarter(exe, 'TangoSchemeTest/test_removeme') + devname = 'testing/tangoschemetest/temp-1' + s.addNewDevice(devname, klass='TangoSchemeTest') s.startDs() try: print('Is running:', s.isRunning()) @@ -243,3 +243,4 @@ def hardKill(self): print(e) s.stopDs() s.cleanDb(force=False) + diff --git a/lib/taurus/core/tango/test/res/TangoSchemeTest b/lib/taurus/core/tango/test/res/TangoSchemeTest index 467fce31b..cba6710d9 100755 --- a/lib/taurus/core/tango/test/res/TangoSchemeTest +++ b/lib/taurus/core/tango/test/res/TangoSchemeTest @@ -581,6 +581,11 @@ class TangoSchemeTest(with_metaclass(__DeviceMeta, Device)): } self._quality = quality_states.get(quality) or AttrQuality.ATTR_VALID + @command(dtype_in=float, dtype_out=float) + def Sleep(self, seconds): + sleep(seconds) + return seconds + @command def Reset(self): """ Reset the attributes value and configuration parameters diff --git a/lib/taurus/qt/qtgui/button/test/res/Timeout b/lib/taurus/qt/qtgui/button/test/res/Timeout index 860b41ba7..ab305e729 100755 --- a/lib/taurus/qt/qtgui/button/test/res/Timeout +++ b/lib/taurus/qt/qtgui/button/test/res/Timeout @@ -24,6 +24,8 @@ ############################################################################# """ +This module is deprecated. Use the Sleep command of TangoSchemeTest instead + This module contains classes necessary by a simple python Tango Device Server. TimeoutDs is used to provoke timeout exceptions on requests. @@ -36,9 +38,11 @@ The attibute value is the time that will take the read/write the attribute. TimeoutCmd expects as an argument a float scalar. The argument value is the time that will take to execute the command. -#@TODO: create .bat file to allow starting on windows - """ +from taurus.core.util.log import deprecated + +deprecated(dep='Timeout DS', alt='TangoSchemeTest.Sleep', rel="4.6.5") + import PyTango # Tango-centric import sys From 7108c799707af4d5cb8338f912759debc23f2ab3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 4 Jun 2020 15:32:27 +0200 Subject: [PATCH 294/373] Refactor TaurusCommandButton tests Reimplement using pytest and pytest-qt. Use TangoSchemeTest DS instead of Timeout DS --- .../qtgui/button/test/test_commandbutton.py | 91 +++++++++++++++ .../qt/qtgui/button/test/test_taurusbutton.py | 104 ------------------ 2 files changed, 91 insertions(+), 104 deletions(-) create mode 100644 lib/taurus/qt/qtgui/button/test/test_commandbutton.py delete mode 100644 lib/taurus/qt/qtgui/button/test/test_taurusbutton.py diff --git a/lib/taurus/qt/qtgui/button/test/test_commandbutton.py b/lib/taurus/qt/qtgui/button/test/test_commandbutton.py new file mode 100644 index 000000000..45ee14b6d --- /dev/null +++ b/lib/taurus/qt/qtgui/button/test/test_commandbutton.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + + +"""Unit tests for TaurusCommandButton""" + + +import pytest +from functools import partial +from taurus.qt.qtgui.button import TaurusCommandButton +from taurus.qt.qtgui.test.base import taurus_widget_set_models +from taurus.external.qt import Qt + +try: + # The following are Tango-centric imports. + # TODO: change them if/when TaurusCommandbuttongets generalized + from PyTango import CommunicationFailed + from taurus.core.tango.test import taurus_test_ds # pytest fixture + _TANGO = False +except: + _TANGO = True + + +# TODO: move to another module where we parameterize taurus_widget_set_models +# for a lot of widgets +test_models = partial( + taurus_widget_set_models, + klass=TaurusCommandButton, + models=['sys/tg_test/1', None, 'sys/database/2', ''] +) + + +@pytest.mark.skipIf(_TANGO, reason="tango-dependent test") +def test_timeout(qtbot, taurus_test_ds): + """Check that the timeout property works""" + w = TaurusCommandButton(command='Sleep') + qtbot.addWidget(w) + + try: + w.setModel(taurus_test_ds) + + # set the parameter for the sleep command to 0.2 seconds + w.setParameters([.2]) + + # Create a callback to check the result of the execution + def _check_result(res): + return res == .2 + + # Check that the button emits commandExecuted when timeout > exec time + w.setTimeout(10) + assert w.getTimeout() == 10 + with qtbot.waitSignal(w.commandExecuted, check_params_cb=_check_result): + qtbot.mouseClick(w, Qt.Qt.LeftButton) + + # Check that, if timeout < exec time, commandExecuted is NOT emited + # and CommunicationFailed is raised + w.setTimeout(.1) + assert w.getTimeout() == .1 + with pytest.raises(CommunicationFailed): + with qtbot.assertNotEmitted(w.commandExecuted): + # call _onClicked instead of emulating a click to bypass + # the @ProtectTaurusMessageBox protection of onClicked() + w._onClicked() + finally: + # just in case this helps avoiding problems at exit + w.setModel(None) + + + diff --git a/lib/taurus/qt/qtgui/button/test/test_taurusbutton.py b/lib/taurus/qt/qtgui/button/test/test_taurusbutton.py deleted file mode 100644 index 602e8f463..000000000 --- a/lib/taurus/qt/qtgui/button/test/test_taurusbutton.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python - -############################################################################# -## -# This file is part of Taurus -## -# http://taurus-scada.org -## -# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain -## -# Taurus is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -## -# Taurus is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -## -# You should have received a copy of the GNU Lesser General Public License -# along with Taurus. If not, see . -## -############################################################################# - - -"""Unit tests for taurus.button""" - - -import unittest - -from taurus.test import getResourcePath -from taurus.qt.qtgui.test import BaseWidgetTestCase, GenericWidgetTestCase -from taurus.qt.qtgui.button import TaurusCommandButton - -skip, skipmsg = False, None - -try: - # The following are Tango-centric imports. - # TODO: change them if/when TaurusCommandbuttongets generalized - from PyTango import CommunicationFailed - from taurus.core.tango.starter import ProcessStarter -except: - skip = True - skipmsg = "tango-dependent test" - - -class TaurusCommandButtonTest(GenericWidgetTestCase, unittest.TestCase): - _klass = TaurusCommandButton - _modelnames = ['sys/tg_test/1', None, 'sys/database/2', ''] - - -@unittest.skipIf(skip, skipmsg) -class TaurusCommandButtonTest2(BaseWidgetTestCase, unittest.TestCase): - - _klass = TaurusCommandButton - initkwargs = dict(command='TimeoutCmd') - - def setUp(self): - ''' - Requisites: - - instantiate the widget - - make sure that the the timeout server is ready - ''' - # Call base class setup (instantiate the widget,...) - BaseWidgetTestCase.setUp(self) - # get path to DS and executable - timeoutExec = getResourcePath('taurus.qt.qtgui.button.test.res', - 'Timeout') - # create starter for the Timeout server - self._starter = ProcessStarter(timeoutExec, 'Timeout/unittest') - # register timeoutserver #TODO: guarantee that devname is not in use - devname = 'unittests/timeout/temp-1' - self._starter.addNewDevice(devname, klass='Timeout') - # start Timeout server - self._starter.startDs() - - # Configure the widget - self._widget.setModel(devname) - - def tearDown(self): - '''Stop the timeout server and undo changes to the database''' - - self._widget.setModel(None) - # remove timeoutserver - self._starter.cleanDb(force=True) - - def testTimeOutError(self): - '''Check that the timeout property works''' - # lets use commands that take at least 200ms in returning - self._widget.setParameters([.2]) - # With a long timeout it should work... - self._widget.setTimeout(10) - ret = self._widget._onClicked() - msg = 'expected return None when timeout >> command response time' - self.assertIsNone(ret, msg) - #...but with a shorter timeout we expect a timeout exception - self._widget.setTimeout(.1) - self.assertRaises(CommunicationFailed, self._widget._onClicked) - - -if __name__ == "__main__": - #import sys;sys.argv = ['', 'Test.testName'] - unittest.main() From 66f196a7d2bc7ad145fb44d118a61d9cf006501f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 8 Jun 2020 18:10:26 +0200 Subject: [PATCH 295/373] Add generic widget tests file Implement test_set_models to replace GenericWidgetTestCase Parameterize it for TaurusValue and TaurusCommandButton. Tests for other widges are expected to be added later on. --- .../qtgui/button/test/test_commandbutton.py | 15 +--- .../qt/qtgui/test/test_widgets_general.py | 84 +++++++++++++++++++ 2 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 lib/taurus/qt/qtgui/test/test_widgets_general.py diff --git a/lib/taurus/qt/qtgui/button/test/test_commandbutton.py b/lib/taurus/qt/qtgui/button/test/test_commandbutton.py index 45ee14b6d..8870b2a2d 100644 --- a/lib/taurus/qt/qtgui/button/test/test_commandbutton.py +++ b/lib/taurus/qt/qtgui/button/test/test_commandbutton.py @@ -38,21 +38,12 @@ # TODO: change them if/when TaurusCommandbuttongets generalized from PyTango import CommunicationFailed from taurus.core.tango.test import taurus_test_ds # pytest fixture - _TANGO = False + _TANGO_MISSING = False except: - _TANGO = True + _TANGO_MISSING = True -# TODO: move to another module where we parameterize taurus_widget_set_models -# for a lot of widgets -test_models = partial( - taurus_widget_set_models, - klass=TaurusCommandButton, - models=['sys/tg_test/1', None, 'sys/database/2', ''] -) - - -@pytest.mark.skipIf(_TANGO, reason="tango-dependent test") +@pytest.mark.skipIf(_TANGO_MISSING, reason="tango-dependent test") def test_timeout(qtbot, taurus_test_ds): """Check that the timeout property works""" w = TaurusCommandButton(command='Sleep') diff --git a/lib/taurus/qt/qtgui/test/test_widgets_general.py b/lib/taurus/qt/qtgui/test/test_widgets_general.py new file mode 100644 index 000000000..4e17d5b19 --- /dev/null +++ b/lib/taurus/qt/qtgui/test/test_widgets_general.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +""" +Generic tests (instantiation, set models, etc) for several widgets +""" + +import pytest +from importlib import import_module +import taurus +from taurus.test.pytest import check_taurus_deprecations + + +EMPTY = '' +TGT = 'tango:sys/tg_test/1' +DB = 'sys/database/2' +TGT_F_SC = 'tango:sys/tg_test/1/' +TGT_F_SP = 'tango:sys/tg_test/1/' +TGT_WAVE = 'tango:sys/tg_test/1/wave' +EV_INT = 'eval:123' +EV_Q = 'eval:1.23*UR.mV' + +# TODO: create model objects in setup_module to speed up + + +def _import_obj(obj_str, package="taurus.qt.qtgui"): + """ + returns objects described by a string like "[:]" + """ + if ":" in obj_str: + modname, oname = obj_str.split(":") + return getattr(import_module(modname, package=package), oname) + else: + return import_module(obj_str, package=package) + + +@pytest.mark.parametrize( + "widgetname,depr,models", + [ + (".display:TaurusLabel", 0, [TGT_WAVE, "", EV_INT, None]), + (".button:TaurusCommandButton", 0, [TGT, "", DB, None]), + ] +) +def test_set_models(qtbot, caplog, widgetname, depr, models): + """ + Generic test that checks that a widget can be instantiated and given + its setModel called sequentially. + + It can be parameterized or run with functools.partial + """ + with check_taurus_deprecations(caplog, expected=depr): + klass = _import_obj(widgetname) + w = klass() + qtbot.addWidget(w) + + for model in models: + if not model: + model_obj = None + else: + model_obj = taurus.Object(model) + w.setModel(model) + assert w.getModelObj() == model_obj From e48a2d91bef948bfb453138add9349de6d04739b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 8 Jun 2020 21:07:10 +0200 Subject: [PATCH 296/373] Protect TaurusLabel against issues with state Sometimes (specially during deletion) the TaurusLabel may be required to update the foreground or background at a moment in which the device state is not set. This raises exceptions if the label fg or bg is set to state. Protect these specific situations. --- lib/taurus/qt/qtgui/base/tauruscontroller.py | 5 ++++- lib/taurus/qt/qtgui/display/tauruslabel.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/base/tauruscontroller.py b/lib/taurus/qt/qtgui/base/tauruscontroller.py index da84afdf7..2ba86dfa1 100644 --- a/lib/taurus/qt/qtgui/base/tauruscontroller.py +++ b/lib/taurus/qt/qtgui/base/tauruscontroller.py @@ -327,7 +327,10 @@ def updateLabelBackground(ctrl, widget): palette = QT_ATTRIBUTE_QUALITY_PALETTE bgItem = ctrl.quality() elif bgRole == 'state': - bgItem = ctrl.state() + try: + bgItem = ctrl.state() + except AttributeError: + pass # protect against calls with state not instantiated elif bgRole == 'value': bgItem = ctrl.value() else: diff --git a/lib/taurus/qt/qtgui/display/tauruslabel.py b/lib/taurus/qt/qtgui/display/tauruslabel.py index 999f09bc0..e9c6ce486 100644 --- a/lib/taurus/qt/qtgui/display/tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/tauruslabel.py @@ -91,7 +91,10 @@ def _updateForeground(self, label): # handle special cases (that are not covered with fragment) if fgRole.lower() == 'state': - value = self.state().name + try: + value = self.state().name + except AttributeError: + pass # protect against calls with state not instantiated elif fgRole.lower() in ('', 'none'): pass else: From c7bb8181366bc41e83d00d3b250ed6cfe40c2d2f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 8 Jun 2020 22:47:33 +0200 Subject: [PATCH 297/373] Refactor TaurusLabel tests Reimplement using pytest and pytest-qt. The bug169 tests (use-parentModel) are not reimplemented --- .../qt/qtgui/display/test/test_tauruslabel.py | 451 +++++++----------- 1 file changed, 168 insertions(+), 283 deletions(-) diff --git a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py index e231faa3b..c0ce5757d 100644 --- a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py @@ -22,301 +22,186 @@ ## ############################################################################# - """Unit tests for Taurus Label""" -import unittest -from taurus.external.qt import Qt -from taurus.test import insertTest -from taurus.qt.qtgui.test import GenericWidgetTestCase, BaseWidgetTestCase + from taurus.qt.qtgui.display import TaurusLabel -from taurus.qt.qtgui.container import TaurusWidget -from taurus.core.tango.test import TangoSchemeTestLauncher -import functools from taurus.core.util.colors import ATTRIBUTE_QUALITY_DATA, DEVICE_STATE_DATA +from taurus.external.qt import Qt from pint import UnitRegistry as ur import numpy -DEV_NAME = TangoSchemeTestLauncher.DEV_NAME - - -class TaurusLabelTest(GenericWidgetTestCase, unittest.TestCase): - - ''' - Generic tests for TaurusLabel. - - .. seealso: :class:`taurus.qt.qtgui.test.base.GenericWidgetTestCase` - ''' - _klass = TaurusLabel - modelnames = ['sys/tg_test/1/wave', '', 'eval:1', None] - - -class Bug169_Test(BaseWidgetTestCase, unittest.TestCase): - - ''' - Test bug169: See: http://sf.net/p/tauruslib/tickets/65/ - (aka, Sradana's bug169) - - - .. seealso: :class:`taurus.qt.qtgui.test.base.BaseWidgetTestCase` - ''' - _klass = TaurusLabel - - def setUp(self): - BaseWidgetTestCase.setUp(self) - self._widget.setModel('sys/tg_test/1/double_scalar#label') - self._expectedModelClass = self._widget.getModelClass() - self._parent = TaurusWidget() - self._parent.setModel('sys/tg_test/1') - self._widget.setUseParentModel(True) - self._widget.setParent(self._parent) - - def test_bug169(self): - '''Check if setModel works when using parent model''' - self._widget.setModel('/double_scalar#label') - self.assertMaxDeprecations(1) - - def test_relativemodelclass(self): - '''Check consistency in modelClass when using parent model (re: bug169) - ''' - try: - self._widget.setModel('/double_scalar#label') - finally: - mc = self._widget.getModelClass() - msg = ('getModelClass() inconsistency:\n expected: %s\n got: %s' % - (self._expectedModelClass, mc)) - self.assertEqual(self._expectedModelClass, mc, msg) - self.assertMaxDeprecations(1) - - -# ------------------------------------------------------------------------------ -# Check bck-compat with pre-tep14 FgRoles: value, w_value, state, -# quality, none -testOldFgroles = functools.partial(insertTest, helper_name='text', maxdepr=1, - model='tango:' + DEV_NAME + '/double_scalar') - - -@testOldFgroles(fgRole='value', expected='1.23 mm') -@testOldFgroles(fgRole='w_value', expected='0.00 mm') -@testOldFgroles(fgRole='state', expected='Ready') -@testOldFgroles(fgRole='quality', expected='ATTR_VALID') -@testOldFgroles(fgRole='none', expected='') -# ------------------------------------------------------------------------------ -@insertTest(helper_name='text', - model='tango:' + DEV_NAME + '/double_image#rvalue[1,::2]', - expected='{:~}'.format(numpy.array([1.23,1.23])*ur().mm) - # expected is not explicit to support pint v<0.8 particularities - ) -@insertTest(helper_name='text', - model='tango:' + DEV_NAME + '/double_spectrum#rvalue[1]', - expected='1.23 mm') -@insertTest(helper_name='text', - model='tango:' + DEV_NAME + '/double_image', - modelIndex=(1,1), - expected='1.23 mm') -@insertTest(helper_name='text', - model='tango:' + DEV_NAME + '/double_spectrum', - modelIndex=1, - expected='1.23 mm') -@insertTest(helper_name='text', - model='tango:' + DEV_NAME + '/double_scalar#state', - expected='Ready') -@insertTest(helper_name='text', - model='tango:' + DEV_NAME + '/double_scalar#rvalue', - fgRole='label', - expected='double_scalar') -@insertTest(helper_name='text', - model='tango:' + DEV_NAME + '/double_scalar', - fgRole='label', - expected='double_scalar') -@insertTest(helper_name='text', - model='tango:' + DEV_NAME + '/double_scalar#label', - expected='double_scalar') -# ------------------------------------------------------------------------------ -# Check bck-compat with pre-tep14 BgRoles: state, quality, none -@insertTest(helper_name='bgRole', - model='tango:' + DEV_NAME + '/float_scalar_ro', - bgRole='none', - expected=Qt.QColor(Qt.Qt.transparent).getRgb()[:3]) -@insertTest(helper_name='bgRole', - model='tango:' + DEV_NAME + '/float_scalar_ro', - bgRole='state', - expected=DEVICE_STATE_DATA["TaurusDevState.Ready"][1:4]) -@insertTest(helper_name='bgRole', - model='tango:' + DEV_NAME + '/float_scalar_ro', - bgRole='quality', - expected=ATTRIBUTE_QUALITY_DATA["ATTR_VALID"][1:4]) -@insertTest(helper_name='bgRole', - model='tango:' + DEV_NAME + '/float_scalar_ro', - expected=ATTRIBUTE_QUALITY_DATA["ATTR_VALID"][1:4]) -class TaurusLabelTest2(TangoSchemeTestLauncher, BaseWidgetTestCase, - unittest.TestCase): - ''' - Specific tests for TaurusLabel - ''' - _klass = TaurusLabel - - def text(self, model=None, expected=None, fgRole=None, maxdepr=0, - modelIndex=None): - """Check that the label text""" - self._widget.setModel(model) - if fgRole is not None: - self._widget.setFgRole(fgRole) +from taurus.test.pytest import check_taurus_deprecations + +import pytest + +try: + # The following are Tango-centric imports. + from taurus.core.tango.test import taurus_test_ds # pytest fixture + + _TANGO_MISSING = False +except: + _TANGO_MISSING = True + + +def _chek_tauruslabel( + qtbot, + caplog, + taurus_test_ds, + model, + fmt=None, + fgRole=None, + bgRole=None, + modelIndex=None, + depr=0, + expected_fg=None, + expected_bg=None, +): + """Check the label foreground and background""" + if expected_fg is None and expected_bg is None: + raise ValueError("expected_fg or expected_bg must not be None") + with check_taurus_deprecations(caplog, expected=depr): + w = TaurusLabel() + qtbot.addWidget(w) + if model.startswith("/"): + model = "tango:{}{}".format(taurus_test_ds, model) + with qtbot.waitSignal(w.modelChanged, timeout=3200): + w.setModel(model) + if fmt is not None: + w.setFormat(fmt) if modelIndex is not None: - self._widget.setModelIndex(modelIndex) - self.processEvents(repetitions=10, sleep=.1) - got = str(self._widget.text()) - msg = ('wrong text for "%s":\n expected: %s\n got: %s' % - (model, expected, got)) - self.assertEqual(got, expected, msg) - self.assertMaxDeprecations(maxdepr) - - # TODO: fix the bgRole test (they fail sometimes when run by testsuite) - # ~~~~~~~~~~~~~~~~~~~~~~~ - # FAIL: test_bgRole (...) - # expected: (0, 255, 0) - # got: (239, 240, 241) # <-- these values change (unitialized garbage?) - # ~~~~~~~~~~~~~~~~~~~~~~~ - #@unittest.skip('bgRole tests fail randomly. Skip until fixed.') - def bgRole(self, model=None, expected=None, bgRole=None, maxdepr=0): - '''Check that the label text''' - self._widget.setModel(model) - self.processEvents(repetitions=50, sleep=.1) + w.setModelIndex(modelIndex) + if fgRole is not None: + w.setFgRole(fgRole) if bgRole is not None: - self._widget.setBgRole(bgRole) - p = self._widget.palette() - got = p.color(p.Background).getRgb()[:3] # RGB value of the background - msg = ('wrong background RGB for "%s":\n expected: %s\n got: %s' % - (model, expected, got)) - self.assertEqual(got, expected, msg) - self.assertMaxDeprecations(maxdepr) - - -def baseFormatter1(dtype, **kwargs): + w.setBgRole(bgRole) + + def _ok(): + """Check text""" + if expected_fg is not None: + assert w.text() == expected_fg + if expected_bg is not None: + p = w.palette() + assert p.color(p.Background).getRgb()[:3] == expected_bg + + qtbot.waitUntil(_ok, timeout=3200) + + +@pytest.mark.skipIf(_TANGO_MISSING, reason="tango-dependent test") +@pytest.mark.parametrize( + "model, fgRole, modelIndex, depr, fg", + [ + # pre-tep14 compat FgRole checks: value, w_value, state, quality, none + ("/double_scalar", "value", None, 1, "1.23 mm"), + ("/double_scalar", "w_value", None, 1, "0.00 mm"), + ("/double_scalar", "state", None, 0, "Ready"), + ("/double_scalar", "quality", None, 0, "ATTR_VALID"), + ("/double_scalar", "none", None, 0, ""), + # fragment and modelIndex checks + ("/double_scalar#label", None, None, 0, "double_scalar"), + ("/double_scalar", "label", None, 0, "double_scalar"), + ("/double_scalar#rvalue", "label", None, 0, "double_scalar"), + ("/double_scalar#state", None, None, 0, "Ready"), + ("/double_spectrum", None, 1, 0, "1.23 mm"), + ("/double_spectrum#rvalue[1]", None, None, 0, "1.23 mm"), + ("/double_image", None, (1, 1), 0, "1.23 mm"), + ( + "/double_image#rvalue[1,::2]", + None, + None, + 0, + # expected is not explicit to support pint v<0.8 particularities + "{:~}".format(numpy.array([1.23, 1.23]) * ur().mm), + ), + ], +) +def test_tauruslabel_text( + qtbot, caplog, taurus_test_ds, model, fgRole, modelIndex, depr, fg +): + """Check the label text""" + _chek_tauruslabel( + qtbot, + caplog, + taurus_test_ds, + model, + fgRole=fgRole, + modelIndex=modelIndex, + depr=depr, + expected_fg=fg, + ) + + +TRANSPARENT_BG = Qt.QColor(Qt.Qt.transparent).getRgb()[:3] +TAURUS_READY_BG = DEVICE_STATE_DATA["TaurusDevState.Ready"][1:4] +TG_ATTR_VALID_BG = ATTRIBUTE_QUALITY_DATA["ATTR_VALID"][1:4] + + +@pytest.mark.skipIf(_TANGO_MISSING, reason="tango-dependent test") +@pytest.mark.parametrize( + "model, bgRole, bg", + [ + # pre-tep14 compat BgRole checks: state, quality, none + ("/float_scalar_ro", "none", TRANSPARENT_BG), + ("/float_scalar_ro", "state", TAURUS_READY_BG), + ("/float_scalar_ro", "quality", TG_ATTR_VALID_BG), + ("/float_scalar_ro", None, TG_ATTR_VALID_BG), + ], +) +def test_tauruslabel_bg(qtbot, caplog, taurus_test_ds, model, bgRole, bg): + """"Check the label background""" + _chek_tauruslabel( + qtbot, caplog, taurus_test_ds, model, bgRole=bgRole, expected_bg=bg + ) + + +def _oneDecimalFormater(dtype, **kwargs): return "{:~.1f}" -def baseFormatter2(dtype, **kwargs): +def _typeFormatter(dtype, **kwargs): return dtype.__name__ -# ------------------------------------------------------------------------- -# Class formatter tests -@insertTest(helper_name='checkClassFormat', - model='eval:1.2345', - formatter='>>{}<<', - expected=">>1.2345<<") -@insertTest(helper_name='checkClassFormat', - model='eval:Q(5)#rvalue.magnitude', - formatter=baseFormatter2, - expected="int") -@insertTest(helper_name='checkClassFormat', - model='eval:Q("5m")#rvalue.units', - formatter=baseFormatter2, - expected="Unit") -@insertTest(helper_name='checkClassFormat', - model='eval:1.2345', - formatter=baseFormatter1, - expected="1.2") -@insertTest(helper_name='checkClassFormat', - model='eval:"hello"', - formatter=baseFormatter1, - expected="hello") -@insertTest(helper_name='checkClassFormat', - model='eval:"hello"', - formatter=baseFormatter2, - expected="str") -@insertTest(helper_name='checkClassFormat', - model='eval:"hello"', - formatter=None, - expected="hello") -@insertTest(helper_name='checkClassFormat', - model='eval:1.2345', - formatter='{:~.3f}', - expected="1.234") -@insertTest(helper_name='checkClassFormat', - model='eval:1.2345', - formatter='{:.3f}', - expected="1.234 dimensionless") -# ------------------------------------------------------------------------- -# Instance formatter tests -@insertTest(helper_name='checkInstanceFormat', - model='eval:1.2345', - formatter='>>{}<<', - expected=">>1.2345<<") -@insertTest(helper_name='checkInstanceFormat', - model='eval:Q(5)#rvalue.magnitude', - formatter=baseFormatter2, - expected="int") -@insertTest(helper_name='checkInstanceFormat', - model='eval:Q("5m")#rvalue.units', - formatter=baseFormatter2, - expected="Unit") -@insertTest(helper_name='checkInstanceFormat', - model='eval:1.2345', - formatter=baseFormatter1, - expected="1.2") -@insertTest(helper_name='checkInstanceFormat', - model='eval:"hello"', - formatter=baseFormatter1, - expected="hello") -@insertTest(helper_name='checkInstanceFormat', - model='eval:"hello"', - formatter=baseFormatter2, - expected="str") -@insertTest(helper_name='checkInstanceFormat', - model='eval:"hello"', - formatter=None, - expected="hello") -@insertTest(helper_name='checkInstanceFormat', - model='eval:1.2345', - formatter='{:~.3f}', - expected="1.234") -@insertTest(helper_name='checkInstanceFormat', - model='eval:1.2345', - formatter='{:.3f}', - expected="1.234 dimensionless") -class TaurusLabelFormatTest(BaseWidgetTestCase, unittest.TestCase): - """ - Specific tests for testing the Formatting API with TaurusLabel - instances - """ - - _klass = TaurusLabel - - def setUp(self): - BaseWidgetTestCase.setUp(self) - # store original class format - self._origFormatter = self._klass.FORMAT - - def tearDown(self): - # restore original class format - self._klass.FORMAT = self._origFormatter - - def checkInstanceFormat(self, model, formatter, expected): - if formatter is not None: - self._widget.setFormat(formatter) - self._widget.setModel(model) - self.processEvents(repetitions=50, sleep=.1) - - got = self._widget.text() - msg = ('wrong text for "%s":\n expected: %s\n got: %s' % - (model, expected, got)) - self.assertEqual(got, expected, msg) - - def checkClassFormat(self, model, formatter, expected): - self._klass.FORMAT = formatter - # self._widget was already created by BaseWidgetTestCase.setUp(self) - # but we need to re-create it to use the class formatter - self._widget = self._klass(*self.initargs, **self.initkwargs) - self._widget.setModel(model) - self.processEvents(repetitions=50, sleep=.1) - - got = self._widget.text() - msg = ('wrong text for "%s":\n expected: %s\n got: %s' % - (model, expected, got)) - self.assertEqual(got, expected, msg) - -# -# if __name__ == "__main__": -# unittest.main() +@pytest.mark.parametrize( + "model, fmt, fg", + [ + ("eval:1.2345", "{:.3f}", "1.234 dimensionless"), + ("eval:1.2345", "{:~.3f}", "1.234"), + ("eval:1.2345", "{:~.3f}", "1.234"), + ("eval:1.2345", ">>{}<<", ">>1.2345<<"), + ('eval:"hello"', None, "hello"), + ('eval:"hello"', _oneDecimalFormater, "hello"), + ('eval:"hello"', _typeFormatter, "str"), + ("eval:1.2345", _oneDecimalFormater, "1.2"), + ('eval:Q("5m")#rvalue.units', _typeFormatter, "Unit"), + ("eval:Q(5)#rvalue.magnitude", _typeFormatter, "int"), + ], +) +def test_instance_format(qtbot, caplog, taurus_test_ds, model, fmt, fg): + """Check formatter API at instance level""" + _chek_tauruslabel( + qtbot, caplog, taurus_test_ds, model, fmt=fmt, expected_fg=fg + ) + + +@pytest.mark.parametrize( + "model, fmt, fg", + [ + ("eval:1.2345", "{:.3f}", "1.234 dimensionless"), + ("eval:1.2345", "{:~.3f}", "1.234"), + ("eval:1.2345", "{:~.3f}", "1.234"), + ("eval:1.2345", ">>{}<<", ">>1.2345<<"), + ('eval:"hello"', None, "hello"), + ('eval:"hello"', _oneDecimalFormater, "hello"), + ('eval:"hello"', _typeFormatter, "str"), + ("eval:1.2345", _oneDecimalFormater, "1.2"), + ('eval:Q("5m")#rvalue.units', _typeFormatter, "Unit"), + ("eval:Q(5)#rvalue.magnitude", _typeFormatter, "int"), + ], +) +def test_class_format( + monkeypatch, qtbot, caplog, taurus_test_ds, model, fmt, fg +): + """Check formatter API at class level""" + monkeypatch.setattr(TaurusLabel, "FORMAT", fmt) + _chek_tauruslabel(qtbot, caplog, taurus_test_ds, model, expected_fg=fg) From ecd7e1f3c4dc0c17606d404af3294b8f20a42a5a Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 9 Jun 2020 14:00:13 +0200 Subject: [PATCH 298/373] Use own mirror for docker images CI is failing because of unavailability of nexus.engageska-portugal.pt docker registry. I pushed the required images (retagged from ska-docker (tango-db, tango-java and tango-cpp)to DockerHub. Use them for the CI --- ci/docker-compose/.env | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ci/docker-compose/.env b/ci/docker-compose/.env index bf415f70a..6198c433f 100644 --- a/ci/docker-compose/.env +++ b/ci/docker-compose/.env @@ -1,2 +1,5 @@ -DOCKER_REGISTRY_HOST=nexus.engageska-portugal.pt -DOCKER_REGISTRY_USER=ska-docker +#DOCKER_REGISTRY_HOST=nexus.engageska-portugal.pt +#DOCKER_REGISTRY_USER=ska-docker +DOCKER_REGISTRY_HOST=registry.hub.docker.com +DOCKER_REGISTRY_USER=cpascual + From 180b2f47153351d7ae52f469884d8c12eacb5ad2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 9 Jun 2020 16:31:43 +0200 Subject: [PATCH 299/373] Workaround: Fix pyvirtualdisplay version in tox Travis CI started failing with an XVFB error when pyvirtualdisplay was updated in conda from 0.2.5 to 1.3. Probably pytest-xvfb needs to be updated. For the moment, pin the version of pyvirtualdisplay as an attempt to workaround the issue. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index a98dbf062..833fef68e 100644 --- a/tox.ini +++ b/tox.ini @@ -40,6 +40,7 @@ conda_deps= pytest-xvfb pytest-qt flaky + pyvirtualdisplay=0.2 docs: sphinx docs: sphinx_rtd_theme docs: graphviz From 6e0acf2acf480139af64310691e793b703e74b19 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 9 Jun 2020 20:51:41 +0200 Subject: [PATCH 300/373] Refactor taurusbase tests Reimplement using pytest and pytest-qt. The skipped tests are not ported. --- .../qt/qtgui/base/test/test_taurusbase.py | 111 +++++++----------- 1 file changed, 45 insertions(+), 66 deletions(-) diff --git a/lib/taurus/qt/qtgui/base/test/test_taurusbase.py b/lib/taurus/qt/qtgui/base/test/test_taurusbase.py index 4dce9f334..e811c2fce 100644 --- a/lib/taurus/qt/qtgui/base/test/test_taurusbase.py +++ b/lib/taurus/qt/qtgui/base/test/test_taurusbase.py @@ -26,76 +26,55 @@ """Unit tests for taurusbase""" -import unittest -from taurus.test import insertTest -from taurus.qt.qtgui.test import BaseWidgetTestCase -from taurus.core.tango.test import TangoSchemeTestLauncher # tango-centric +import pytest from taurus.qt.qtgui.container import TaurusWidget -DEV_NAME = TangoSchemeTestLauncher.DEV_NAME +from taurus.test.pytest import check_taurus_deprecations +try: + # The following are Tango-centric imports. + from taurus.core.tango.test import taurus_test_ds # pytest fixture -@insertTest(helper_name='getDisplayValue', - model='eval:1+2#', - expected='-----') -@insertTest(helper_name='getDisplayValue', - model='eval:1+2#label', - expected='1+2') -@insertTest(helper_name='getDisplayValue', - model='eval:1+2', - expected='3') -# This checks if the pre-tep3 behavior is kept (and it fails) -# ...but I think it should *not* be kept -@insertTest(helper_name='getDisplayValue', - model='tango://' + DEV_NAME + '/double_scalar?configuration', - expected='double_scalar?configuration', - test_skip="old behaviour which we probably don't want") -@insertTest(helper_name='getDisplayValue', - model='tango://' + DEV_NAME + '/float_scalar?configuration=label', - expected='float_scalar') -@insertTest(helper_name='getDisplayValue', - model='tango:' + DEV_NAME + '/double_scalar#rvalue.magnitude', - expected='1.23') -@insertTest(helper_name='getDisplayValue', - model='tango:' + DEV_NAME + '/float_scalar#label', - expected='float_scalar') -@insertTest(helper_name='getDisplayValue', - model='tango:' + DEV_NAME + '/float_scalar#', - expected='-----') -@insertTest(helper_name='getDisplayValue', - model='tango:' + DEV_NAME + '/state', - expected='ON') -# This fails due to encode/decode rounding errors for float<-->numpy.float32 -@insertTest(helper_name='getDisplayValue', - model='tango:' + DEV_NAME + '/float_scalar', - expected='1.23 mm', - test_skip='enc/decode rounding errors for float<-->numpy.float32') -@insertTest(helper_name='getDisplayValue', - model='tango:' + DEV_NAME + '/double_scalar', - expected='1.23 mm') -@insertTest(helper_name='getDisplayValue', - model='tango:' + DEV_NAME + '/short_scalar', - expected='123 mm') -@insertTest(helper_name='getDisplayValue', - model='tango:' + DEV_NAME + '/boolean_scalar', - expected='True') -class GetDisplayValueTestCase(TangoSchemeTestLauncher, BaseWidgetTestCase, - unittest.TestCase): - """Check TaurusBaseComponent.getDisplayValue - """ - _klass = TaurusWidget + _TANGO_MISSING = False +except: + _TANGO_MISSING = True - def setUp(self): - BaseWidgetTestCase.setUp(self) - # def tearDown(self): - # BaseWidgetTestCase.tearDown(self) +@pytest.mark.skipIf(_TANGO_MISSING, reason="tango-dependent test") +@pytest.mark.parametrize( + "model, expected", + [ + ("/boolean_scalar", "True"), + ("/short_scalar", "123 mm"), + ("/double_scalar", "1.23 mm"), + ("/state", "ON"), + ("/float_scalar#", "-----"), + ("/float_scalar#label", "float_scalar"), + ("/double_scalar#rvalue.magnitude", "1.23"), + #("/float_scalar?configuration=label", "float_scalar"), + ("eval:1+2", "3"), + ("eval:1+2#label", "1+2"), + ("eval:1+2#", "-----"), + ], +) +def test_display_value( + qtbot, + caplog, + taurus_test_ds, + model, + expected +): + """Check the getDisplayValue method""" + with check_taurus_deprecations(caplog): + w = TaurusWidget() + qtbot.addWidget(w) + if model.startswith("/"): + model = "tango:{}{}".format(taurus_test_ds, model) + with qtbot.waitSignal(w.modelChanged, timeout=3200): + w.setModel(model) - def getDisplayValue(self, model=None, expected=None): - '''Check if setModel works when using parent model''' - self._widget.setModel(model) - got = self._widget.getDisplayValue() - msg = ('getDisplayValue for "%s" should be %r (got %r)' % - (model, expected, got)) - self.assertEqual(expected, got, msg) - self.assertMaxDeprecations(0) + def _ok(): + """Check text""" + assert w.getDisplayValue() == expected + + qtbot.waitUntil(_ok, timeout=3200) From e6f466f1b344ddb4f0fd80ee6f1162d41d8f61c2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 9 Jun 2020 22:46:20 +0200 Subject: [PATCH 301/373] Refactor ui tests Reimplement using pytest and pytest-qt. --- .../qt/qtgui/util/test/test_ui/test_ui.py | 108 ++++++++---------- 1 file changed, 45 insertions(+), 63 deletions(-) diff --git a/lib/taurus/qt/qtgui/util/test/test_ui/test_ui.py b/lib/taurus/qt/qtgui/util/test/test_ui/test_ui.py index b5726f906..64697193b 100644 --- a/lib/taurus/qt/qtgui/util/test/test_ui/test_ui.py +++ b/lib/taurus/qt/qtgui/util/test/test_ui/test_ui.py @@ -32,7 +32,6 @@ import unittest from taurus.external.qt import Qt from taurus.qt.qtgui.util.ui import UILoadable -from taurus.qt.qtgui.test import BaseWidgetTestCase from .mywidget3 import MyWidget3 @@ -41,65 +40,48 @@ class UILoadableTestCase(unittest.TestCase): Test cases for UILoadable decorator """ - @UILoadable - class MyWidget1(Qt.QWidget): - - def __init__(self, parent=None): - Qt.QWidget.__init__(self, parent) - self.loadUi() - self.my_button.setText("This is MY1 button") - - @UILoadable(with_ui="ui") - class MyWidget2(Qt.QWidget): - - def __init__(self, parent=None): - Qt.QWidget.__init__(self, parent) - path = os.path.join(os.path.dirname(__file__), "ui", "mywidget2") - self.loadUi(filename="mywidget2_custom.ui", path=path) - self.ui.my_button.setText("This is MY2 button") - - def setUp(self): - app = Qt.QApplication.instance() - if app is None: - app = Qt.QApplication([]) - self.__app = app - - def test_uiloadable_default(self): - """Test UILoadable with default arguments""" - widget = self.MyWidget1() - self.assertEquals(widget.my_button.text(), "This is MY1 button", - "button text differs from expected") - - def test_uiloadable_customized(self): - """Test UILoadable with customized filename and path""" - widget = self.MyWidget2() - self.assertTrue(hasattr(widget, "ui"), - "widget doesn't have 'ui' member") - self.assertTrue(hasattr(widget.ui, "my_button"), - "widget.ui doesn't have a 'my_button' member") - self.assertFalse(hasattr(widget, "my_button"), - "widget has a my_button member") - self.assertEquals(widget.ui.my_button.text(), "This is MY2 button", - "button text differs from expected") - - -class Bug151_TestCase(BaseWidgetTestCase, unittest.TestCase): - '''Test for bug 151: https://sourceforge.net/p/tauruslib/tickets/151/''' - - def test_bug151(self): - '''Check inheritance of UILoadable classes across packages (bug #151) - ''' - class Bug151_Widget(MyWidget3): - pass - try: - Bug151_Widget() - except: - self.fail('Inheriting from UILoadable from another package fails') - - -def main(): - unittest.main() - - -if __name__ == "__main__": - main() +@UILoadable +class MyWidget1(Qt.QWidget): + + def __init__(self, parent=None): + Qt.QWidget.__init__(self, parent) + self.loadUi() + self.my_button.setText("This is MY1 button") + + +@UILoadable(with_ui="ui") +class MyWidget2(Qt.QWidget): + + def __init__(self, parent=None): + Qt.QWidget.__init__(self, parent) + path = os.path.join(os.path.dirname(__file__), "ui", "mywidget2") + self.loadUi(filename="mywidget2_custom.ui", path=path) + self.ui.my_button.setText("This is MY2 button") + + +class Bug151_Widget(MyWidget3): + pass + + +def test_uiloadable_default(qtbot): + """Test UILoadable with default arguments""" + w = MyWidget1() + qtbot.addWidget(w) + assert w.my_button.text() == "This is MY1 button" + + +def test_uiloadable_customized(qtbot): + """Test UILoadable with customized filename and path""" + w = MyWidget2() + qtbot.addWidget(w) + assert hasattr(w, "ui") + assert hasattr(w.ui, "my_button") + assert not hasattr(w, "my_button") + assert w.ui.my_button.text() == "This is MY2 button" + + +def test_bug151(qtbot): + """Check inheritance of UILoadable classes across packages (bug #151)""" + + w = Bug151_Widget() + qtbot.addWidget(w) From 147bb032250f506187925f5baa582c0a932a0365 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 10 Jun 2020 12:38:55 +0200 Subject: [PATCH 302/373] Refactor taurusvalue tests Reimplement using pytest and pytest-qt. --- .../qt/qtgui/panel/test/test_taurusvalue.py | 133 ++++++++---------- 1 file changed, 58 insertions(+), 75 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py index 45c80b37e..30e3c3d7f 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py @@ -25,86 +25,69 @@ """Test for taurus.qt.qtgui.panel.taurusvalue""" -import unittest import pytest -from taurus.test import insertTest -from taurus.qt.qtgui.test import BaseWidgetTestCase from taurus.qt.qtgui.panel import TaurusValue -from taurus.core.tango.test import TangoSchemeTestLauncher +from taurus.test.pytest import check_taurus_deprecations +try: + # The following are Tango-centric imports. + from taurus.core.tango.test import taurus_test_ds # pytest fixture -DEV_NAME = TangoSchemeTestLauncher.DEV_NAME + _TANGO_MISSING = False +except: + _TANGO_MISSING = True -class TaurusValueTestTango(TangoSchemeTestLauncher, BaseWidgetTestCase, - unittest.TestCase): - """ - Specific tests for TaurusValue - """ - _klass = TaurusValue - - def test_bug126(self): - """Verify that case is not lost when customizing a label (bug#126)""" - w = self._widget - # self._widget.setModel("eval:1") - self._widget.setModel("tango:" + DEV_NAME + "/double_scalar") +@pytest.mark.skipif(_TANGO_MISSING, reason="tango-dependent test") +def test_bug126(qtbot, caplog): + """Verify that case is not lost when customizing a label (bug#126)""" + with check_taurus_deprecations(caplog, expected=0): + w = TaurusValue() + qtbot.addWidget(w) + w.setModel("tango:sys/tg_test/1/double_scalar") label = "MIXEDcase" w.setLabelConfig(label) - self.processEvents(repetitions=10, sleep=.1) - shownLabel = str(w.labelWidget().text()) - msg = 'Shown label ("%s") differs from set label ("%s")' % (shownLabel, - label) - self.assertEqual(label, shownLabel, msg) - self.assertMaxDeprecations(1) - - @pytest.mark.flaky - def test_labelCaseSensitivity(self): - """Verify that case is respected of in the label widget""" - w = self._widget - self._widget.setModel("tango:" + DEV_NAME + "/MIXEDcase") - label = "MIXEDcase" - self.processEvents(repetitions=10, sleep=.1) - shownLabel = str(w.labelWidget().text()) - msg = 'Shown label ("%s") differs from set label ("%s")' % (shownLabel, - label) - self.assertEqual(label, shownLabel, msg) - self.assertMaxDeprecations(0) - - def tearDown(self): - """Set Model to None""" - self._widget.setModel(None) - TangoSchemeTestLauncher.tearDown(self) - unittest.TestCase.tearDown(self) - - -@insertTest( - helper_name="texts", - model='eval:@a=taurus.core.evaluation.test.res.mymod.MyClass()/a.foo', - expected=("a.foo", "123", "123", "m") -) -class TaurusValueTest(BaseWidgetTestCase, unittest.TestCase): - """ - Specific tests for TaurusValue - """ - _klass = TaurusValue - - def texts(self, model=None, expected=None, fgRole=None, maxdepr=0): - """Checks the texts for scalar attributes""" - self._widget.setModel(model) - if fgRole is not None: - self._widget.setFgRole(fgRole) - # wait until there is some text in the read widget - self.processEvents(repetitions=20, sleep=.05) - got = (str(self._widget.labelWidget().text()), - str(self._widget.readWidget().text()), - str(self._widget.writeWidget().displayText()), - str(self._widget.unitsWidget().text()), - ) - msg = ('wrong text for "%s":\n expected: %s\n got: %s' % - (model, expected, got)) - self.assertEqual(got, expected, msg) - self.assertMaxDeprecations(maxdepr) - - -if __name__ == "__main__": - pass + + def _ok(): + assert w.labelWidget().text() == label + + qtbot.waitUntil(_ok, timeout=3200) + + +@pytest.mark.skipif(_TANGO_MISSING, reason="tango-dependent test") +def test_label_case_sensitivity(qtbot, caplog, taurus_test_ds): + """Verify that case is respected of in the label widget""" + with check_taurus_deprecations(caplog, expected=0): + w = TaurusValue() + qtbot.addWidget(w) + w.setModel("tango:{}/MIXEDcase".format(taurus_test_ds)) + + def _ok(): + assert w.labelWidget().text() == "MIXEDcase" + + qtbot.waitUntil(_ok, timeout=3200) + + +def test_taurusvalue_subwidget_texts(qtbot, caplog): + """Checks the texts for scalar attributes""" + + model = "eval:@a=taurus.core.evaluation.test.res.mymod.MyClass()/a.foo" + expected = ("a.foo", "123", "123", "m") + depr = 0 + + with check_taurus_deprecations(caplog, expected=depr): + w = TaurusValue() + qtbot.addWidget(w) + w.setModel(model) + + def _ok(): + got = ( + str(w.labelWidget().text()), + str(w.readWidget().text()), + str(w.writeWidget().displayText()), + str(w.unitsWidget().text()), + ) + assert got == expected + + qtbot.waitUntil(_ok, timeout=3200) + From 4f0a77144b5f86d9769c04d26596ea7bdb9eef58 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 10 Jun 2020 12:40:38 +0200 Subject: [PATCH 303/373] Fix typo in pytest skipif marks Use pytest.mark.skipif instead of pytest.mark.skipIf --- lib/taurus/qt/qtgui/base/test/test_taurusbase.py | 2 +- lib/taurus/qt/qtgui/button/test/test_commandbutton.py | 2 +- lib/taurus/qt/qtgui/display/test/test_tauruslabel.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/taurus/qt/qtgui/base/test/test_taurusbase.py b/lib/taurus/qt/qtgui/base/test/test_taurusbase.py index e811c2fce..045826d8c 100644 --- a/lib/taurus/qt/qtgui/base/test/test_taurusbase.py +++ b/lib/taurus/qt/qtgui/base/test/test_taurusbase.py @@ -40,7 +40,7 @@ _TANGO_MISSING = True -@pytest.mark.skipIf(_TANGO_MISSING, reason="tango-dependent test") +@pytest.mark.skipif(_TANGO_MISSING, reason="tango-dependent test") @pytest.mark.parametrize( "model, expected", [ diff --git a/lib/taurus/qt/qtgui/button/test/test_commandbutton.py b/lib/taurus/qt/qtgui/button/test/test_commandbutton.py index 8870b2a2d..05ddf0fcd 100644 --- a/lib/taurus/qt/qtgui/button/test/test_commandbutton.py +++ b/lib/taurus/qt/qtgui/button/test/test_commandbutton.py @@ -43,7 +43,7 @@ _TANGO_MISSING = True -@pytest.mark.skipIf(_TANGO_MISSING, reason="tango-dependent test") +@pytest.mark.skipif(_TANGO_MISSING, reason="tango-dependent test") def test_timeout(qtbot, taurus_test_ds): """Check that the timeout property works""" w = TaurusCommandButton(command='Sleep') diff --git a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py index c0ce5757d..6b43f200f 100644 --- a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py @@ -87,7 +87,7 @@ def _ok(): qtbot.waitUntil(_ok, timeout=3200) -@pytest.mark.skipIf(_TANGO_MISSING, reason="tango-dependent test") +@pytest.mark.skipif(_TANGO_MISSING, reason="tango-dependent test") @pytest.mark.parametrize( "model, fgRole, modelIndex, depr, fg", [ @@ -136,7 +136,7 @@ def test_tauruslabel_text( TG_ATTR_VALID_BG = ATTRIBUTE_QUALITY_DATA["ATTR_VALID"][1:4] -@pytest.mark.skipIf(_TANGO_MISSING, reason="tango-dependent test") +@pytest.mark.skipif(_TANGO_MISSING, reason="tango-dependent test") @pytest.mark.parametrize( "model, bgRole, bg", [ From b38f6168118ae249d9ff5f545261e111c808dbca Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 10 Jun 2020 12:44:45 +0200 Subject: [PATCH 304/373] Deprecate {Base,Generic}WidgetTestCase The *WidgetTestCase classes were created to help with Qt tests based on unittest. Since we moved all the Qt test to pytest-qt, we no longer want them (they are less robust and don't play well with the pytest-qt based tests) Mark them as deprecated and skip any test based on them. --- lib/taurus/qt/qtgui/test/base.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/test/base.py b/lib/taurus/qt/qtgui/test/base.py index 11ccc8de4..ea3365b00 100644 --- a/lib/taurus/qt/qtgui/test/base.py +++ b/lib/taurus/qt/qtgui/test/base.py @@ -55,8 +55,9 @@ def taurus_widget_set_models(klass, qtbot, caplog, depr=0, models=[]): class BaseWidgetTestCase(object): - ''' + DEPRECATED: use pytest-qt instead of this + A base class for tests that need a widget instance To use it, simply inherit from BaseWidgetTestCase *and* unittest.TestCase @@ -80,6 +81,10 @@ def setUp(self): - The widget must be instantiated """ + from taurus.core.util import deprecated + deprecated(dep="BaseWidgetTestCase", alt="pytest-qt", rel="4.6.5") + raise unittest.SkipTest("*WidgetTestCase is deprecated. Use pytest-qt") + unittest.TestCase.setUp(self) from taurus.core.util.log import _DEPRECATION_COUNT @@ -112,8 +117,10 @@ def processEvents(self, repetitions=1, sleep=0): class GenericWidgetTestCase(BaseWidgetTestCase): + ''' + DEPRECATED: use pytest-qt instead of this - '''a base class for testing common cases of arbitrary Taurus widget classes + a base class for testing common cases of arbitrary Taurus widget classes To use it, simply inherit from GenericWidgetTestCase *and* unittest.TestCase and provide the following class members: @@ -139,6 +146,13 @@ def setUp(self): None should be used as a placeholder when a model cannot be created for a given modelname. """ + from taurus.core.util import deprecated + deprecated( + dep="GenericWidgetTestCase", + alt="taurus_widget_set_models and pytest-qt", + rel="4.6.5" + ) + # Make sure the basics are taken care of (QApplication, etc) BaseWidgetTestCase.setUp(self) From 23ec89482402a561e7251466b6230841aa38da3d Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 10 Jun 2020 13:48:42 +0200 Subject: [PATCH 305/373] Remove left usages of taurus.qt.qtgui.test.base Remove last usages of taurus.qt.qtgui.test.base in taurus code --- .../qtgui/button/test/test_commandbutton.py | 2 -- .../qt/qtgui/panel/test/test_taurusform.py | 34 ------------------ lib/taurus/qt/qtgui/test/base.py | 23 +----------- .../qt/qtgui/test/test_widgets_general.py | 36 +++++++++++++++---- 4 files changed, 30 insertions(+), 65 deletions(-) diff --git a/lib/taurus/qt/qtgui/button/test/test_commandbutton.py b/lib/taurus/qt/qtgui/button/test/test_commandbutton.py index 05ddf0fcd..df15c0890 100644 --- a/lib/taurus/qt/qtgui/button/test/test_commandbutton.py +++ b/lib/taurus/qt/qtgui/button/test/test_commandbutton.py @@ -28,9 +28,7 @@ import pytest -from functools import partial from taurus.qt.qtgui.button import TaurusCommandButton -from taurus.qt.qtgui.test.base import taurus_widget_set_models from taurus.external.qt import Qt try: diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py index a38d7211b..e9403990e 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusform.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusform.py @@ -30,40 +30,6 @@ from taurus.core.util.test.test_plugin import mock_entry_point -class TaurusFormTest(GenericWidgetTestCase, unittest.TestCase): - """ - Generic tests for TaurusForm widget. - - .. seealso: :class:`taurus.qt.qtgui.test.base.GenericWidgetTestCase` - """ - _klass = TaurusForm - modelnames = [['sys/tg_test/1'], - ['sys/tg_test/1/wave'], - [], - '', - ['eval:1'], - None, - ['sys/tg_test/1/%s' % a for a in ( - 'short_scalar', 'double_array', - 'uchar_image_ro', 'string_spectrum', - 'no_value', 'throw_exception')], - [''], - 'sys/tg_test/1,eval:1', - 'sys/tg_test/1/short_image eval:rand(16)', - [None] - ] - - -class TaurusAttrFormTest(GenericWidgetTestCase, unittest.TestCase): - """ - Generic tests for TaurusAttrForm widget. - - .. seealso: :class:`taurus.qt.qtgui.test.base.GenericWidgetTestCase` - """ - _klass = TaurusAttrForm - modelnames = ['sys/tg_test/1', None] - - class _DummyTV(TaurusValue): pass diff --git a/lib/taurus/qt/qtgui/test/base.py b/lib/taurus/qt/qtgui/test/base.py index ea3365b00..e59c69f1f 100644 --- a/lib/taurus/qt/qtgui/test/base.py +++ b/lib/taurus/qt/qtgui/test/base.py @@ -31,27 +31,6 @@ import taurus.core import unittest from taurus.qt.qtgui.application import TaurusApplication -from taurus.test.pytest import check_taurus_deprecations - - -def taurus_widget_set_models(klass, qtbot, caplog, depr=0, models=[]): - """ - Generic test that checks that a widget can be instantiated and given - its setModel called sequentially. - - It can be parameterized or run with functools.partial - """ - with check_taurus_deprecations(caplog, expected=depr): - w = klass() - qtbot.addWidget(w) - - for model in models: - if not model: - model_obj = None - else: - model_obj = taurus.Object(model) - w.setModel(model) - assert w.getModelObj() == model_obj class BaseWidgetTestCase(object): @@ -149,7 +128,7 @@ def setUp(self): from taurus.core.util import deprecated deprecated( dep="GenericWidgetTestCase", - alt="taurus_widget_set_models and pytest-qt", + alt="test_set_models and pytest-qt", rel="4.6.5" ) diff --git a/lib/taurus/qt/qtgui/test/test_widgets_general.py b/lib/taurus/qt/qtgui/test/test_widgets_general.py index 4e17d5b19..e522dbcc1 100644 --- a/lib/taurus/qt/qtgui/test/test_widgets_general.py +++ b/lib/taurus/qt/qtgui/test/test_widgets_general.py @@ -33,16 +33,21 @@ from taurus.test.pytest import check_taurus_deprecations -EMPTY = '' -TGT = 'tango:sys/tg_test/1' DB = 'sys/database/2' -TGT_F_SC = 'tango:sys/tg_test/1/' -TGT_F_SP = 'tango:sys/tg_test/1/' -TGT_WAVE = 'tango:sys/tg_test/1/wave' +TGT = 'tango:sys/tg_test/1' +TGT_FL_SC = TGT + '/float_scalar' +TGT_FL_SP = TGT + '/float_spectrum' +TGT_SH_SC = TGT + '/short_scalar' +TGT_UC_IM = TGT + '/uchar_image_ro' +TGT_ST_SP = TGT + '/string_spectrum' +TGT_WAVE = TGT + '/wave' +TGT_NOVAL = TGT + '/no_value' +TGT_EXC = TGT + '/throw_exception' EV_INT = 'eval:123' EV_Q = 'eval:1.23*UR.mV' +EV_RND5 = 'eval:rand(5)' -# TODO: create model objects in setup_module to speed up +# TODO: create and keep model objects in setup_module to speed up def _import_obj(obj_str, package="taurus.qt.qtgui"): @@ -61,6 +66,20 @@ def _import_obj(obj_str, package="taurus.qt.qtgui"): [ (".display:TaurusLabel", 0, [TGT_WAVE, "", EV_INT, None]), (".button:TaurusCommandButton", 0, [TGT, "", DB, None]), + (".panel:TaurusAttrForm", 0, [TGT, "", DB, None]), + (".panel:TaurusForm", 0, [ + [TGT], + [TGT_WAVE], + [], + "", + [EV_INT], + None, + [TGT_SH_SC, TGT_UC_IM, TGT_ST_SP, TGT_NOVAL, TGT_EXC], + [""], + ",".join((TGT, EV_INT)), + " ".join((TGT_UC_IM, EV_RND5)), + [None] + ]), ] ) def test_set_models(qtbot, caplog, widgetname, depr, models): @@ -79,6 +98,9 @@ def test_set_models(qtbot, caplog, widgetname, depr, models): if not model: model_obj = None else: - model_obj = taurus.Object(model) + try: + model_obj = taurus.Object(model) + except: # allow non-string models (e.g. lists of models) + model_obj = None w.setModel(model) assert w.getModelObj() == model_obj From dcb68317f76a48d00693c93d50b8474c6d4eb949 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 10 Jun 2020 18:20:19 +0200 Subject: [PATCH 306/373] Update label controller on format reset TaurusLabel does not currently update its text when updating the format. Fix this by forcing a controllerUpdate() call whenever resetFormat is called. This was likely the cause of some of the fuzzyness in previous tests. --- lib/taurus/qt/qtgui/display/tauruslabel.py | 5 ++ lib/taurus/test/pytest.py | 3 -- lib/taurus/test/test_pytest.py | 53 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 lib/taurus/test/test_pytest.py diff --git a/lib/taurus/qt/qtgui/display/tauruslabel.py b/lib/taurus/qt/qtgui/display/tauruslabel.py index e9c6ce486..15c807088 100644 --- a/lib/taurus/qt/qtgui/display/tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/tauruslabel.py @@ -486,6 +486,11 @@ def resetAutoTrim(self): """Reset auto-trimming to its default value""" self.setAutoTrim(self.DefaultAutoTrim) + def resetFormat(self): + """reimplement to update controller if format is changed""" + TaurusBaseWidget.resetFormat(self) + self.controllerUpdate() + def displayValue(self, v): """Reimplementation of displayValue for TaurusLabel""" if self._permanentText is None: diff --git a/lib/taurus/test/pytest.py b/lib/taurus/test/pytest.py index e51aa8491..2f288a4cf 100644 --- a/lib/taurus/test/pytest.py +++ b/lib/taurus/test/pytest.py @@ -26,7 +26,6 @@ """Common tools and fixtures for using with taurus and pytest""" import contextlib -import functools @contextlib.contextmanager @@ -50,5 +49,3 @@ def check_taurus_deprecations(caplog, expected=0): n = len(deps) msg = "{} Deprecation Warnings ({} expected)".format(n, expected) assert len(deps) == expected, msg - - diff --git a/lib/taurus/test/test_pytest.py b/lib/taurus/test/test_pytest.py new file mode 100644 index 000000000..8d2365512 --- /dev/null +++ b/lib/taurus/test/test_pytest.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +from taurus.core.util.log import deprecated +import pytest +from .pytest import check_taurus_deprecations + + +def test_deprecations(caplog): + """Test the check_taurus_deprecations context manager""" + + with check_taurus_deprecations(caplog, expected=1): + deprecated(dep="foo", alt="bar") + + with check_taurus_deprecations(caplog): + pass + + with check_taurus_deprecations(caplog, expected=0): + pass + + with pytest.raises(AssertionError): + with check_taurus_deprecations(caplog, expected=1): + pass + + with pytest.raises(AssertionError): + with check_taurus_deprecations(caplog, expected=0): + deprecated(dep="foo", alt="bar") + + with pytest.raises(AssertionError): + with check_taurus_deprecations(caplog, expected=10): + deprecated(dep="foo", alt="bar") From 611fa161e43a8b69343d970687776a0aa0494760 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 10 Jun 2020 20:36:43 +0200 Subject: [PATCH 307/373] Correct deprecation check for label test The "value" and "w_value" FgRoles do not raise deprecation warnings. Adapt the tests accordingly --- lib/taurus/qt/qtgui/display/test/test_tauruslabel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py index 6b43f200f..d7ac115d0 100644 --- a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py @@ -92,8 +92,8 @@ def _ok(): "model, fgRole, modelIndex, depr, fg", [ # pre-tep14 compat FgRole checks: value, w_value, state, quality, none - ("/double_scalar", "value", None, 1, "1.23 mm"), - ("/double_scalar", "w_value", None, 1, "0.00 mm"), + ("/double_scalar", "value", None, 0, "1.23 mm"), + ("/double_scalar", "w_value", None, 0, "0.00 mm"), ("/double_scalar", "state", None, 0, "Ready"), ("/double_scalar", "quality", None, 0, "ATTR_VALID"), ("/double_scalar", "none", None, 0, ""), From 8a129bdb59f437251f2f133110d483ceb24ad699 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 10 Jun 2020 20:39:40 +0200 Subject: [PATCH 308/373] Fix fuzziness in test_class_format The test_class_format is fuzzy because not always the format is reset (probably a race condition). Force an explicit format reset to avoid it. --- .../qt/qtgui/display/test/test_tauruslabel.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py index d7ac115d0..2a84e7adb 100644 --- a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py @@ -199,9 +199,19 @@ def test_instance_format(qtbot, caplog, taurus_test_ds, model, fmt, fg): ("eval:Q(5)#rvalue.magnitude", _typeFormatter, "int"), ], ) -def test_class_format( - monkeypatch, qtbot, caplog, taurus_test_ds, model, fmt, fg -): +def test_class_format(monkeypatch, qtbot, caplog, model, fmt, fg): """Check formatter API at class level""" monkeypatch.setattr(TaurusLabel, "FORMAT", fmt) - _chek_tauruslabel(qtbot, caplog, taurus_test_ds, model, expected_fg=fg) + + with check_taurus_deprecations(caplog, expected=0): + w = TaurusLabel() + qtbot.addWidget(w) + + w.setModel(model) + w.resetFormat() # needed to avoid fuzzyness in the tests + + def _ok(): + """Check text""" + assert w.text() == fg + + qtbot.waitUntil(_ok, timeout=3200) \ No newline at end of file From e71762a1ad0e7da2e9115b76395f336a5786d7d1 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 11 Jun 2020 11:13:31 +0200 Subject: [PATCH 309/373] Workaround: Allow fails in depr tests for PySide2 For some reason, the check_taurus_deprecations context manager is not working ok when PySide2 is used via taurus.external.qt (maybe it has to do with the installed log manager?) As a workaround use xfail in these cases. --- lib/taurus/test/pytest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/taurus/test/pytest.py b/lib/taurus/test/pytest.py index 2f288a4cf..cf05f2b77 100644 --- a/lib/taurus/test/pytest.py +++ b/lib/taurus/test/pytest.py @@ -25,7 +25,9 @@ """Common tools and fixtures for using with taurus and pytest""" +import pytest import contextlib +from taurus.external.qt import PYSIDE2 @contextlib.contextmanager @@ -48,4 +50,8 @@ def check_taurus_deprecations(caplog, expected=0): deps = [r for r in caplog.records if "DeprecationWarning" in r.msg] n = len(deps) msg = "{} Deprecation Warnings ({} expected)".format(n, expected) + if PYSIDE2 and len(deps) != expected: + # TODO: investigate the cause for this (note that it happens only + # if taurus.external.qt has been imported) + pytest.xfail("log handling is not working as expected for PySide2") assert len(deps) == expected, msg From 4addee3d7f8c5337ca71757bbfb343f36fb434a1 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 11 Jun 2020 15:39:48 +0200 Subject: [PATCH 310/373] Workaround: improper isolation on label tests test_tauruslabel_text is not properly isolated and when it is called with FgRole="quality" it fails in PySide2 if it has been previously called with another FgRole. Workaround this by moving it to the first position. --- lib/taurus/qt/qtgui/display/test/test_tauruslabel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py index 2a84e7adb..9a6c35489 100644 --- a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py @@ -58,6 +58,9 @@ def _chek_tauruslabel( expected_bg=None, ): """Check the label foreground and background""" + # TODO: these tests are not properly isolated. For example, the + # parameterization testing fgrole="quality" fails in PySide2 + # if it is called after another parameterization. if expected_fg is None and expected_bg is None: raise ValueError("expected_fg or expected_bg must not be None") with check_taurus_deprecations(caplog, expected=depr): @@ -92,10 +95,10 @@ def _ok(): "model, fgRole, modelIndex, depr, fg", [ # pre-tep14 compat FgRole checks: value, w_value, state, quality, none + ("/double_scalar", "quality", None, 0, "ATTR_VALID"), ("/double_scalar", "value", None, 0, "1.23 mm"), ("/double_scalar", "w_value", None, 0, "0.00 mm"), ("/double_scalar", "state", None, 0, "Ready"), - ("/double_scalar", "quality", None, 0, "ATTR_VALID"), ("/double_scalar", "none", None, 0, ""), # fragment and modelIndex checks ("/double_scalar#label", None, None, 0, "double_scalar"), From 1d44c7a296daa63c83cde30d61f7587c92d075e9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 00:40:09 +0200 Subject: [PATCH 311/373] Avoid invalid escape sequence warnings Since python3.6 we get DeprecationWarning for invalid scape sequences. Fix them by marking the string literals as "raw" --- lib/taurus/core/tango/tangoattribute.py | 4 ++-- lib/taurus/core/tango/tangofactory.py | 2 +- lib/taurus/core/tango/tangovalidator.py | 6 ++--- lib/taurus/core/util/fqdn.py | 2 +- lib/taurus/core/util/propertyfile.py | 8 +++---- lib/taurus/qt/qtgui/graphic/taurusgraphic.py | 2 +- lib/taurus/qt/qtgui/table/taurusgrid.py | 4 ++-- .../qtgui/taurusgui/paneldescriptionwizard.py | 2 +- lib/taurus/test/base.py | 3 +-- lib/taurus/test/moduleexplorer.py | 24 ++++++++++--------- lib/taurus/test/testsuite.py | 4 ++-- 11 files changed, 31 insertions(+), 30 deletions(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 898585d04..fc21af821 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -1101,10 +1101,10 @@ def _decodeAttrInfoEx(self, pytango_attrinfoex=None): ############################################################### fmt = standard_display_format_from_tango(i.data_type, i.format) self.format_spec = fmt.lstrip('%') # format specifier - match = re.search("[^\.]*\.(?P[0-9]+)[eEfFgG%]", fmt) + match = re.search(r"[^\.]*\.(?P[0-9]+)[eEfFgG%]", fmt) if match: self.precision = int(match.group(1)) - elif re.match("%[0-9]*d", fmt): + elif re.match(r"%[0-9]*d", fmt): self.precision = 0 # self._units and self._display_format is to be used by # TangoAttrValue for performance reasons. Do not rely on it in other diff --git a/lib/taurus/core/tango/tangofactory.py b/lib/taurus/core/tango/tangofactory.py index 755e915ab..3e04dc7de 100644 --- a/lib/taurus/core/tango/tangofactory.py +++ b/lib/taurus/core/tango/tangofactory.py @@ -64,7 +64,7 @@ class TangoFactory(Singleton, TaurusFactory, Logger): - """A :class:`TaurusFactory` singleton class to provide Tango-specific + r"""A :class:`TaurusFactory` singleton class to provide Tango-specific Taurus Element objects (TangoAuthority, TangoDevice, TangoAttribute) Tango model names are URI based See https://tools.ietf.org/html/rfc3986. diff --git a/lib/taurus/core/tango/tangovalidator.py b/lib/taurus/core/tango/tangovalidator.py index 0b7b4ce59..40cb72cb1 100644 --- a/lib/taurus/core/tango/tangovalidator.py +++ b/lib/taurus/core/tango/tangovalidator.py @@ -52,7 +52,7 @@ class TangoAuthorityNameValidator(TaurusAuthorityNameValidator): ''' scheme = 'tango' - authority = '//(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})' + authority = r'//(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})' path = '(?!)' query = '(?!)' fragment = '(?!)' @@ -181,7 +181,7 @@ def nonStrictNamePattern(self): r'(?P%(path)s)' + \ r'(\?(?P%(query)s))?' + \ r'(#%(fragment)s)?$' - authority = '(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})' + authority = r'(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})' path = '/?(?P((?P<_devalias>([^/?#:]+))|' + \ '(?P<_devslashname>[^/?#:]+/[^/?#:]+/[^/?#:]+)))' @@ -267,7 +267,7 @@ def nonStrictNamePattern(self): r'(?P%(path)s)' + \ r'(\?(?P%(query)s))?' + \ r'(#%(fragment)s)?$' - authority = '(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})' + authority = r'(?P([\w\-_]+\.)*[\w\-_]+):(?P\d{1,5})' query = 'configuration(=(?P(?P[^# ]+)))?' return pattern % dict(scheme=self.scheme, diff --git a/lib/taurus/core/util/fqdn.py b/lib/taurus/core/util/fqdn.py index d628be5fc..0618694fe 100644 --- a/lib/taurus/core/util/fqdn.py +++ b/lib/taurus/core/util/fqdn.py @@ -87,7 +87,7 @@ def test_fqdn_no_alias_local(name="www"): # test with an existent host in your domain print(socket.gethostbyname_ex(name)) print("socket.getfqdn:", socket.getfqdn(name)) - assert re.match(name + "\.[a-zA-Z0-9\.-]+", fqdn_no_alias(name)) + assert re.match(name + r"\.[a-zA-Z0-9\.-]+", fqdn_no_alias(name)) test_fqdn_no_alias() diff --git a/lib/taurus/core/util/propertyfile.py b/lib/taurus/core/util/propertyfile.py index 91182aaf3..39bc0396e 100644 --- a/lib/taurus/core/util/propertyfile.py +++ b/lib/taurus/core/util/propertyfile.py @@ -230,16 +230,16 @@ def escape(self, value): # Java escapes the '=' and ':' in the value # string with backslashes in the store method. # So let us do the same. - newvalue = value.replace(':', '\:') - newvalue = newvalue.replace('=', '\=') + newvalue = value.replace(':', r'\:') + newvalue = newvalue.replace('=', r'\=') return newvalue def unescape(self, value): # Reverse of escape - newvalue = value.replace('\:', ':') - newvalue = newvalue.replace('\=', '=') + newvalue = value.replace(r'\:', ':') + newvalue = newvalue.replace(r'\=', '=') return newvalue diff --git a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py index 3744e90bc..847c2636e 100644 --- a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py +++ b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py @@ -365,7 +365,7 @@ def getItemByName(self, item_name, strict=None): strict = ( not self.ANY_ATTRIBUTE_SELECTS_DEVICE) if strict is None else strict - alnum = '(?:[a-zA-Z0-9-_\*]|(?:\.\*))(?:[a-zA-Z0-9-_\*]|(?:\.\*))*' + alnum = r'(?:[a-zA-Z0-9-_\*]|(?:\.\*))(?:[a-zA-Z0-9-_\*]|(?:\.\*))*' target = str(item_name).strip().split()[0].lower().replace( '/state', '') # If it has spaces only the first word is used # Device names should match also its attributes or only state? diff --git a/lib/taurus/qt/qtgui/table/taurusgrid.py b/lib/taurus/qt/qtgui/table/taurusgrid.py index b9570a6c7..3bb2def45 100644 --- a/lib/taurus/qt/qtgui/table/taurusgrid.py +++ b/lib/taurus/qt/qtgui/table/taurusgrid.py @@ -56,7 +56,7 @@ from taurus.qt.qtgui.base import TaurusBaseWidget from taurus.qt.qtgui.panel import TaurusValue -metachars = re.compile('([.][*])|([.][^*])|([$^+\-?{}\[\]|()])') +metachars = re.compile(r'([.][*])|([.][^*])|([$^+\-?{}\[\]|()])') def re_search_low(regexp, target): @@ -151,7 +151,7 @@ def get_readwrite_models(expressions, limit=1000): # self.debug( 'In TaurusGrid.get_all_models(%s:"%s") ...' % (type(expressions),expressions)) if isinstance(expressions, string_types): if any(re.match(s, expressions) for s in - ('\{.*\}', '\(.*\)', '\[.*\]')): + (r'\{.*\}', r'\(.*\)', r'\[.*\]')): # self.trace( 'evaluating expressions ....') expressions = list(eval(expressions)) else: diff --git a/lib/taurus/qt/qtgui/taurusgui/paneldescriptionwizard.py b/lib/taurus/qt/qtgui/taurusgui/paneldescriptionwizard.py index 0acf00297..6c9c3a429 100644 --- a/lib/taurus/qt/qtgui/taurusgui/paneldescriptionwizard.py +++ b/lib/taurus/qt/qtgui/taurusgui/paneldescriptionwizard.py @@ -64,7 +64,7 @@ def __init__(self, parent=None): # subwidgets self.moduleNameLE = Qt.QLineEdit() self.moduleNameLE.setValidator(Qt.QRegExpValidator( - Qt.QRegExp('[a-zA-Z0-9\.\_]*'), self.moduleNameLE)) + Qt.QRegExp(r'[a-zA-Z0-9\.\_]*'), self.moduleNameLE)) self.membersCB = Qt.QComboBox() self.dlgBox = Qt.QDialogButtonBox( Qt.QDialogButtonBox.Ok | Qt.QDialogButtonBox.Cancel) diff --git a/lib/taurus/test/base.py b/lib/taurus/test/base.py index e0111eca9..a51fa3b10 100644 --- a/lib/taurus/test/base.py +++ b/lib/taurus/test/base.py @@ -57,8 +57,7 @@ class based on a helper method. - test_skip (str): Optional. A reason for skipping the test. If None given, the test will not be skipped - - \*\*helper_kwargs: All remaining keyword arguments are passed to the - helper. + - All remaining keyword arguments are passed to the helper. This decorator can be considered a "base" decorator. It is often used to create other decorators in which the helper method is pre-set, as in diff --git a/lib/taurus/test/moduleexplorer.py b/lib/taurus/test/moduleexplorer.py index 40547b162..024969adc 100644 --- a/lib/taurus/test/moduleexplorer.py +++ b/lib/taurus/test/moduleexplorer.py @@ -217,17 +217,19 @@ def explore(modulename, exclude_patterns=(), verbose=True): return minfo, ModuleExplorer.getAll(minfo, 'warnings') -def main(modulename='taurus', exclude_patterns=( - '_[^\.]*[^_]', - '.*\.test', - 'taurus\.external', - 'taurus\.qt\.qtgui\.extra_sardana', - 'taurus\.qt\.qtgui\.extra_pool', - 'taurus\.qt\.qtgui\.extra_macroexecutor', - 'taurus\.qt\.qtgui\.resource', - 'taurus\.qt\.qtgui\.taurusgui\.conf', - ) - ): +def main( + modulename='taurus', + exclude_patterns=( + r'_[^\.]*[^_]', + r'.*\.test', + r'taurus\.external', + r'taurus\.qt\.qtgui\.extra_sardana', + r'taurus\.qt\.qtgui\.extra_pool', + r'taurus\.qt\.qtgui\.extra_macroexecutor', + r'taurus\.qt\.qtgui\.resource', + r'taurus\.qt\.qtgui\.taurusgui\.conf', + ) +): moduleinfo, allw = ModuleExplorer.explore( modulename, exclude_patterns=exclude_patterns, verbose=True) print('\n\n' + '*' * 50) diff --git a/lib/taurus/test/testsuite.py b/lib/taurus/test/testsuite.py index 2bf5878e5..cafbe4b21 100644 --- a/lib/taurus/test/testsuite.py +++ b/lib/taurus/test/testsuite.py @@ -111,7 +111,7 @@ def run(disableLogger=True, exclude_pattern='(?!)'): @click.option( '-e', '--exclude-pattern', 'exclude_pattern', default='(?!)', - help="""regexp pattern matching test ids to be excluded. + help=r"""regexp pattern matching test ids to be excluded. (e.g. 'taurus\.core\..*' would exclude taurus.core tests) """ ) @@ -121,7 +121,7 @@ def testsuite_cmd(gui_tests, exclude_pattern): taurus.test.skip.GUI_TESTS_ENABLED = gui_tests if not taurus.test.skip.GUI_TESTS_ENABLED: - exclude_pattern = '(taurus\.qt\..*)|(%s)' % exclude_pattern + exclude_pattern = r'(taurus\.qt\..*)|(%s)' % exclude_pattern else: exclude_pattern = exclude_pattern From 46ddb2cf96b9cb92e5106c3d7e51a1ba00948f05 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 00:57:41 +0200 Subject: [PATCH 312/373] replace Thread.isAlive --> is_alive Thread.isAlive was deprecated in 2.7 and is removed in 3.9/ Avoid it in our code --- lib/taurus/core/util/containers.py | 4 ++-- lib/taurus/core/util/threadpool.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/taurus/core/util/containers.py b/lib/taurus/core/util/containers.py index 61aca30ef..fc535bc1d 100644 --- a/lib/taurus/core/util/containers.py +++ b/lib/taurus/core/util/containers.py @@ -745,7 +745,7 @@ def start(self): import threading if not self.threaded: return - if hasattr(self, '_Thread') and self._Thread and self._Thread.isAlive(): + if hasattr(self, '_Thread') and self._Thread and self._Thread.is_alive(): return self.event = threading.Event() self.event.clear() @@ -763,7 +763,7 @@ def alive(self): if not hasattr(self, '_Thread') or not self._Thread: return False else: - return self._Thread.isAlive() + return self._Thread.is_alive() def __del__(self): self.stop() diff --git a/lib/taurus/core/util/threadpool.py b/lib/taurus/core/util/threadpool.py index 4adc43d25..960b6999a 100644 --- a/lib/taurus/core/util/threadpool.py +++ b/lib/taurus/core/util/threadpool.py @@ -102,7 +102,7 @@ def join(self): self.accept = False while True: for w in self.workers: - if w.isAlive(): + if w.is_alive(): self.jobs.put(self.NoJob) break else: From b5173ad82f8333e02ad99cd157c3dc316920b60c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 01:41:33 +0200 Subject: [PATCH 313/373] Use collections.abc for ABCs Since python3.3, Sequence and Mapping should be imported from collections.abc instead of from collections. Change code accordingly, keeping bck-compat for python2.7 --- lib/taurus/console/list.py | 9 ++++++--- lib/taurus/core/resource/resfactory.py | 7 +++++-- lib/taurus/core/tango/tangodatabase.py | 9 ++++++--- lib/taurus/core/taurusmodel.py | 7 +++++-- lib/taurus/core/util/containers.py | 8 +++++--- lib/taurus/core/util/event.py | 7 +++++-- lib/taurus/qt/qtgui/display/tauruslabel.py | 7 +++++-- lib/taurus/qt/qtgui/display/tauruslcd.py | 7 +++++-- lib/taurus/qt/qtgui/display/taurusled.py | 7 +++++-- lib/taurus/qt/qtgui/graphic/taurusgraphic.py | 7 +++++-- lib/taurus/qt/qtgui/panel/taurusinputpanel.py | 17 ++++++++++------- 11 files changed, 62 insertions(+), 30 deletions(-) diff --git a/lib/taurus/console/list.py b/lib/taurus/console/list.py index 9ebdf7bc2..ee951429d 100644 --- a/lib/taurus/console/list.py +++ b/lib/taurus/console/list.py @@ -31,7 +31,10 @@ from builtins import map from builtins import range import textwrap -import collections +try: + from collections.abc import Sequence +except ImportError: # bck-compat py 2.7 + from collections import Sequence from future.utils import string_types from .enums import Alignment @@ -87,7 +90,7 @@ def getRowSeparator(self): def setMaxColumnWidth(self, max_col_width): if max_col_width is None: max_col_width = -1 - if not isinstance(max_col_width, collections.Sequence): + if not isinstance(max_col_width, Sequence): max_col_width = self.col_nb * [max_col_width] self.MaxColumnWidth = max_col_width @@ -97,7 +100,7 @@ def getMaxColumnWidth(self): max_column_width = property(getMaxColumnWidth, setMaxColumnWidth) def setTextAlignment(self, text_alignment): - if not isinstance(text_alignment, collections.Sequence): + if not isinstance(text_alignment, Sequence): text_alignment = self.col_nb * [text_alignment] self.TextAlignment = text_alignment diff --git a/lib/taurus/core/resource/resfactory.py b/lib/taurus/core/resource/resfactory.py index 564164d48..c008c4ebe 100644 --- a/lib/taurus/core/resource/resfactory.py +++ b/lib/taurus/core/resource/resfactory.py @@ -31,7 +31,10 @@ import os import imp -import collections +try: + from collections.abc import Mapping +except ImportError: # bck-compat py 2.7 + from collections import Mapping from taurus.core.taurushelper import Manager from taurus.core.util.singleton import Singleton @@ -85,7 +88,7 @@ def reloadResource(self, obj=None, priority=1, name=None): """ if priority < 1: raise ValueError('priority must be >=1') - if isinstance(obj, collections.Mapping): + if isinstance(obj, Mapping): name = name or 'DICT%02d' % priority elif type(obj) in (str,) or obj is None: name, mod = self.__reloadResource(obj) diff --git a/lib/taurus/core/tango/tangodatabase.py b/lib/taurus/core/tango/tangodatabase.py index 0a46dd250..0af4e2990 100644 --- a/lib/taurus/core/tango/tangodatabase.py +++ b/lib/taurus/core/tango/tangodatabase.py @@ -31,7 +31,10 @@ from builtins import map from builtins import range from builtins import object -import collections +try: + from collections.abc import Mapping +except ImportError: # bck-compat py 2.7 + from collections import Mapping import os import weakref @@ -517,7 +520,7 @@ def __init__(self, other=None): def _update(self, other): try: - if isinstance(other, collections.Mapping): + if isinstance(other, Mapping): other = list(other.values()) for dev in other: try: @@ -563,7 +566,7 @@ def __init__(self, other=None): def _update(self, other): try: - if isinstance(other, collections.Mapping): + if isinstance(other, Mapping): other = list(other.values()) for serv in other: try: diff --git a/lib/taurus/core/taurusmodel.py b/lib/taurus/core/taurusmodel.py index 01662257f..196ba6f85 100644 --- a/lib/taurus/core/taurusmodel.py +++ b/lib/taurus/core/taurusmodel.py @@ -28,7 +28,10 @@ from builtins import object import weakref -import collections +try: + from collections.abc import Sequence +except ImportError: # bck-compat py 2.7 + from collections import Sequence from .util.log import Logger from .util.event import (CallableRef, @@ -277,7 +280,7 @@ def fireEvent(self, event_type, event_value, listeners=None): if listeners is None: return - if not isinstance(listeners, collections.Sequence): + if not isinstance(listeners, Sequence): listeners = listeners, for listener in listeners: diff --git a/lib/taurus/core/util/containers.py b/lib/taurus/core/util/containers.py index fc535bc1d..ca76e585f 100644 --- a/lib/taurus/core/util/containers.py +++ b/lib/taurus/core/util/containers.py @@ -35,10 +35,12 @@ from future.utils import string_types import copy -import collections import time import weakref - +try: + from collections.abc import Sequence +except ImportError: # bck-compat py 2.7 + from collections import Sequence __all__ = ["CaselessList", "CaselessDict", "CaselessWeakValueDict", "LoopList", "CircBuf", "LIFO", "TimedQueue", "self_locked", "ThreadDict", @@ -637,7 +639,7 @@ def __init__(self, arg=None): """ Initializes the list with a sequence or an initial value. """ if arg is None: list.__init__(self) - elif isinstance(arg, collections.Sequence): + elif isinstance(arg, Sequence): list.__init__(self, arg) else: list.__init__(self) diff --git a/lib/taurus/core/util/event.py b/lib/taurus/core/util/event.py index e77f6f206..316512d60 100644 --- a/lib/taurus/core/util/event.py +++ b/lib/taurus/core/util/event.py @@ -34,7 +34,10 @@ import weakref import threading import time -import collections +try: + from collections.abc import Sequence +except ImportError: # bck-compat py 2.7 + from collections import Sequence import taurus.core @@ -695,7 +698,7 @@ def __init__(self, *attrs): self.connect(attrs) def connect(self, attrs): - if not isinstance(attrs, collections.Sequence): + if not isinstance(attrs, Sequence): attrs = (attrs,) self.disconnect() self._attrs = attrs diff --git a/lib/taurus/qt/qtgui/display/tauruslabel.py b/lib/taurus/qt/qtgui/display/tauruslabel.py index 15c807088..d2f1d42ce 100644 --- a/lib/taurus/qt/qtgui/display/tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/tauruslabel.py @@ -29,7 +29,10 @@ from builtins import str from builtins import object -import collections +try: + from collections.abc import Sequence +except ImportError: # bck-compat py 2.7 + from collections import Sequence import re from taurus.core.taurusbasetypes import (TaurusElementType, TaurusEventType, @@ -350,7 +353,7 @@ def setModelIndex(self, modelIndex): return if type(mi_value) == int: mi_value = mi_value, - if not isinstance(mi_value, collections.Sequence): + if not isinstance(mi_value, Sequence): return self._modelIndex = mi_value self._modelIndexStr = mi diff --git a/lib/taurus/qt/qtgui/display/tauruslcd.py b/lib/taurus/qt/qtgui/display/tauruslcd.py index dcc395ad5..ca905eb46 100644 --- a/lib/taurus/qt/qtgui/display/tauruslcd.py +++ b/lib/taurus/qt/qtgui/display/tauruslcd.py @@ -30,7 +30,10 @@ from builtins import str from builtins import object -import collections +try: + from collections.abc import Sequence +except ImportError: # bck-compat py 2.7 + from collections import Sequence from taurus.core.taurusbasetypes import (TaurusElementType, TaurusEventType, AttrQuality, TaurusDevState) @@ -284,7 +287,7 @@ def setModelIndex(self, modelIndex): return if type(mi_value) == int: mi_value = mi_value, - if not isinstance(mi_value, collections.Sequence): + if not isinstance(mi_value, Sequence): return self._modelIndex = mi_value self._modelIndexStr = mi diff --git a/lib/taurus/qt/qtgui/display/taurusled.py b/lib/taurus/qt/qtgui/display/taurusled.py index b4195a7a6..d7ccadfa5 100644 --- a/lib/taurus/qt/qtgui/display/taurusled.py +++ b/lib/taurus/qt/qtgui/display/taurusled.py @@ -31,7 +31,10 @@ from builtins import object import weakref -import collections +try: + from collections.abc import Sequence +except ImportError: # bck-compat py 2.7 + from collections import Sequence from taurus.external.qt import Qt @@ -349,7 +352,7 @@ def setModelIndex(self, modelIndex): return if type(mi_value) == int: mi_value = mi_value, - if not isinstance(mi_value, collections.Sequence): + if not isinstance(mi_value, Sequence): return self._modelIndex = mi_value self._modelIndexStr = mi diff --git a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py index 847c2636e..34686154f 100644 --- a/lib/taurus/qt/qtgui/graphic/taurusgraphic.py +++ b/lib/taurus/qt/qtgui/graphic/taurusgraphic.py @@ -39,7 +39,10 @@ import os import subprocess import traceback -import collections +try: + from collections.abc import Sequence +except ImportError: # bck-compat py 2.7 + from collections import Sequence from future.utils import string_types from queue import Queue @@ -153,7 +156,7 @@ def run(self): break else: continue - if not isinstance(item, collections.Sequence): + if not isinstance(item, Sequence): item = (item,) # @todo: Unless the call to boundingRect() has a side effect, this line is useless.. probably related to todo in _updateView() item_rects = [i.boundingRect() for i in item] diff --git a/lib/taurus/qt/qtgui/panel/taurusinputpanel.py b/lib/taurus/qt/qtgui/panel/taurusinputpanel.py index 6df5d2462..5852bad58 100644 --- a/lib/taurus/qt/qtgui/panel/taurusinputpanel.py +++ b/lib/taurus/qt/qtgui/panel/taurusinputpanel.py @@ -29,7 +29,10 @@ from builtins import str from builtins import object -import collections +try: + from collections.abc import Sequence, Mapping +except ImportError: # bck-compat py 2.7 + from collections import Sequence, Mapping import numpy from future.utils import string_types @@ -111,7 +114,7 @@ def fill_main_panel(self, panel, input_data): layout = Qt.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) panel.setLayout(layout) - if isinstance(input_data, collections.Mapping): + if isinstance(input_data, Mapping): single_panel, getter = self.create_single_input_panel(input_data) layout.addWidget(single_panel) self.value = getter @@ -127,7 +130,7 @@ def create_single_input_panel(self, input_data): data_type = input_data.get('data_type', 'String') is_seq = not isinstance(data_type, string_types) and \ - isinstance(data_type, collections.Sequence) + isinstance(data_type, Sequence) if is_seq: panel, getter = self.create_selection_panel(input_data) else: @@ -167,7 +170,7 @@ def _create_combobox_panel(self, input_data): items = input_data['data_type'] for item in items: is_seq = not isinstance(item, string_types) and \ - isinstance(item, collections.Sequence) + isinstance(item, Sequence) if is_seq: text, userData = item else: @@ -189,7 +192,7 @@ def _create_radiobutton_panel(self, input_data): buttongroup.setExclusive(True) for item in items: is_seq = not isinstance(item, string_types) and \ - isinstance(item, collections.Sequence) + isinstance(item, Sequence) if is_seq: text, userData = item else: @@ -216,7 +219,7 @@ def _create_multi_selection_panel(self, input_data): if default_value is None: default_value = () dft_is_seq = not isinstance(default_value, string_types) and \ - isinstance(default_value, collections.Sequence) + isinstance(default_value, Sequence) if not dft_is_seq: default_value = default_value, @@ -225,7 +228,7 @@ def _create_multi_selection_panel(self, input_data): for item in items: is_seq = not isinstance(item, string_types) and \ - isinstance(item, collections.Sequence) + isinstance(item, Sequence) if is_seq: text, userData = item else: From 964b18fa8a140d143f6377a59c0378f18c812ace Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 02:03:46 +0200 Subject: [PATCH 314/373] Avoid using deprecated logging.warn Use logging.warning instead --- lib/taurus/core/taurusdatabase.py | 6 +++--- lib/taurus/core/util/argparse/taurusargparse.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/taurus/core/taurusdatabase.py b/lib/taurus/core/taurusdatabase.py index 2f6631d16..7a2fdd3ef 100644 --- a/lib/taurus/core/taurusdatabase.py +++ b/lib/taurus/core/taurusdatabase.py @@ -27,8 +27,8 @@ TaurusDatabase to TaurusAuthority""" -from logging import warn -warn('taurusdatabase module is deprecated. Use taurusauthority instead') +from logging import warning +warning('taurusdatabase module is deprecated. Use taurusauthority instead') import traceback traceback.print_stack() @@ -46,5 +46,5 @@ # TaurusDevClassInfo # from taurus.core.tango.tangodatabase import TangoServInfo as TaurusServInfo # except ImportError, e: -# warn('taurusdatabase: Cannot import tango info objects: %s', repr(e)) +# warning('taurusdatabase: Cannot import tango info objects: %s', repr(e)) # diff --git a/lib/taurus/core/util/argparse/taurusargparse.py b/lib/taurus/core/util/argparse/taurusargparse.py index 4555f019f..597098f2a 100644 --- a/lib/taurus/core/util/argparse/taurusargparse.py +++ b/lib/taurus/core/util/argparse/taurusargparse.py @@ -110,7 +110,7 @@ def get_taurus_parser(parser=None): "Basic options present in any taurus application") help_tauruslog = "taurus log level. Allowed values are (case insensitive): critical, "\ - "error, warning/warn, info, debug, trace" + "error, warning, info, debug, trace" help_tangohost = "Tango host name (either HOST:PORT or a Taurus URI, e.g. tango://foo:1234)" help_tauruspolling = "taurus global polling period in milliseconds" help_taurusserial = "taurus serialization mode. Allowed values are (case insensitive): "\ From 6cb42ffd025817e827deda7778f99e7688ec3726 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 3 Jun 2020 04:44:37 +0200 Subject: [PATCH 315/373] Avoid file not closed warning Ensure file is closed. --- lib/taurus/qt/qtgui/icon/icon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/icon/icon.py b/lib/taurus/qt/qtgui/icon/icon.py index 78c7ded42..3a3fac582 100644 --- a/lib/taurus/qt/qtgui/icon/icon.py +++ b/lib/taurus/qt/qtgui/icon/icon.py @@ -134,7 +134,8 @@ def registerPathFiles(pathfilenames): """ for filename in pathfilenames: try: - pathmap = json.load(open(filename)) + with open(filename) as f: + pathmap = json.load(f) except Exception as e: __LOGGER.error('Error registering "%s": %r', filename, e) pathmap = [] From 4aaf110a5b5684dcaea4c0b27f5e79740c80ab37 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 12 Jun 2020 16:38:05 +0200 Subject: [PATCH 316/373] Avoid warning in write_read_attribute tests AttributeTestCase.write_read_attribute() issues a pint warning. Refactor it to avoid unnecessary downcasting of Quantities to numpy arrays. --- .../core/tango/test/test_tangoattribute.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/taurus/core/tango/test/test_tangoattribute.py b/lib/taurus/core/tango/test/test_tangoattribute.py index 3cf2c7fbc..429a84658 100644 --- a/lib/taurus/core/tango/test/test_tangoattribute.py +++ b/lib/taurus/core/tango/test/test_tangoattribute.py @@ -735,7 +735,7 @@ def _getDecodePyTangoAttr(self, attr_name, cfg): dev = PyTango.DeviceProxy(self.DEV_NAME) infoex = dev.get_attribute_config_ex(attr_name)[0] try: - unit = UR.parse_units(infoex.unit) + unit = UR.parse_units(infoex.unit) except (UndefinedUnitError, UnicodeDecodeError): unit = UR.parse_units(None) if cfg in ['range', 'alarms', 'warnings']: @@ -809,7 +809,7 @@ def write_read_attr(self, attrname=None, setvalue=None, expected=None, (attrname, k)) self.fail(msg) msg = ('%s for "%s" should be %r (got %r)' % - (k, attrname, exp, got)) + (k, attrname, exp, got)) self.__assertValidValue(exp, got, msg) # Test attribute value @@ -836,18 +836,22 @@ def __assertValidValue(self, exp, got, msg): if isinstance(exp, Quantity): exp = exp.magnitude try: - # for those values that can be handled by numpy.allclose() - chk = numpy.allclose(got, exp) + # first try the most generic equality + chk = bool(got == exp) except: - if isinstance(got, numpy.ndarray): - # uchars were not handled with allclose - # UGLY!! but numpy.all does not work - chk = got.tolist() == exp.tolist() - else: - # for the rest - chk = bool(got == exp) - + chk = False + if not chk: + # some cases may fail the simple equality but still be True + try: + # for those values that can be handled by numpy.allclose() + chk = numpy.allclose(got, exp) + except: + if isinstance(got, numpy.ndarray): + # uchars were not handled with allclose + # UGLY!! but numpy.all does not work + chk = got.tolist() == exp.tolist() self.assertTrue(chk, msg) + if __name__ == '__main__': pass From 5bc24ad6ca4b8cd255bcd8b181f3057f5569221f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 12 Jun 2020 16:53:36 +0200 Subject: [PATCH 317/373] Use numpy.frombuffer instead of numpy.fromstring Usage of the VideoImageCodec raises the following deprecation warning in py3: "DeprecationWarning: The binary mode of fromstring is deprecated, as it behaves surprisingly on unicode inputs. Use frombuffer instead" Replace fromstring -> frombuffer accordingly. --- lib/taurus/core/util/codecs.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/taurus/core/util/codecs.py b/lib/taurus/core/util/codecs.py index 15cdd0ee9..686090667 100644 --- a/lib/taurus/core/util/codecs.py +++ b/lib/taurus/core/util/codecs.py @@ -602,7 +602,7 @@ def decode(self, data, *args, **kwargs): if header['imageMode'] == 6: # RGB24, 3 bytes per pixel - rgba = numpy.fromstring(imgBuffer, dtype) + rgba = numpy.frombuffer(imgBuffer, dtype) bbuf = rgba[0::3] gbuf = rgba[1::3] rbuf = rgba[2::3] @@ -613,7 +613,7 @@ def decode(self, data, *args, **kwargs): elif header['imageMode'] == 7: # RGBA 4 bytes per pixel - rgba = numpy.fromstring(imgBuffer, dtype) + rgba = numpy.frombuffer(imgBuffer, dtype) bbuf = rgba[0::4] gbuf = rgba[1::4] rbuf = rgba[2::4] @@ -626,7 +626,7 @@ def decode(self, data, *args, **kwargs): elif header['imageMode'] == 17: # YUV444 3 bytes per pixel - yuv = numpy.fromstring(imgBuffer, dtype) + yuv = numpy.frombuffer(imgBuffer, dtype) y = yuv[0::3] u = yuv[1::3] v = yuv[2::3] @@ -643,7 +643,7 @@ def decode(self, data, *args, **kwargs): elif header['imageMode'] == 16: # YUV422 4 bytes per 2 pixels - yuv = numpy.fromstring(imgBuffer, dtype) + yuv = numpy.frombuffer(imgBuffer, dtype) u = yuv[0::4] y1 = yuv[1::4] v = yuv[2::4] @@ -670,7 +670,7 @@ def decode(self, data, *args, **kwargs): elif header['imageMode'] == 15: # YUV411 6 bytes per 4 pixels - yuv = numpy.fromstring(imgBuffer, dtype) + yuv = numpy.frombuffer(imgBuffer, dtype) u = yuv[0::6] y1 = yuv[1::6] y2 = yuv[2::6] @@ -699,7 +699,7 @@ def decode(self, data, *args, **kwargs): img2D = numpy.dstack((r, g, b)) else: - img1D = numpy.fromstring(imgBuffer, dtype) + img1D = numpy.frombuffer(imgBuffer, dtype) img2D = img1D.reshape(header['height'], header['width']) return fmt, img2D From 66e4d32e61b84e1daed06c13ac0f936b4af487e2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 11 Jun 2020 17:40:06 +0200 Subject: [PATCH 318/373] Add pytest.ini to consider warnings as errors Ignore 3rd-party issued deprecation warnings and treat all warnings issued by taurus as errors (tests should fail if taurus emits a warning) --- pytest.ini | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..05cfcd44a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +filterwarnings = + # Ignore deprecation warnings issued by 3rd party modules + ignore:.*:DeprecationWarning:^(?!taurus).*$ + # Treat taurus issued warnings as errors + error:.*:Warning:^taurus.*$ From 6492eb2a80db5cfcf9b0b8bd903ae054a14759e5 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 18 Jun 2020 16:00:35 +0200 Subject: [PATCH 319/373] Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee7053457..d5e8f20a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,11 +28,14 @@ develop branch) won't be reflected in this file. - Modules registered with `"taurus.qt.qtgui"` entry-point are now lazy-loaded (#1090) - The control over which custom widgets are to be used in a TaurusForm is now done by registering factories to `"taurus.form.item_factories"` entry-point (#1108) +- Qt unit tests reimplemented using pytest-qt (#1114) ### Deprecated - `TaurusBaseWidget.showFormatterDlg()` (#1039) - Custom widget API in TaurusForm, TaurusValue and TaurusGui (#1108) - `tauruscustomsettings.T_FORM_CUSTOM_WIDGET_MAP` (#1108) +- `BaseWidgetTestCase` and `GenericWidgetTestCase` (#1114) +- `TimeOut` Device Server (#1114) ### Fixed - Several issues in TaurusWheelEdit (#1010, #1021) @@ -44,7 +47,7 @@ develop branch) won't be reflected in this file. - Exception in DelayedSubscriber (#1030) - Compatibility issue in deprecated TangoAttribute's `isScalar()` and `isSpectrum()` (#1034) - Some issues in taurus v3 to v4 migration support (#1059) -- Some CI test issues (#1075, #1069, #1109) +- Some CI test issues (#1075, #1069, #1109, #1114) ## [4.6.1] - 2019-08-19 Hotfix for auto-deployment in PyPI with Travis. No other difference from 4.6.0. From e1b110ed019f5e6e46fb3cf3223dbb24c232c2f9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 22 Jun 2020 11:06:36 +0200 Subject: [PATCH 320/373] LineEdit: enforce explicit disable TaurusValueLineEdit auto enables/disables itself based based on events. This precludes the user from explicitly disable it. Fix by avoiding reenabling if setEnabled(False) is called. Note that auto-disabling on errors is still allowed even if setEnabled(True) is called, since the widget is not usable in case of error. --- lib/taurus/qt/qtgui/input/tauruslineedit.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/taurus/qt/qtgui/input/tauruslineedit.py b/lib/taurus/qt/qtgui/input/tauruslineedit.py index 931a30408..a36e8abf7 100644 --- a/lib/taurus/qt/qtgui/input/tauruslineedit.py +++ b/lib/taurus/qt/qtgui/input/tauruslineedit.py @@ -77,6 +77,7 @@ def __init__(self, qt_parent=None, designMode=False): self._enableWheelEvent = False self._last_value = None self._singleStep = 1. + self._allow_auto_enable = True self.setAlignment(Qt.Qt.AlignRight) self.setValidator(None) @@ -146,7 +147,10 @@ def handleEvent(self, evt_src, evt_type, evt_value): except Exception as e: self.info('Failed attempt to initialize value: %r', e) - self.setEnabled(evt_type != TaurusEventType.Error) + if self._allow_auto_enable: + # use QLineEdit.setEnabled in order to avoid changing + # _allow_auto_enable status + Qt.QLineEdit.setEnabled(self, evt_type != TaurusEventType.Error) if evt_type in (TaurusEventType.Change, TaurusEventType.Periodic): self._updateValidator(evt_value) @@ -157,6 +161,14 @@ def handleEvent(self, evt_src, evt_type, evt_value): if evt_type == TaurusEventType.Error: self.updateStyle() + def setEnabled(self, enabled): + """Reimplement from :class:`QLineEdit` to avoid autoenabling if the + widget is explicitly disabled (but allow auto-disabling if the + widget is explicitly enabled) + """ + self._allow_auto_enable = enabled + return Qt.QLineEdit.setEnabled(self, enabled) + def isTextValid(self): """ Validates current text From 616d4b69492eb91ecab8b3411c27a6a9f5b8d44f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 23 Jun 2020 12:34:57 +0200 Subject: [PATCH 321/373] Support py2 in selectEntryPoints selectEntryPoints uses re.fullmatch, which was introduced in py 3.4 . Emulate fullmatch to support py2 --- lib/taurus/core/util/plugin.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/taurus/core/util/plugin.py b/lib/taurus/core/util/plugin.py index b91db3757..bdcc62535 100644 --- a/lib/taurus/core/util/plugin.py +++ b/lib/taurus/core/util/plugin.py @@ -31,6 +31,17 @@ import re import pkg_resources +try: + from re import fullmatch +except ImportError: # TODO: remove this when dropping support of py2 + def fullmatch(regex, string, flags=0): + """ + Emulate python-3.4 re.fullmatch()... mostly + See: https://stackoverflow.com/a/30212414 + """ + m = re.match(regex, string, flags=flags) + if m and m.span()[1] == len(string): + return m class EntryPointAlike(object): @@ -107,7 +118,7 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): # filter out the entry points whose name matches a exclude pattern for p in exclude: - remaining = [e for e in remaining if not re.fullmatch(p, e.name)] + remaining = [e for e in remaining if not fullmatch(p, e.name)] # sort the remaining entry points alphabetically remaining.sort(key=lambda e: e.name) @@ -131,7 +142,7 @@ def selectEntryPoints(group=None, include=('.*',), exclude=()): tmp = remaining remaining = [] for e in tmp: - if re.fullmatch(p, e.name): + if fullmatch(p, e.name): ret.append(e) else: remaining.append(e) From fc6dd3ee7ef9c9d1f84b62ae3bd4461fdb664bff Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 23 Jun 2020 12:47:16 +0200 Subject: [PATCH 322/373] Add plot subcommand Add a plot subcommand to taurus that uses entry points to select alternative implementations of the TaurusPlot class --- lib/taurus/cli/common.py | 14 ++++ lib/taurus/cli/plot.py | 135 +++++++++++++++++++++++++++++++ lib/taurus/cli/test/test_plot.py | 88 ++++++++++++++++++++ setup.py | 3 +- 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 lib/taurus/cli/plot.py create mode 100644 lib/taurus/cli/test/test_plot.py diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 04120dfa9..71a986919 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -152,3 +152,17 @@ def window_name(default): default=None, help="Override the default formatter (use with caution!)", ) + +list_alternatives = click.option( + '--ls-alt', + is_flag=True, + help="List the available alternative implementations", +) + +use_alternative = click.option( + '--use-alt', + metavar="ALT", + type=click.STRING, + default=None, + help="Use ALT alternative implementation" +) \ No newline at end of file diff --git a/lib/taurus/cli/plot.py b/lib/taurus/cli/plot.py new file mode 100644 index 000000000..8a215d6fb --- /dev/null +++ b/lib/taurus/cli/plot.py @@ -0,0 +1,135 @@ +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +import sys +import click +import taurus.cli.common +from taurus.core.util.plugin import selectEntryPoints +from taurus import tauruscustomsettings as _ts +from taurus import warning +from taurus.qt.qtgui.application import TaurusApplication + + +EP_GROUP_PLOT = "taurus.alt.plots" + + +def _print_alts(group): + alts = [ep.name for ep in selectEntryPoints(group)] + print("Available alternatives :\n {}\n".format("\n ".join(alts))) + + +def _load_class_from_group(group, include=(".*",), exclude=()): + """ + Factory that returns the first available class from the group entry point. + The selection is done among the classes registered in + the `group` entry-point, prioritized according to the given + `include` and `exclude` patterns + (see :function:`taurus.core.util.plugin.selectEntryPoints`) + """ + eps = selectEntryPoints(group, include=include, exclude=exclude) + for ep in eps: + try: + return ep.load(), ep.name + except: + pass + raise ImportError("Could not load any class from {}".format(eps)) + + +x_axis_mode_option = click.option( + "-x", + "--x-axis-mode", + "x_axis_mode", + type=click.Choice(["t", "n"]), + default="n", + show_default=True, + help=( + 'X axis mode. "t" implies using a Date axis' + + '"n" uses the regular axis' + ), +) + + +@click.command("plot") +@taurus.cli.common.models +@taurus.cli.common.config_file +@x_axis_mode_option +@taurus.cli.common.demo +@taurus.cli.common.window_name("TaurusPlot") +@taurus.cli.common.use_alternative +@taurus.cli.common.list_alternatives +def plot_cmd( + models, config_file, x_axis_mode, demo, window_name, use_alt, ls_alt +): + """Shows a plot for the given models""" + + if ls_alt: + _print_alts(EP_GROUP_PLOT) + sys.exit(0) + + if use_alt is None: + use_alt = getattr(_ts, "PLOT_IMPL", ".*") + + try: + TPlot, epname = _load_class_from_group(EP_GROUP_PLOT, include=[use_alt]) + except: + _print_alts(EP_GROUP_PLOT) + sys.exit(1) + + app = TaurusApplication(app_name="taurusplot({})".format(epname)) + + w = TPlot() + w.setWindowTitle(window_name) + + if demo: + models = list(models) + models.extend(["eval:rand(100)", "eval:0.5*sqrt(arange(100))"]) + + try: + w.set_x_axis_mode(x_axis_mode) + except Exception as e: + warning( + "Could not set X axis mode to '%s' on %s plot. Reason: %s", + x_axis_mode, + epname, + e + ) + sys.exit(1) + + if config_file is not None: + try: + w.loadConfigFile(config_file) + except Exception as e: + warning( + "Could not load config file '%s' on %s plot. Reason: %s", + config_file, + epname, + e + ) + sys.exit(1) + + if models: + w.setModel(models) + + w.show() + sys.exit(app.exec_()) + diff --git a/lib/taurus/cli/test/test_plot.py b/lib/taurus/cli/test/test_plot.py new file mode 100644 index 000000000..8b51aa951 --- /dev/null +++ b/lib/taurus/cli/test/test_plot.py @@ -0,0 +1,88 @@ +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +import pytest +from taurus.core.util.test.test_plugin import mock_entry_point +from taurus.cli.plot import _load_class_from_group, plot_cmd +from click.testing import CliRunner + +runner = CliRunner() + +class _MockPlot(object): + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.model = None + + def setModel(self, model): + self.model = model + +def test_load_good_class_from_group(): + """Check get_plot_class with several registered plot alternatives""" + lines = ["mock{}={}:_MockPlot".format(i, __name__) for i in range(3)] + mapping = mock_entry_point(lines) + group_name = list(mapping.keys())[0] + assert _load_class_from_group(group_name) == (_MockPlot, "mock0") + + +def test_load_onebad_class_from_group(): + """Check get_plot_class with a bad and a good plot alternatives""" + lines = ["bad=_unimportablemod:Bad", "good={}:_MockPlot".format(__name__)] + mapping = mock_entry_point(lines) + group_name = list(mapping.keys())[0] + assert _load_class_from_group(group_name) == (_MockPlot, "good") + + +def test_load_onlybad_class_from_group(): + """Check get_plot_class with no plot alternatives""" + lines = ["bad=_unimportablemod:Bad"] + mapping = mock_entry_point(lines) + group_name = list(mapping.keys())[0] + with pytest.raises(ImportError) as exc_info: + _load_class_from_group(group_name) + assert "bad = _unimportablemod:Bad" in str(exc_info.value) + + +def test_plot_cmd_ls_alt(): + response = runner.invoke(plot_cmd, ["--ls-alt"]) + assert response.exit_code == 0 + assert "Available alternatives" in response.output + + +def test_plot_cmd_use_alt(): + response = runner.invoke(plot_cmd, ["--use-alt", "_non_existent_alt_"]) + assert "Available alternatives" in response.output + assert response.exit_code == 1 + + +def test_plot_cmd_help(): + response = runner.invoke(plot_cmd, ["--help"]) + assert "--demo" in response.output + assert "--ls-alt" in response.output + assert "--use-alt" in response.output + assert "--config" in response.output + assert "--window-name" in response.output + assert "--x-axis-mode" in response.output + assert "-x" in response.output + assert "--help" in response.output + assert response.exit_code == 0 \ No newline at end of file diff --git a/setup.py b/setup.py index ffaa8a661..10aac8fea 100644 --- a/setup.py +++ b/setup.py @@ -111,7 +111,8 @@ def get_release_info(): 'demo = taurus.qt.qtgui.panel.taurusdemo:demo_cmd', 'logmon = taurus.core.util.remotelogmonitor:logmon_cmd', 'qlogmon = taurus.qt.qtgui.table.qlogtable:qlogmon_cmd', - 'check-deps = taurus.core.taurushelper:check_dependencies_cmd' + 'check-deps = taurus.core.taurushelper:check_dependencies_cmd', + 'plot = taurus.cli.plot:plot_cmd', ] model_selectors = [ From 20b5541bc3cf4eda93e05d698b7bc36e14481a8e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 23 Jun 2020 12:49:50 +0200 Subject: [PATCH 323/373] Register qwt5 as an alternative for TaurusPlot Register in the corresponding group and implement .set_x_axis_mode() to comply with expected API --- lib/taurus/qt/qtgui/qwt5/taurusplot.py | 4 ++++ setup.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/lib/taurus/qt/qtgui/qwt5/taurusplot.py b/lib/taurus/qt/qtgui/qwt5/taurusplot.py index d562e6341..769994dc1 100644 --- a/lib/taurus/qt/qtgui/qwt5/taurusplot.py +++ b/lib/taurus/qt/qtgui/qwt5/taurusplot.py @@ -3568,6 +3568,10 @@ def resetXIsTime(self): '''equivalent to setXIsTime(False)''' self.setXIsTime(False) + def set_x_axis_mode(self, x_axis_mode): + """Required generic TaurusPlot API """ + self.setXIsTime(x_axis_mode.lower() == "t") + @Qt.pyqtSlot(bool) def setAllowZoomers(self, allow): '''enable/disable the zoomers for the plot. (The zoomers provide zooming diff --git a/setup.py b/setup.py index 10aac8fea..4b87f1b03 100644 --- a/setup.py +++ b/setup.py @@ -115,6 +115,10 @@ def get_release_info(): 'plot = taurus.cli.plot:plot_cmd', ] +plot_alternatives = [ + "qwt5 = taurus.qt.qtgui.qwt5:TaurusPlot", +] + model_selectors = [ 'Tango = taurus.qt.qtgui.panel.taurusmodelchooser:TangoModelSelectorItem', ] @@ -131,6 +135,7 @@ def get_release_info(): 'taurus.cli.subcommands': taurus_subcommands, 'taurus.qt.qtgui.panel.TaurusModelSelector.items': model_selectors, 'taurus.qt.formatters': formatters, + 'taurus.alt.plots': plot_alternatives, } classifiers = [ From 0723bf8dbc0dc1c386f0ba2ac8480e543c0066d4 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 23 Jun 2020 15:52:31 +0200 Subject: [PATCH 324/373] Add __init__.py to test dir --- lib/taurus/cli/test/__init__.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 lib/taurus/cli/test/__init__.py diff --git a/lib/taurus/cli/test/__init__.py b/lib/taurus/cli/test/__init__.py new file mode 100644 index 000000000..37a9eb713 --- /dev/null +++ b/lib/taurus/cli/test/__init__.py @@ -0,0 +1,24 @@ +############################################################################# +## +# This file is part of Taurus +## +# http://taurus-scada.org +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Taurus is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Taurus is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Taurus. If not, see . +## +############################################################################# + +"""Tests for taurus.cli""" \ No newline at end of file From 80d818b8094129bcc1e4c1175e997becf45d544f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 25 Jun 2020 13:16:39 +0200 Subject: [PATCH 325/373] rename set_x_axis_mode to setXAxisMode Use Qt naming style (CamelCase) for consistency --- lib/taurus/cli/{plot.py => alt.py} | 2 +- lib/taurus/qt/qtgui/qwt5/taurusplot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/taurus/cli/{plot.py => alt.py} (98%) diff --git a/lib/taurus/cli/plot.py b/lib/taurus/cli/alt.py similarity index 98% rename from lib/taurus/cli/plot.py rename to lib/taurus/cli/alt.py index 8a215d6fb..a3644c2d2 100644 --- a/lib/taurus/cli/plot.py +++ b/lib/taurus/cli/alt.py @@ -105,7 +105,7 @@ def plot_cmd( models.extend(["eval:rand(100)", "eval:0.5*sqrt(arange(100))"]) try: - w.set_x_axis_mode(x_axis_mode) + w.setXAxisMode(x_axis_mode) except Exception as e: warning( "Could not set X axis mode to '%s' on %s plot. Reason: %s", diff --git a/lib/taurus/qt/qtgui/qwt5/taurusplot.py b/lib/taurus/qt/qtgui/qwt5/taurusplot.py index 769994dc1..b7afe8d94 100644 --- a/lib/taurus/qt/qtgui/qwt5/taurusplot.py +++ b/lib/taurus/qt/qtgui/qwt5/taurusplot.py @@ -3568,7 +3568,7 @@ def resetXIsTime(self): '''equivalent to setXIsTime(False)''' self.setXIsTime(False) - def set_x_axis_mode(self, x_axis_mode): + def setXAxisMode(self, x_axis_mode): """Required generic TaurusPlot API """ self.setXIsTime(x_axis_mode.lower() == "t") From d40063385728ea8f0d41988344b0e893a16e6570 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 25 Jun 2020 13:20:01 +0200 Subject: [PATCH 326/373] group plot_cmd option tests --- lib/taurus/cli/test/{test_plot.py => test_alt.py} | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) rename lib/taurus/cli/test/{test_plot.py => test_alt.py} (89%) diff --git a/lib/taurus/cli/test/test_plot.py b/lib/taurus/cli/test/test_alt.py similarity index 89% rename from lib/taurus/cli/test/test_plot.py rename to lib/taurus/cli/test/test_alt.py index 8b51aa951..6684bab7d 100644 --- a/lib/taurus/cli/test/test_plot.py +++ b/lib/taurus/cli/test/test_alt.py @@ -63,7 +63,7 @@ def test_load_onlybad_class_from_group(): assert "bad = _unimportablemod:Bad" in str(exc_info.value) -def test_plot_cmd_ls_alt(): +def test_plot_cmd_options(): response = runner.invoke(plot_cmd, ["--ls-alt"]) assert response.exit_code == 0 assert "Available alternatives" in response.output @@ -74,6 +74,15 @@ def test_plot_cmd_use_alt(): assert "Available alternatives" in response.output assert response.exit_code == 1 + response = runner.invoke(plot_cmd, ["-x", "unsupported"]) + assert "Invalid value" in response.output + assert response.exit_code == 2 + + response = runner.invoke(plot_cmd, ["--config", "_non_existent_"]) + assert "Invalid value" in response.output + assert "No such file or directory" in response.output + assert response.exit_code == 2 + def test_plot_cmd_help(): response = runner.invoke(plot_cmd, ["--help"]) From 12df57c225e2cc9314d70cf4b15e1e6d69748743 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 25 Jun 2020 13:24:33 +0200 Subject: [PATCH 327/373] Add taurus trend subcommand - Register trend class for "taurus.trend.alts" EP - Adapt TaurusTrend class to provide the minimum required API for generic TaurusTrend in taurus and register it. - Adapt to change in "taurus.plot.alts" entry point name --- lib/taurus/cli/alt.py | 161 ++++++++++++++++++++++++++++---- lib/taurus/cli/test/test_alt.py | 51 ++++++++-- setup.py | 10 +- 3 files changed, 195 insertions(+), 27 deletions(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index a3644c2d2..2f3b1bb66 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -26,16 +26,17 @@ import taurus.cli.common from taurus.core.util.plugin import selectEntryPoints from taurus import tauruscustomsettings as _ts -from taurus import warning +from taurus import warning, info from taurus.qt.qtgui.application import TaurusApplication -EP_GROUP_PLOT = "taurus.alt.plots" +EP_GROUP_PLOT = "taurus.plot.alts" +EP_GROUP_TREND = "taurus.trend.alts" def _print_alts(group): alts = [ep.name for ep in selectEntryPoints(group)] - print("Available alternatives :\n {}\n".format("\n ".join(alts))) + print("Registered alternatives :\n {}\n".format("\n ".join(alts))) def _load_class_from_group(group, include=(".*",), exclude=()): @@ -51,28 +52,50 @@ def _load_class_from_group(group, include=(".*",), exclude=()): try: return ep.load(), ep.name except: - pass + info("Cannot load %s", ep.name) raise ImportError("Could not load any class from {}".format(eps)) -x_axis_mode_option = click.option( - "-x", - "--x-axis-mode", - "x_axis_mode", - type=click.Choice(["t", "n"]), - default="n", - show_default=True, - help=( - 'X axis mode. "t" implies using a Date axis' - + '"n" uses the regular axis' - ), +def x_axis_mode_option(default="n"): + x_axis_mode_option = click.option( + "-x", + "--x-axis-mode", + "x_axis_mode", + type=click.Choice(["t", "n"]), + default=default, + show_default=True, + help=('X axis mode. Use "t" for a time axis or "n" for a regular one'), + ) + return x_axis_mode_option + + +def max_buffer_option(default): + o = click.option( + '-b', '--buffer', 'max_buffer_size', + type=int, + default=default, + show_default=True, + help=("Maximum number of values to be stacked " + + "(when reached, the oldest values will be " + + "discarded)"), + ) + return o + +forced_read_option = click.option( + '-r', + '--forced-read', + 'forced_read_period', + type=int, + default=-1, + metavar="MILLISECONDS", + help="force re-reading of the attributes every MILLISECONDS ms" ) @click.command("plot") @taurus.cli.common.models @taurus.cli.common.config_file -@x_axis_mode_option +@x_axis_mode_option("n") @taurus.cli.common.demo @taurus.cli.common.window_name("TaurusPlot") @taurus.cli.common.use_alternative @@ -108,7 +131,7 @@ def plot_cmd( w.setXAxisMode(x_axis_mode) except Exception as e: warning( - "Could not set X axis mode to '%s' on %s plot. Reason: %s", + 'Could not set X axis mode to "%s" on %s plot. Reason: "%s"', x_axis_mode, epname, e @@ -120,7 +143,7 @@ def plot_cmd( w.loadConfigFile(config_file) except Exception as e: warning( - "Could not load config file '%s' on %s plot. Reason: %s", + 'Could not load config file "%s" on %s plot. Reason: "%s"', config_file, epname, e @@ -133,3 +156,105 @@ def plot_cmd( w.show() sys.exit(app.exec_()) + +@click.command("trend") +@taurus.cli.common.models +@taurus.cli.common.config_file +@x_axis_mode_option("t") +@taurus.cli.common.demo +@taurus.cli.common.window_name("TaurusTrend") +@taurus.cli.common.use_alternative +@taurus.cli.common.list_alternatives +@max_buffer_option(None ) +@forced_read_option +def trend_cmd( + models, + config_file, + x_axis_mode, + demo, + window_name, + use_alt, + ls_alt, + max_buffer_size, + forced_read_period +): + """Shows a trend for the given models""" + + # list alternatives option + if ls_alt: + _print_alts(EP_GROUP_TREND) + sys.exit(0) + + # use alternative + if use_alt is None: + use_alt = getattr(_ts, "TREND_IMPL", ".*") + + # get the selected alternative + try: + TTrend, epname = _load_class_from_group( + EP_GROUP_TREND, + include=[use_alt] + ) + except: + _print_alts(EP_GROUP_TREND) + sys.exit(1) + + app = TaurusApplication(app_name="taurustrend({})".format(epname)) + w = TTrend() + + # window title option + w.setWindowTitle(window_name) + + # demo option + if demo: + models = list(models) + models.extend(["eval:rand()", "eval:1+rand(2)"]) + + # x axis mode option + try: + w.setXAxisMode(x_axis_mode) + except Exception as e: + warning( + 'Could not set X axis mode to "%s" on %s plot. Reason: "%s"', + x_axis_mode, + epname, + e + ) + sys.exit(1) + + # configuration file option + if config_file is not None: + try: + w.loadConfigFile(config_file) + except Exception as e: + warning( + 'Could not load config file "%s" on %s plot. Reason: "%s"', + config_file, + epname, + e + ) + sys.exit(1) + + # max buffer size option + if max_buffer_size is not None: + try: + w.setMaxDataBufferSize(max_buffer_size) + except Exception as e: + warning( + 'Could not set max buffer size on %s trend. Reason: "%s"', + epname, + e + ) + sys.exit(1) + + # set models + if models: + w.setModel(list(models)) + + # period option + if forced_read_period > 0: + w.setForcedReadingPeriod(forced_read_period) + + w.show() + sys.exit(app.exec_()) + diff --git a/lib/taurus/cli/test/test_alt.py b/lib/taurus/cli/test/test_alt.py index 6684bab7d..706bd54f3 100644 --- a/lib/taurus/cli/test/test_alt.py +++ b/lib/taurus/cli/test/test_alt.py @@ -23,11 +23,12 @@ import pytest from taurus.core.util.test.test_plugin import mock_entry_point -from taurus.cli.plot import _load_class_from_group, plot_cmd +from taurus.cli.alt import _load_class_from_group, plot_cmd, trend_cmd from click.testing import CliRunner runner = CliRunner() + class _MockPlot(object): def __init__(self, *args, **kwargs): self.args = args @@ -37,6 +38,7 @@ def __init__(self, *args, **kwargs): def setModel(self, model): self.model = model + def test_load_good_class_from_group(): """Check get_plot_class with several registered plot alternatives""" lines = ["mock{}={}:_MockPlot".format(i, __name__) for i in range(3)] @@ -66,12 +68,10 @@ def test_load_onlybad_class_from_group(): def test_plot_cmd_options(): response = runner.invoke(plot_cmd, ["--ls-alt"]) assert response.exit_code == 0 - assert "Available alternatives" in response.output + assert "Registered alternatives" in response.output - -def test_plot_cmd_use_alt(): - response = runner.invoke(plot_cmd, ["--use-alt", "_non_existent_alt_"]) - assert "Available alternatives" in response.output + response = runner.invoke(plot_cmd, ["--use-alt", "_non_existent_"]) + assert "Registered alternatives" in response.output assert response.exit_code == 1 response = runner.invoke(plot_cmd, ["-x", "unsupported"]) @@ -94,4 +94,41 @@ def test_plot_cmd_help(): assert "--x-axis-mode" in response.output assert "-x" in response.output assert "--help" in response.output - assert response.exit_code == 0 \ No newline at end of file + assert response.exit_code == 0 + + +def test_trend_cmd_options(): + response = runner.invoke(trend_cmd, ["--ls-alt"]) + assert response.exit_code == 0 + assert "Registered alternatives" in response.output + + response = runner.invoke(trend_cmd, ["--use-alt", "_non_existent_"]) + assert "Registered alternatives" in response.output + assert response.exit_code == 1 + + response = runner.invoke(trend_cmd, ["-x", "unsupported"]) + assert "Invalid value" in response.output + assert response.exit_code == 2 + + response = runner.invoke(trend_cmd, ["--config", "_non_existent_"]) + assert "Invalid value" in response.output + assert "No such file or directory" in response.output + assert response.exit_code == 2 + + +def test_trend_cmd_help(): + response = runner.invoke(trend_cmd, ["--help"]) + assert "--demo" in response.output + assert "--ls-alt" in response.output + assert "--use-alt" in response.output + assert "--config" in response.output + assert "--window-name" in response.output + assert "--x-axis-mode" in response.output + assert "-x" in response.output + assert "--buffer" in response.output + assert "-b" in response.output + assert "--forced-read" in response.output + assert "-r" in response.output + assert "--help" in response.output + assert response.exit_code == 0 + diff --git a/setup.py b/setup.py index 4b87f1b03..8ebdd3c86 100644 --- a/setup.py +++ b/setup.py @@ -112,13 +112,18 @@ def get_release_info(): 'logmon = taurus.core.util.remotelogmonitor:logmon_cmd', 'qlogmon = taurus.qt.qtgui.table.qlogtable:qlogmon_cmd', 'check-deps = taurus.core.taurushelper:check_dependencies_cmd', - 'plot = taurus.cli.plot:plot_cmd', + 'plot = taurus.cli.alt:plot_cmd', + 'trend = taurus.cli.alt:trend_cmd', ] plot_alternatives = [ "qwt5 = taurus.qt.qtgui.qwt5:TaurusPlot", ] +trend_alternatives = [ + "qwt5 = taurus.qt.qtgui.qwt5:TaurusTrend", +] + model_selectors = [ 'Tango = taurus.qt.qtgui.panel.taurusmodelchooser:TangoModelSelectorItem', ] @@ -135,7 +140,8 @@ def get_release_info(): 'taurus.cli.subcommands': taurus_subcommands, 'taurus.qt.qtgui.panel.TaurusModelSelector.items': model_selectors, 'taurus.qt.formatters': formatters, - 'taurus.alt.plots': plot_alternatives, + 'taurus.plot.alts': plot_alternatives, + 'taurus.trend.alts': trend_alternatives, } classifiers = [ From 4345f1a9d6c283c29912a64806128ea83d5cef5e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 25 Jun 2020 19:05:06 +0200 Subject: [PATCH 328/373] Add trend2d subcommand - Adapt extra_guiqwt's TaurusTrend2DDialog to be used as a trend2d alternative and register it - Generalize the x_axis_mode_option --- lib/taurus/cli/alt.py | 78 +++++++++++++++++-- lib/taurus/cli/test/test_alt.py | 31 +++++++- .../qt/qtgui/extra_guiqwt/taurustrend2d.py | 9 ++- setup.py | 6 ++ 4 files changed, 112 insertions(+), 12 deletions(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index 2f3b1bb66..83447ea3a 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -32,6 +32,7 @@ EP_GROUP_PLOT = "taurus.plot.alts" EP_GROUP_TREND = "taurus.trend.alts" +EP_GROUP_TREND2D = "taurus.trend2d.alts" def _print_alts(group): @@ -56,17 +57,25 @@ def _load_class_from_group(group, include=(".*",), exclude=()): raise ImportError("Could not load any class from {}".format(eps)) -def x_axis_mode_option(default="n"): - x_axis_mode_option = click.option( +def x_axis_mode_option(choices=("t", "n")): + hlp = { + "n": "regular axis", + "e": "event number", + "t": "absolute time/date axis", + "d": "delta time axis" + } + o = click.option( "-x", "--x-axis-mode", "x_axis_mode", - type=click.Choice(["t", "n"]), - default=default, + type=click.Choice(choices), + default=choices[0], show_default=True, - help=('X axis mode. Use "t" for a time axis or "n" for a regular one'), + help=("X axis mode: " + + ', '.join([k + " for " + hlp[k] for k in choices]) + ) ) - return x_axis_mode_option + return o def max_buffer_option(default): @@ -95,7 +104,7 @@ def max_buffer_option(default): @click.command("plot") @taurus.cli.common.models @taurus.cli.common.config_file -@x_axis_mode_option("n") +@x_axis_mode_option(["n", "t"]) @taurus.cli.common.demo @taurus.cli.common.window_name("TaurusPlot") @taurus.cli.common.use_alternative @@ -160,7 +169,7 @@ def plot_cmd( @click.command("trend") @taurus.cli.common.models @taurus.cli.common.config_file -@x_axis_mode_option("t") +@x_axis_mode_option(["t", "n"]) @taurus.cli.common.demo @taurus.cli.common.window_name("TaurusTrend") @taurus.cli.common.use_alternative @@ -258,3 +267,56 @@ def trend_cmd( w.show() sys.exit(app.exec_()) + +@click.command('trend2d') +@taurus.cli.common.model +@x_axis_mode_option(["d", "t", "n"]) +@taurus.cli.common.demo +@taurus.cli.common.window_name("TaurusTrend2D") +@taurus.cli.common.use_alternative +@taurus.cli.common.list_alternatives +@max_buffer_option(512) +def trend2d_cmd( + model, + x_axis_mode, + demo, + window_name, + use_alt, + ls_alt, + max_buffer_size +): + + # list alternatives option + if ls_alt: + _print_alts(EP_GROUP_TREND2D) + sys.exit(0) + + # use alternative + if use_alt is None: + use_alt = getattr(_ts, "TREND2D_IMPL", ".*") + + # get the selected alternative + try: + TTrend2D, epname = _load_class_from_group( + EP_GROUP_TREND2D, + include=[use_alt] + ) + except: + _print_alts(EP_GROUP_TREND2D) + sys.exit(1) + + app = TaurusApplication(app_name="Taurus Trend 2D ({})".format(epname)) + w = TTrend2D( + stackMode=x_axis_mode, + wintitle=window_name, + buffersize=max_buffer_size + ) + + if demo: + model = 'eval:x=linspace(0,3,40);t=rand();sin(x+t)' + + if model: + w.setModel(model) + + w.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/lib/taurus/cli/test/test_alt.py b/lib/taurus/cli/test/test_alt.py index 706bd54f3..ee7bbe109 100644 --- a/lib/taurus/cli/test/test_alt.py +++ b/lib/taurus/cli/test/test_alt.py @@ -23,7 +23,8 @@ import pytest from taurus.core.util.test.test_plugin import mock_entry_point -from taurus.cli.alt import _load_class_from_group, plot_cmd, trend_cmd +from taurus.cli.alt import ( + _load_class_from_group, plot_cmd, trend_cmd, trend2d_cmd) from click.testing import CliRunner runner = CliRunner() @@ -132,3 +133,31 @@ def test_trend_cmd_help(): assert "--help" in response.output assert response.exit_code == 0 + +def test_trend2d_cmd_options(): + response = runner.invoke(trend2d_cmd, ["--ls-alt"]) + assert response.exit_code == 0 + assert "Registered alternatives" in response.output + + response = runner.invoke(trend2d_cmd, ["--use-alt", "_non_existent_"]) + assert "Registered alternatives" in response.output + assert response.exit_code == 1 + + response = runner.invoke(trend2d_cmd, ["-x", "unsupported"]) + assert "Invalid value" in response.output + assert response.exit_code == 2 + + +def test_trend2d_cmd_help(): + response = runner.invoke(trend2d_cmd, ["--help"]) + assert "--demo" in response.output + assert "--ls-alt" in response.output + assert "--use-alt" in response.output + assert "--window-name" in response.output + assert "--x-axis-mode" in response.output + assert "-x" in response.output + assert "--buffer" in response.output + assert "-b" in response.output + assert "--help" in response.output + assert response.exit_code == 0 + diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index dff93b85b..4d4caefe2 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -74,6 +74,9 @@ def __init__(self, parent=None, designMode=False, toolbar=True, ImageDialog.__init__(self, parent=parent, toolbar=toolbar, options=defaultOptions, **kwargs) TaurusBaseWidget.__init__(self, "TaurusTrend2DDialog") + # support x_axis_mode values (map them to stackMode values) + stackMode = dict( + t='datetime', d='deltatime', e='event').get(stackMode, stackMode) self.trendItem = None self.buffersize = buffersize self._useArchiving = False @@ -155,9 +158,9 @@ def setStackMode(self, mode): """set the type of stack to be used. This determines how X values are interpreted: - - as timestamps ('datetime') - - as time deltas ('timedelta') - - as event numbers ('event') + - as timestamps ('datetime' or 't') + - as time deltas ('deltatime' or 'd') + - as event numbers ('event' or 'e') :param mode:(one of 'datetime', 'timedelta' or 'event') """ diff --git a/setup.py b/setup.py index 8ebdd3c86..c3f287818 100644 --- a/setup.py +++ b/setup.py @@ -114,6 +114,7 @@ def get_release_info(): 'check-deps = taurus.core.taurushelper:check_dependencies_cmd', 'plot = taurus.cli.alt:plot_cmd', 'trend = taurus.cli.alt:trend_cmd', + 'trend2d = taurus.cli.alt:trend2d_cmd', ] plot_alternatives = [ @@ -124,6 +125,10 @@ def get_release_info(): "qwt5 = taurus.qt.qtgui.qwt5:TaurusTrend", ] +trend2d_alternatives = [ + "guiqwt = taurus.qt.qtgui.extra_guiqwt:TaurusTrend2DDialog", +] + model_selectors = [ 'Tango = taurus.qt.qtgui.panel.taurusmodelchooser:TangoModelSelectorItem', ] @@ -142,6 +147,7 @@ def get_release_info(): 'taurus.qt.formatters': formatters, 'taurus.plot.alts': plot_alternatives, 'taurus.trend.alts': trend_alternatives, + 'taurus.trend2d.alts': trend2d_alternatives, } classifiers = [ From 39da949a53a27b7704fffd5a19a45a150c6f2bbf Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 25 Jun 2020 19:45:52 +0200 Subject: [PATCH 329/373] Add image subcommand - Register extra_guiqwt's TaurusImageDialog as an image alternative - implement taurus.cli.alt image_cmd --- lib/taurus/cli/alt.py | 66 ++++++++++++++++++++++++++++++--- lib/taurus/cli/test/test_alt.py | 32 +++++++++++++++- setup.py | 6 +++ 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index 83447ea3a..1ed8d9fd9 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -33,6 +33,7 @@ EP_GROUP_PLOT = "taurus.plot.alts" EP_GROUP_TREND = "taurus.trend.alts" EP_GROUP_TREND2D = "taurus.trend2d.alts" +EP_GROUP_IMAGE = "taurus.image.alts" def _print_alts(group): @@ -307,16 +308,71 @@ def trend2d_cmd( app = TaurusApplication(app_name="Taurus Trend 2D ({})".format(epname)) w = TTrend2D( - stackMode=x_axis_mode, - wintitle=window_name, - buffersize=max_buffer_size + stackMode=x_axis_mode, wintitle=window_name, buffersize=max_buffer_size ) if demo: - model = 'eval:x=linspace(0,3,40);t=rand();sin(x+t)' + model = "eval:x=linspace(0,3,40);t=rand();sin(x+t)" if model: w.setModel(model) w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) + + +@click.command("image") +@taurus.cli.common.model +@taurus.cli.common.demo +@taurus.cli.common.window_name("TaurusImage") +@taurus.cli.common.use_alternative +@taurus.cli.common.list_alternatives +@click.option( + "-c", + "--color-mode", + "color_mode", + type=click.Choice(["gray", "rgb"]), + default="gray", + show_default=True, + help=("Color mode expected from the attribute"), +) +def image_cmd(model, demo, window_name, color_mode, use_alt, ls_alt): + # list alternatives option + if ls_alt: + _print_alts(EP_GROUP_IMAGE) + sys.exit(0) + + # use alternative + if use_alt is None: + use_alt = getattr(_ts, "IMAGE_IMPL", ".*") + + # get the selected alternative + try: + TImage, epname = _load_class_from_group( + EP_GROUP_IMAGE, include=[use_alt] + ) + except: + _print_alts(EP_GROUP_IMAGE) + sys.exit(1) + + app = TaurusApplication(app_name="Taurus Image ({})".format(epname)) + + rgb_mode = color_mode == "rgb" + + # TODO: is "-c rgb --demo" doing the right thing?? Check it. + if demo: + if color_mode == "rgb": + model = "eval:randint(0,256,(10,20,3))" + else: + model = "eval:rand(256,128)" + + w = TImage(wintitle=window_name) + + w.setRGBmode(rgb_mode) + + # set model + if model: + w.setModel(model) + + w.show() + sys.exit(app.exec_()) diff --git a/lib/taurus/cli/test/test_alt.py b/lib/taurus/cli/test/test_alt.py index ee7bbe109..d1e8a9c14 100644 --- a/lib/taurus/cli/test/test_alt.py +++ b/lib/taurus/cli/test/test_alt.py @@ -24,7 +24,12 @@ import pytest from taurus.core.util.test.test_plugin import mock_entry_point from taurus.cli.alt import ( - _load_class_from_group, plot_cmd, trend_cmd, trend2d_cmd) + _load_class_from_group, + plot_cmd, + trend_cmd, + trend2d_cmd, + image_cmd, +) from click.testing import CliRunner runner = CliRunner() @@ -161,3 +166,28 @@ def test_trend2d_cmd_help(): assert "--help" in response.output assert response.exit_code == 0 + +def test_image_cmd_options(): + response = runner.invoke(image_cmd, ["--ls-alt"]) + assert response.exit_code == 0 + assert "Registered alternatives" in response.output + + response = runner.invoke(image_cmd, ["--use-alt", "_non_existent_"]) + assert "Registered alternatives" in response.output + assert response.exit_code == 1 + + response = runner.invoke(image_cmd, ["-c", "unsupported"]) + assert "Invalid value" in response.output + assert response.exit_code == 2 + + +def test_image_cmd_help(): + response = runner.invoke(image_cmd, ["--help"]) + assert "--demo" in response.output + assert "--ls-alt" in response.output + assert "--use-alt" in response.output + assert "--window-name" in response.output + assert "--color-mode" in response.output + assert "-c" in response.output + assert "--help" in response.output + assert response.exit_code == 0 diff --git a/setup.py b/setup.py index c3f287818..35383d485 100644 --- a/setup.py +++ b/setup.py @@ -115,6 +115,7 @@ def get_release_info(): 'plot = taurus.cli.alt:plot_cmd', 'trend = taurus.cli.alt:trend_cmd', 'trend2d = taurus.cli.alt:trend2d_cmd', + 'image = taurus.cli.alt:image_cmd', ] plot_alternatives = [ @@ -129,6 +130,10 @@ def get_release_info(): "guiqwt = taurus.qt.qtgui.extra_guiqwt:TaurusTrend2DDialog", ] +image_alternatives = [ + "guiqwt = taurus.qt.qtgui.extra_guiqwt:TaurusImageDialog", +] + model_selectors = [ 'Tango = taurus.qt.qtgui.panel.taurusmodelchooser:TangoModelSelectorItem', ] @@ -148,6 +153,7 @@ def get_release_info(): 'taurus.plot.alts': plot_alternatives, 'taurus.trend.alts': trend_alternatives, 'taurus.trend2d.alts': trend2d_alternatives, + 'taurus.image.alts': image_alternatives, } classifiers = [ From f9536f25d9dbfa8b149e5082ce47f23bf3b66aa5 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 25 Jun 2020 19:48:06 +0200 Subject: [PATCH 330/373] Blackify the cli submodule Run `black -tpy27 -l79 taurus/lib/cli` --- lib/taurus/cli/__init__.py | 2 +- lib/taurus/cli/alt.py | 84 ++++++++++++++++----------------- lib/taurus/cli/cli.py | 59 +++++++++++++++-------- lib/taurus/cli/common.py | 68 +++++++++++++------------- lib/taurus/cli/test/__init__.py | 2 +- 5 files changed, 118 insertions(+), 97 deletions(-) diff --git a/lib/taurus/cli/__init__.py b/lib/taurus/cli/__init__.py index d94b9b80c..7d371d74f 100644 --- a/lib/taurus/cli/__init__.py +++ b/lib/taurus/cli/__init__.py @@ -40,4 +40,4 @@ """ -from .cli import main, register_subcommands, taurus_cmd \ No newline at end of file +from .cli import main, register_subcommands, taurus_cmd diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index 1ed8d9fd9..c1dfdce93 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -63,7 +63,7 @@ def x_axis_mode_option(choices=("t", "n")): "n": "regular axis", "e": "event number", "t": "absolute time/date axis", - "d": "delta time axis" + "d": "delta time axis", } o = click.option( "-x", @@ -72,33 +72,39 @@ def x_axis_mode_option(choices=("t", "n")): type=click.Choice(choices), default=choices[0], show_default=True, - help=("X axis mode: " - + ', '.join([k + " for " + hlp[k] for k in choices]) - ) + help=( + "X axis mode: " + + ", ".join([k + " for " + hlp[k] for k in choices]) + ), ) return o def max_buffer_option(default): o = click.option( - '-b', '--buffer', 'max_buffer_size', + "-b", + "--buffer", + "max_buffer_size", type=int, default=default, show_default=True, - help=("Maximum number of values to be stacked " - + "(when reached, the oldest values will be " - + "discarded)"), - ) + help=( + "Maximum number of values to be stacked " + + "(when reached, the oldest values will be " + + "discarded)" + ), + ) return o + forced_read_option = click.option( - '-r', - '--forced-read', - 'forced_read_period', + "-r", + "--forced-read", + "forced_read_period", type=int, default=-1, metavar="MILLISECONDS", - help="force re-reading of the attributes every MILLISECONDS ms" + help="force re-reading of the attributes every MILLISECONDS ms", ) @@ -111,7 +117,7 @@ def max_buffer_option(default): @taurus.cli.common.use_alternative @taurus.cli.common.list_alternatives def plot_cmd( - models, config_file, x_axis_mode, demo, window_name, use_alt, ls_alt + models, config_file, x_axis_mode, demo, window_name, use_alt, ls_alt ): """Shows a plot for the given models""" @@ -123,7 +129,9 @@ def plot_cmd( use_alt = getattr(_ts, "PLOT_IMPL", ".*") try: - TPlot, epname = _load_class_from_group(EP_GROUP_PLOT, include=[use_alt]) + TPlot, epname = _load_class_from_group( + EP_GROUP_PLOT, include=[use_alt] + ) except: _print_alts(EP_GROUP_PLOT) sys.exit(1) @@ -144,7 +152,7 @@ def plot_cmd( 'Could not set X axis mode to "%s" on %s plot. Reason: "%s"', x_axis_mode, epname, - e + e, ) sys.exit(1) @@ -156,7 +164,7 @@ def plot_cmd( 'Could not load config file "%s" on %s plot. Reason: "%s"', config_file, epname, - e + e, ) sys.exit(1) @@ -175,18 +183,18 @@ def plot_cmd( @taurus.cli.common.window_name("TaurusTrend") @taurus.cli.common.use_alternative @taurus.cli.common.list_alternatives -@max_buffer_option(None ) +@max_buffer_option(None) @forced_read_option def trend_cmd( - models, - config_file, - x_axis_mode, - demo, - window_name, - use_alt, - ls_alt, - max_buffer_size, - forced_read_period + models, + config_file, + x_axis_mode, + demo, + window_name, + use_alt, + ls_alt, + max_buffer_size, + forced_read_period, ): """Shows a trend for the given models""" @@ -202,8 +210,7 @@ def trend_cmd( # get the selected alternative try: TTrend, epname = _load_class_from_group( - EP_GROUP_TREND, - include=[use_alt] + EP_GROUP_TREND, include=[use_alt] ) except: _print_alts(EP_GROUP_TREND) @@ -228,7 +235,7 @@ def trend_cmd( 'Could not set X axis mode to "%s" on %s plot. Reason: "%s"', x_axis_mode, epname, - e + e, ) sys.exit(1) @@ -241,7 +248,7 @@ def trend_cmd( 'Could not load config file "%s" on %s plot. Reason: "%s"', config_file, epname, - e + e, ) sys.exit(1) @@ -253,7 +260,7 @@ def trend_cmd( warning( 'Could not set max buffer size on %s trend. Reason: "%s"', epname, - e + e, ) sys.exit(1) @@ -269,7 +276,7 @@ def trend_cmd( sys.exit(app.exec_()) -@click.command('trend2d') +@click.command("trend2d") @taurus.cli.common.model @x_axis_mode_option(["d", "t", "n"]) @taurus.cli.common.demo @@ -278,13 +285,7 @@ def trend_cmd( @taurus.cli.common.list_alternatives @max_buffer_option(512) def trend2d_cmd( - model, - x_axis_mode, - demo, - window_name, - use_alt, - ls_alt, - max_buffer_size + model, x_axis_mode, demo, window_name, use_alt, ls_alt, max_buffer_size ): # list alternatives option @@ -299,8 +300,7 @@ def trend2d_cmd( # get the selected alternative try: TTrend2D, epname = _load_class_from_group( - EP_GROUP_TREND2D, - include=[use_alt] + EP_GROUP_TREND2D, include=[use_alt] ) except: _print_alts(EP_GROUP_TREND2D) diff --git a/lib/taurus/cli/cli.py b/lib/taurus/cli/cli.py index c4172bed4..c7279258e 100644 --- a/lib/taurus/cli/cli.py +++ b/lib/taurus/cli/cli.py @@ -26,20 +26,30 @@ import taurus import taurus.cli.common -from .import common +from . import common -@click.group('taurus') +@click.group("taurus") @common.log_level @common.poll_period @common.serial_mode @common.default_formatter -@click.option("--rconsole", "rconsole_port", type=click.INT, - metavar="PORT", default=None, - help="Enable remote debugging with rfoo on the given PORT") +@click.option( + "--rconsole", + "rconsole_port", + type=click.INT, + metavar="PORT", + default=None, + help="Enable remote debugging with rfoo on the given PORT", +) @click.version_option(version=taurus.Release.version) -def taurus_cmd(log_level, polling_period, serialization_mode, default_formatter, - rconsole_port): +def taurus_cmd( + log_level, + polling_period, + serialization_mode, + default_formatter, + rconsole_port, +): """The main taurus command""" # set log level @@ -52,6 +62,7 @@ def taurus_cmd(log_level, polling_period, serialization_mode, default_formatter, # set serialization mode if serialization_mode is not None: from taurus.core.taurusbasetypes import TaurusSerializationMode + m = getattr(TaurusSerializationMode, serialization_mode) taurus.Manager().setSerializationMode(m) @@ -59,25 +70,30 @@ def taurus_cmd(log_level, polling_period, serialization_mode, default_formatter, if rconsole_port is not None: try: import rfoo.utils.rconsole + rfoo.utils.rconsole.spawn_server(port=rconsole_port) - taurus.info(("rconsole started. " - + "You can connect to it by typing: rconsole -p %d"), - rconsole_port - ) + taurus.info( + ( + "rconsole started. " + + "You can connect to it by typing: rconsole -p %d" + ), + rconsole_port, + ) except Exception as e: taurus.warning("Cannot spawn debugger. Reason: %s", e) # set the default formatter if default_formatter is not None: from taurus import tauruscustomsettings - setattr(tauruscustomsettings, 'DEFAULT_FORMATTER', default_formatter) + + setattr(tauruscustomsettings, "DEFAULT_FORMATTER", default_formatter) def register_subcommands(): """Discover and add subcommands to taurus_cmd""" # Add subcommands from the taurus_subcommands entry point - for ep in pkg_resources.iter_entry_points('taurus.cli.subcommands'): + for ep in pkg_resources.iter_entry_points("taurus.cli.subcommands"): try: subcommand = ep.load() taurus_cmd.add_command(subcommand) @@ -88,14 +104,17 @@ def register_subcommands(): # This special case can be removed when taurus.qt.qtgui.qwt5 # is moved to a separate plugin, since the entry point will # be registered only if the plugin is installed - if ep.name == 'qwt5': + if ep.name == "qwt5": taurus.info( 'Cannot add "%s" subcommand to taurus. Reason: %r', - ep.name, e) + ep.name, + e, + ) continue # ----------------------------------------------------------- - taurus.warning('Cannot add "%s" subcommand to taurus. Reason: %r', - ep.name, e) + taurus.warning( + 'Cannot add "%s" subcommand to taurus. Reason: %r', ep.name, e + ) def main(): @@ -106,12 +125,12 @@ def main(): # it will be restored to the desired one first thing in taurus_cmd() taurus.setLogLevel(taurus.Warning) - #register the subcommands + # register the subcommands register_subcommands() # launch the taurus command taurus_cmd() -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/lib/taurus/cli/common.py b/lib/taurus/cli/common.py index 71a986919..60919696a 100644 --- a/lib/taurus/cli/common.py +++ b/lib/taurus/cli/common.py @@ -85,7 +85,7 @@ def cmd2(model, serial_mode, poll_period, default_formatter): import click -__levels = ['Critical', 'Error', 'Warning', 'Info', 'Debug', 'Trace'] +__levels = ["Critical", "Error", "Warning", "Info", "Debug", "Trace"] try: __log_choice = click.Choice(__levels, case_sensitive=False) except TypeError: # click v< 7 does not allow case_sensitive option @@ -93,60 +93,62 @@ def cmd2(model, serial_mode, poll_period, default_formatter): log_level = click.option( - '--log-level', 'log_level', - type=__log_choice, default='Info', show_default=True, - help='Show only logs with priority LEVEL or above', + "--log-level", + "log_level", + type=__log_choice, + default="Info", + show_default=True, + help="Show only logs with priority LEVEL or above", ) config_file = click.option( - '--config', 'config_file', - type=click.File('rb'), - help='Configuration file for initialization', + "--config", + "config_file", + type=click.File("rb"), + help="Configuration file for initialization", ) + def window_name(default): window_name = click.option( - '--window-name', 'window_name', + "--window-name", + "window_name", default=default, - help='Name of the window', + help="Name of the window", ) return window_name -models = click.argument( - 'models', nargs=-1, -) -demo = click.option( - "--demo", - is_flag=True, - help="Show a demo of the widget", -) +models = click.argument("models", nargs=-1,) -model = click.argument( - 'model', - nargs=1, - required=False, -) +demo = click.option("--demo", is_flag=True, help="Show a demo of the widget",) + +model = click.argument("model", nargs=1, required=False,) poll_period = click.option( - "--polling-period", "polling_period", + "--polling-period", + "polling_period", type=click.INT, metavar="MILLISEC", default=None, - help='Change the Default Taurus polling period', + help="Change the Default Taurus polling period", ) serial_mode = click.option( - "--serialization-mode", "serialization_mode", - type=click.Choice(['Serial', 'Concurrent', 'TangoSerial']), + "--serialization-mode", + "serialization_mode", + type=click.Choice(["Serial", "Concurrent", "TangoSerial"]), default=None, show_default=True, - help=("Set the default Taurus serialization mode for those " - + "models that do not explicitly define it)"), + help=( + "Set the default Taurus serialization mode for those " + + "models that do not explicitly define it)" + ), ) default_formatter = click.option( - "--default-formatter", "default_formatter", + "--default-formatter", + "default_formatter", type=click.STRING, metavar="FORMATTER", default=None, @@ -154,15 +156,15 @@ def window_name(default): ) list_alternatives = click.option( - '--ls-alt', + "--ls-alt", is_flag=True, help="List the available alternative implementations", ) use_alternative = click.option( - '--use-alt', + "--use-alt", metavar="ALT", type=click.STRING, default=None, - help="Use ALT alternative implementation" -) \ No newline at end of file + help="Use ALT alternative implementation", +) diff --git a/lib/taurus/cli/test/__init__.py b/lib/taurus/cli/test/__init__.py index 37a9eb713..294cb5636 100644 --- a/lib/taurus/cli/test/__init__.py +++ b/lib/taurus/cli/test/__init__.py @@ -21,4 +21,4 @@ ## ############################################################################# -"""Tests for taurus.cli""" \ No newline at end of file +"""Tests for taurus.cli""" From e6f48461630f56173c055ae3edcb0c6180b0085e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 25 Jun 2020 20:24:22 +0200 Subject: [PATCH 331/373] Redirect guiqwt and its subcommands Replace by dummies pointing to the new image and trend2d subcommands --- lib/taurus/qt/qtgui/extra_guiqwt/cli.py | 23 +++++---- lib/taurus/qt/qtgui/extra_guiqwt/plot.py | 46 ------------------ .../qt/qtgui/extra_guiqwt/taurustrend2d.py | 48 ------------------- 3 files changed, 15 insertions(+), 102 deletions(-) diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/cli.py b/lib/taurus/qt/qtgui/extra_guiqwt/cli.py index 1a00f8dfc..e3ce51299 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/cli.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/cli.py @@ -22,20 +22,27 @@ ############################################################################# import click +import sys +@click.group('guiqwt') +def guiqwt(): + """[DEPRECATED] use "taurus image" or "taurus trend2d" instead""" + print('"taurus guiqwt" subcommand is deprecated. ' + + 'Use "taurus image" or "taurus trend2d" instead\n') + sys.exit(1) -from .plot import image_cmd -from .taurustrend2d import trend2d_cmd +@guiqwt.command() +def image(): + """[DEPRECATED] use "taurus image" instead""" + sys.exit(1) -@click.group('guiqwt') -def guiqwt(): - """guiqwt related commands (image, trend2d)""" - pass +@guiqwt.command() +def trend2d(): + """[DEPRECATED] use "taurus trend2d" instead""" + sys.exit(1) -guiqwt.add_command(image_cmd) -guiqwt.add_command(trend2d_cmd) if __name__ == '__main__': guiqwt() diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py index 739a11a58..7b749c994 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/plot.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/plot.py @@ -29,7 +29,6 @@ from builtins import next from builtins import str -import click import copy from future.utils import string_types @@ -628,48 +627,3 @@ def taurusTrendDlgMain(): w.show() sys.exit(app.exec_()) - -@click.command('image') -@taurus.cli.common.model -@taurus.cli.common.demo -@taurus.cli.common.window_name('TaurusPlot (qwt5)') -@click.option( - '-c', '--color-mode', 'color_mode', - type=click.Choice(['gray', 'rgb']), - default='gray', - show_default=True, - help=('Color mode expected from the attribute'), -) -def image_cmd(model, demo, window_name, color_mode): - from taurus.qt.qtgui.application import TaurusApplication - import sys - - app = TaurusApplication(cmd_line_parser=None, - app_name="Taurus Image Dialog") - - rgb_mode = (color_mode == 'rgb') - - # TODO: is "-c rgb --demo" doing the right thing?? Check it. - if demo: - if color_mode == 'rgb': - model = 'eval:randint(0,256,(10,20,3))' - else: - model = 'eval:rand(256,128)' - - w = TaurusImageDialog(wintitle=window_name) - - w.setRGBmode(rgb_mode) - - # set model - if model: - w.setModel(model) - - w.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - image_cmd() - # taurusCurveDlgMain() - # taurusTrendDlgMain() - # taurusImageDlgMain() diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 4d4caefe2..3efd3fa93 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -28,8 +28,6 @@ """ __all__ = ["TaurusTrend2DDialog"] -import click - from guiqwt.plot import ImageDialog from taurus.external.qt import Qt import taurus.core @@ -310,49 +308,3 @@ def setModifiableByUser(self, modifiable): TaurusBaseWidget.isModifiableByUser, setModifiableByUser, TaurusBaseWidget.resetModifiableByUser) - - -@click.command('trend2d') -@taurus.cli.common.model -@taurus.cli.common.demo -@taurus.cli.common.window_name('TaurusTrend2D') -@click.option( - '-b', '--buffer', 'max_buffer_size', - type=int, - default=512, - show_default=True, - help=("Maximum number of values to be stacked " - + "(when reached, the oldest values will be " - + "discarded)"), -) -@click.option( - "-x", "--x-axis-mode", "x_axis_mode", - type=click.Choice(['t', 'd', 'e']), - default='d', - show_default=True, - help=("Interpret X values as timestamps (t), time deltas (d) " - + " or event numbers (e). ") -) -def trend2d_cmd(model, demo, window_name, max_buffer_size, - x_axis_mode): - from taurus.qt.qtgui.application import TaurusApplication - import sys - - if demo: - model = 'eval:x=linspace(0,3,40);t=rand();sin(x+t)' - - app = TaurusApplication(cmd_line_parser=None, app_name="Taurus Trend 2D") - - stackMode = dict(t='datetime', d='deltatime', e='event')[x_axis_mode] - - w = TaurusTrend2DDialog(stackMode=stackMode, wintitle=window_name, - buffersize=max_buffer_size) - if model: - w.setModel(model) - - w.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - trend2d_cmd() From 2e3e1658bbbb571111ab912f8d1ec75742aba4e6 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 25 Jun 2020 20:40:55 +0200 Subject: [PATCH 332/373] Deprecate qwt5 subcommand Document qwt5 plot and qwt5 trend as deprecated, but do not remove them since they are not completely replaced by taurus plot and taurus trend --- lib/taurus/qt/qtgui/qwt5/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/taurus/qt/qtgui/qwt5/cli.py b/lib/taurus/qt/qtgui/qwt5/cli.py index 2039ef04d..5e1aa2596 100644 --- a/lib/taurus/qt/qtgui/qwt5/cli.py +++ b/lib/taurus/qt/qtgui/qwt5/cli.py @@ -50,7 +50,7 @@ def buffer(default): @click.group('qwt5') def qwt5(): - """Qwt5 related commands""" + """[DEPRECATED] use "taurus plot" or "taurus trend" instead""" pass @@ -61,7 +61,7 @@ def qwt5(): @taurus.cli.common.demo @taurus.cli.common.window_name("TaurusPlot (qwt5)") def plot_cmd(models, config_file, x_axis_mode, demo, window_name): - """Shows a plot for the given models""" + """[DEPRECATED] use "taurus plot" instead""" from .taurusplot import plot_main return plot_main(models=models, config_file=config_file, @@ -88,7 +88,7 @@ def plot_cmd(models, config_file, x_axis_mode, demo, window_name): help="force re-reading of the attributes every MILLISECONDS ms") def trend_cmd(models, x_axis_mode, config_file, demo, window_name, max_buffer_size, use_archiving, forced_read_period): - """Shows a trend for the given models""" + """[DEPRECATED] use "taurus trend" instead""" from .taurustrend import trend_main return trend_main(models=models, config_file=config_file, From 8c3c6de267e5e0286813f95ae108659295323ea4 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 29 Jun 2020 20:14:08 +0200 Subject: [PATCH 333/373] Add *_ALT options in tauruscustomsettings Add PLOT_ALT, TREND_ALT, TREND_ALT, IMAGE_ALT and to tauruscustomsettings.py (and use them in taurus.cli.alt) --- lib/taurus/cli/alt.py | 8 ++++---- lib/taurus/tauruscustomsettings.py | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index c1dfdce93..94f5bc3a2 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -126,7 +126,7 @@ def plot_cmd( sys.exit(0) if use_alt is None: - use_alt = getattr(_ts, "PLOT_IMPL", ".*") + use_alt = getattr(_ts, "PLOT_ALT", ".*") try: TPlot, epname = _load_class_from_group( @@ -205,7 +205,7 @@ def trend_cmd( # use alternative if use_alt is None: - use_alt = getattr(_ts, "TREND_IMPL", ".*") + use_alt = getattr(_ts, "TREND_ALT", ".*") # get the selected alternative try: @@ -295,7 +295,7 @@ def trend2d_cmd( # use alternative if use_alt is None: - use_alt = getattr(_ts, "TREND2D_IMPL", ".*") + use_alt = getattr(_ts, "TREND2D_ALT", ".*") # get the selected alternative try: @@ -344,7 +344,7 @@ def image_cmd(model, demo, window_name, color_mode, use_alt, ls_alt): # use alternative if use_alt is None: - use_alt = getattr(_ts, "IMAGE_IMPL", ".*") + use_alt = getattr(_ts, "IMAGE_ALT", ".*") # get the selected alternative try: diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index 28b17991d..c2fcbe252 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -30,6 +30,23 @@ aspects of Taurus. """ +#: Widget alternatives. Some widgets may be have alternative implementations +#: (e.g. the `TaurusPlot` is implemented both by the qwt5 submodule and by the +#: taurus_pyqtgraph plugin). The different implementations are registered in +#: entry point groups (`taurus.plot.alt`, `taurus.trend.alt`, ...) and they +#: are tried in alphabetical order of their registered entry point names +#: (the first one that works is used). You can restrict the set of available +#: implementation alternatives to be tried (or even just select a given +#: alternative) by setting the corresponding *_ALT variable with a name +#: regexp pattern that must be matched by the entry point name in order to be +#: tried. For example, to force the `taurus_pyqtgraph` implementation for the +#: plots, set `PLOT_ALT = "tpg"`. +#: Leaving the variable undefined is equivalent to setting it to `".*"` +PLOT_ALT = ".*" +TREND_ALT = ".*" +TREND2D_ALT = ".*" +IMAGE_ALT = ".*" + #: Default include and exclude patterns for TaurusForm item factories #: See `TaurusForm.setItemFactories` docs. By default, all available #: factories are enabled (and tried alphabetically) @@ -44,7 +61,6 @@ #: False enables a backwards-compatibility mode for pre-sep3 model names STRICT_MODEL_NAMES = False - #: Lightweight imports: #: True enables delayed imports (may break older code). #: False (or commented out) for backwards compatibility From 4c7fe3f093334d4835f828c398b8a79bb55ef1c4 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 29 Jun 2020 23:16:36 +0200 Subject: [PATCH 334/373] Rename private entry point group name Rename `taurus.qt.qtgui.panel.TaurusModelSelector.items` to `taurus.model_selector.items`. Do not bother about providing backwards compatibility because this has been until now a non-advertised entry-point used only by taurus_tangoarchiving (which has already been adapted) --- lib/taurus/qt/qtgui/panel/taurusmodelchooser.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py b/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py index 5b82a14e4..f81941e36 100644 --- a/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py +++ b/lib/taurus/qt/qtgui/panel/taurusmodelchooser.py @@ -64,8 +64,8 @@ def __init__(self, parent=None): # Note: this is an experimental feature # It may be removed or changed in future releases # Discover the taurus.modelselector plugins - ep_name = 'taurus.qt.qtgui.panel.TaurusModelSelector.items' - for ep in pkg_resources.iter_entry_points(ep_name): + ep_group_name = 'taurus.model_selector.items' + for ep in pkg_resources.iter_entry_points(ep_group_name): try: ms_class = ep.load() ms_item = ms_class(parent=self) diff --git a/setup.py b/setup.py index 35383d485..cc3ca49dc 100644 --- a/setup.py +++ b/setup.py @@ -148,7 +148,7 @@ def get_release_info(): entry_points = { 'console_scripts': console_scripts, 'taurus.cli.subcommands': taurus_subcommands, - 'taurus.qt.qtgui.panel.TaurusModelSelector.items': model_selectors, + 'taurus.model_selector.items': model_selectors, 'taurus.qt.formatters': formatters, 'taurus.plot.alts': plot_alternatives, 'taurus.trend.alts': trend_alternatives, From 4b04590403009193ef2e1ec31eb01814817f86b3 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 29 Jun 2020 23:19:43 +0200 Subject: [PATCH 335/373] Add stub of docs for plugins.rst Add the seed for the plugins documentation. This should be extended when advancing in the implementation of TEP13. Document currently-used entry point group names. --- doc/source/devel/index.rst | 1 + doc/source/devel/plugins.rst | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 doc/source/devel/plugins.rst diff --git a/doc/source/devel/index.rst b/doc/source/devel/index.rst index 009fed192..521ff6f2e 100644 --- a/doc/source/devel/index.rst +++ b/doc/source/devel/index.rst @@ -14,6 +14,7 @@ Developer's Guide Icon guide Core tutorial Plotting guide + Plugins Taurus Custom Settings Examples API diff --git a/doc/source/devel/plugins.rst b/doc/source/devel/plugins.rst new file mode 100644 index 000000000..2eaac48c9 --- /dev/null +++ b/doc/source/devel/plugins.rst @@ -0,0 +1,68 @@ +.. _plugins: + +============== +Taurus plugins +============== + +.. note:: The taurus plugins API is still experimental. Current entry point + group names, and expected entry point objects API may be modified or + removed in later versions. + +Taurus can be extended by third party modules in various ways. For example, a +mechanism exists since before v4.0.0 to use add new schemes to the +`taurus.core` module. But these extension mechanisms grew organically and +independently of each other, resulting in several non-consistent plugin APIs. +This has been long-standing pending issue as can be seen by date of the draft +of the TEP13_ + +Since taurus v4.3 we started introducing some experimental `pkg_resources`- +based entry points, in an effort to test their viability for being used as a +more generic and standard mechanism for plugins. Here we describe such +APIs which, for now and until TEP13_ is approved, should be still considered +experimental. + + +Entry point-based plugins: +-------------------------- + +The following table lists the entry-point groups used by taurus, with links to +the appropriate docs. + ++---------------------------------+----------------------------------------+-------+ +| Entry point group | Description | Notes | ++=================================+========================================+=======+ +| taurus.qt.qtgui | submodules of taurus.qt.qtgui | 1 | ++---------------------------------+----------------------------------------+-------+ +| taurus.cli.subcommands | click subcommands for the `taurus` | 2 | +| | command. | | ++---------------------------------+----------------------------------------+-------+ +| taurus.model_selector.items | items for `TaurusModelSelector` | 3 | ++---------------------------------+----------------------------------------+-------+ +| taurus.qt.formatters | formatter objects for taurus widgets | 4 | ++---------------------------------+----------------------------------------+-------+ +| - taurus.plot.alts | Alternative implementations for | 2 | +| - taurus.trend.alts | various basic widgets. To be used in | | +| - taurus.trend2d.alts | in, e.g. the `taurus plot` subcommand, | | +| - taurus.image.alts | etc. | | ++---------------------------------+----------------------------------------+-------+ +| taurus.form.item_factories | Factories for custom "taurus value" | 5 | +| | widgets to be used in taurus forms | | ++---------------------------------+----------------------------------------+-------+ +| taurus.core.schemes | Schemes in taurus.core | 6 | ++---------------------------------+----------------------------------------+-------+ + + +Notes: + +1. For internal use only (used backwards compatibility when "outsourcing" +submodules). +2. See :mod:`taurus.cli` +3. See :class:`taurus.qt.qtgui.panel.TaurusModelSelector` +4. See :meth:`taurus.qt.qtgui.base.TaurusBaseComponent.displayValue` +5. See :meth:`taurus.qt.qtgui.panel.TaurusForm.setItemFactories` + + + + + +.. _TEP13: http://www.taurus-scada.org/tep?TEP13.md From f891fc0421a834bd3c5adab29727ec77369e88c6 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 29 Jun 2020 23:48:48 +0200 Subject: [PATCH 336/373] Document taurus.cli --- lib/taurus/cli/__init__.py | 8 +++++--- lib/taurus/cli/alt.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/taurus/cli/__init__.py b/lib/taurus/cli/__init__.py index 7d371d74f..174bb88cc 100644 --- a/lib/taurus/cli/__init__.py +++ b/lib/taurus/cli/__init__.py @@ -27,10 +27,10 @@ It is based on the click module to provide CLI utilities. It defines a `taurus` command which can accept subcommands defined in -other taurus submodules or even in plugins. +other taurus submodules or even in 3rd party plugins. -To define a subcommand for taurus, you need to register it using the -`taurus_subcommands` entry point via setuptools. +To define a subcommand for taurus, you need to register it in the +`taurus.cli.subcommands` entry point group via setuptools. .. todo:: add click link , add code snippet for plugins, etc. @@ -38,6 +38,8 @@ that can be used by taurus plugins. For list of available options, see :mod:`taurus.cli.common` +.. seealso:: :mod:`taurus.cli.alt` + """ from .cli import main, register_subcommands, taurus_cmd diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index 94f5bc3a2..f438f8c74 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -21,6 +21,35 @@ ## ############################################################################# +""" +This module provides several taurus CLI subcommands (plot, trend, image,...) +that show a widget which may have more than one alternative implementation. +The available registered implementations for each command can be listed with +the `--ls-alt` option. Specific implementations can be selected with the +`--use-alt` option. + +These commands also honour the default implementation selection configured in +:mod:`taurus.tauruscustomsettings` via the `*_ALT` variables + +The alternative implementations can be registered using the following +entry-point group names: + +- `taurus.plot.alt`: the loaded object is expected to be a `TaurusPlot` + implementation + +- `taurus.trend.alt`: the loaded object is expected to be a `TaurusTrend` + implementation + +- `taurus.trend2d.alt`: the loaded object is expected to be a + `TaurusTrend2dDialog` implementation + +- `taurus.image.alt`: the loaded object is expected to be a `TaurusImageDialog` + implementation + + +.. todo:: explicit the expected API from the loaded alternative implementations +""" + import sys import click import taurus.cli.common From cf0ca0e0877f54201dae7adf3745cfcdf7f6ef9c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 30 Jun 2020 16:20:20 +0200 Subject: [PATCH 337/373] Document expected API for alt widgets --- lib/taurus/cli/alt.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index f438f8c74..55aa94230 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -47,7 +47,41 @@ implementation -.. todo:: explicit the expected API from the loaded alternative implementations +Expected API for alternative implementations +-------------------------------------------- + +When registering a widget class to be used for these commands, the classes need +to provide the following minimum API: + +- For **all** the classes: + - must be a Qt.QWidget + - must implement `setModel()` and `getModel()` equivalent to those in + :class:`taurus.qt.qtgui.base.TaurusBaseComponent` + +- For TaurusPlot: + - should implement `setXAxisMode(mode)` where mode is one of `"n"` or `"t"` + - should implement `loadConfigFile(name)` equivalent to + :meth:`taurus.qt.qtcore.configuration.BaseConfigurableClass.loadConfigFile` + +- For TaurusTrend: + - should implement `setXAxisMode(mode)` where mode is one of `"n"` or `"t"` + - should implement `loadConfigFile(name)` equivalent to + :meth:`taurus.qt.qtcore.configuration.BaseConfigurableClass.loadConfigFile` + - should implement `setMaxDataBufferSize(n)` where `n` is an integer + - should implement `setForcedReadingPeriod(period)` where `period` is an + integer + +- For TaurusTrend2D: + - should accept the following keyword arguments in its constructor: + - stackMode=x_axis_mode where mode is one of `"d"` or `"n"` or `"t"` + - wintitle=window_name, + - buffersize=max_buffer_size + +- For TaurusImage: + - should accept the following keyword arguments in its constructor: + - wintitle=window_name, + - should implement `setRGBmode(mode)` where mode is one of `"gray"`, `"rgb"` + """ import sys From aed19204ddb1dee147b6ec00cfc51bd2259d589e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 30 Jun 2020 16:33:01 +0200 Subject: [PATCH 338/373] Update CHANGELOG.md --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e8f20a7..6ab399b74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,18 @@ develop branch) won't be reflected in this file. ## [Unreleased] ### Added +- `plot`, `trend`, `trend2d`, `image` first-level taurus subcommands (#1120) +- `"taurus.*.alt"` entry-point groups for registering alternative + implementations of `plot`, `trend`, `trend2d`, `image` (#1120) - check-deps subcommand (#988) - `taurus.cli.register_subcommands()` and `taurus.cli.taurus_cmd()` (#991) - Support for spyder v4 in `taurus.qt.qtgui.editor` (#1038) - Entry-point ("taurus.qt.formatters") for registering formatters via plugins (#1039) - New `worker_cls` argument for `taurus.core.util.ThreadPool` costructor (#1081) - `taurus.core.util.lazymodule` for delayed entry-point loading of modules (#1090) -- `"taurus.form.item_factories"` entry-point (#1108) +- `"taurus.form.item_factories"` entry-point group (#1108) +- Documentation about (experimental) entry-point based plugin support (#1120) + ### Removed ### Changed @@ -29,6 +34,8 @@ develop branch) won't be reflected in this file. - The control over which custom widgets are to be used in a TaurusForm is now done by registering factories to `"taurus.form.item_factories"` entry-point (#1108) - Qt unit tests reimplemented using pytest-qt (#1114) +- `"'taurus.qt.qtgui.panel.TaurusModelSelector.items"` entry-point group + renamed to `"taurus.model_selector.items"` ### Deprecated - `TaurusBaseWidget.showFormatterDlg()` (#1039) @@ -36,6 +43,7 @@ develop branch) won't be reflected in this file. - `tauruscustomsettings.T_FORM_CUSTOM_WIDGET_MAP` (#1108) - `BaseWidgetTestCase` and `GenericWidgetTestCase` (#1114) - `TimeOut` Device Server (#1114) +- `qwt5` and `guiqwt` CLI subcommands (#1120) ### Fixed - Several issues in TaurusWheelEdit (#1010, #1021) From e00f1af7bf3c4a1a6f4c2c911fb6b1d8c9b84074 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 30 Jun 2020 16:37:40 +0200 Subject: [PATCH 339/373] m, Fix doc format issue --- lib/taurus/cli/alt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index 55aa94230..a65b02063 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -73,13 +73,16 @@ - For TaurusTrend2D: - should accept the following keyword arguments in its constructor: + - stackMode=x_axis_mode where mode is one of `"d"` or `"n"` or `"t"` - wintitle=window_name, - buffersize=max_buffer_size - For TaurusImage: - should accept the following keyword arguments in its constructor: + - wintitle=window_name, + - should implement `setRGBmode(mode)` where mode is one of `"gray"`, `"rgb"` """ From 7820b635e282e9732754310e2ca6d69815ed8d06 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 30 Jun 2020 18:07:51 +0200 Subject: [PATCH 340/373] m, Fix doc format issue --- lib/taurus/cli/alt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index a65b02063..41cbe544d 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -56,20 +56,20 @@ - For **all** the classes: - must be a Qt.QWidget - must implement `setModel()` and `getModel()` equivalent to those in - :class:`taurus.qt.qtgui.base.TaurusBaseComponent` + :class:`taurus.qt.qtgui.base.TaurusBaseComponent` - For TaurusPlot: - should implement `setXAxisMode(mode)` where mode is one of `"n"` or `"t"` - should implement `loadConfigFile(name)` equivalent to - :meth:`taurus.qt.qtcore.configuration.BaseConfigurableClass.loadConfigFile` + :meth:`taurus.qt.qtcore.configuration.BaseConfigurableClass.loadConfigFile` - For TaurusTrend: - should implement `setXAxisMode(mode)` where mode is one of `"n"` or `"t"` - should implement `loadConfigFile(name)` equivalent to - :meth:`taurus.qt.qtcore.configuration.BaseConfigurableClass.loadConfigFile` + :meth:`taurus.qt.qtcore.configuration.BaseConfigurableClass.loadConfigFile` - should implement `setMaxDataBufferSize(n)` where `n` is an integer - should implement `setForcedReadingPeriod(period)` where `period` is an - integer + integer - For TaurusTrend2D: - should accept the following keyword arguments in its constructor: @@ -113,7 +113,7 @@ def _load_class_from_group(group, include=(".*",), exclude=()): The selection is done among the classes registered in the `group` entry-point, prioritized according to the given `include` and `exclude` patterns - (see :function:`taurus.core.util.plugin.selectEntryPoints`) + (see :func:`taurus.core.util.plugin.selectEntryPoints`) """ eps = selectEntryPoints(group, include=include, exclude=exclude) for ep in eps: From 99c1fd19d678cf20be2be81299a4986cf9ec8dac Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 1 Jul 2020 19:51:31 +0200 Subject: [PATCH 341/373] Give priority to cpascual channel in tox.ini Use cpascual conda channel as the first option in order to test with the unreleased pytango packages --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 833fef68e..0d73fdf96 100644 --- a/tox.ini +++ b/tox.ini @@ -10,9 +10,9 @@ envlist = setenv= QT_QPA_PLATFORM = offscreen conda_channels= + cpascual conda-forge tango-controls - cpascual conda_deps= qt4: pyqt=4 qt5: pyqt=5 From 04692b5fb8e77fb3b124ece8a0a4d999354e233c Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 2 Jul 2020 12:12:49 +0200 Subject: [PATCH 342/373] Refactor selection of Qt binding (prefer pyqt5) Currently, if taurus.external.qt is imported and no Qt binding is already imported, Taurus falls back to selecting a binding based on the `QT_API` env var or, in its absence, the value of `tauruscustomsettings.DEFAULT_QT_API`. But these variables are used only as hints, and if their selection cannot be imported, the next binding in the internal list are tried. Change this behaviour by introducing the possibility of setting these variables to an empty string to indicate that Taurus should try available bindings. If the value is not empty, then a failure to import the indicated binding is considered an error and a exception is thrown (instead of trying other bindings). Also, change the order in which the bindings are tried: pyqt5, pyqt, pyside2, pyside (up to now, pyqt4 was tried before pyqt5) --- lib/taurus/external/qt/__init__.py | 104 +++++++++++++++++------------ lib/taurus/tauruscustomsettings.py | 6 +- 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/lib/taurus/external/qt/__init__.py b/lib/taurus/external/qt/__init__.py index c149d1970..2888c9e86 100644 --- a/lib/taurus/external/qt/__init__.py +++ b/lib/taurus/external/qt/__init__.py @@ -43,23 +43,25 @@ class PythonQtError(RuntimeError): #: Qt API environment variable name QT_API = 'QT_API' -#: names of the expected PyQt5 api +#: names of the PyQt5 api PYQT5_API = ['pyqt5'] -#: names of the expected PyQt4 api +#: names of the PyQt4 api PYQT4_API = [ 'pyqt', # name used in IPython.qt 'pyqt4' # pyqode.qt original name ] -#: names of the expected PySide api +#: names of the PySide api PYSIDE_API = ['pyside'] -#: names of the expected PySide2 api +#: names of the PySide2 api PYSIDE2_API = ['pyside2'] +#: The constants PYQT5, PYQT4, PYSIDE, PYSIDE2, PYSIDE_VERSION and PYQT_VERSION +#: will be updated depending on the selected binding +PYQT5 = PYQT4 = PYSIDE = PYSIDE2 = False +PYSIDE_VERSION = PYQT_VERSION = None -PYQT5 = True -PYQT4 = PYSIDE = PYSIDE2 = False - +# First, check if some binding is already in use (and, if so, select it) if 'PyQt5' in sys.modules: API = 'pyqt5' elif 'PySide2' in sys.modules: @@ -72,40 +74,22 @@ class PythonQtError(RuntimeError): # if no binding is already loaded, use (in this order): # - QT_API environment variable # - tauruscustomsettings.DEFAULT_QT_API - # - 'pyqt5' - API = os.environ.get(QT_API, getattr(__config, 'DEFAULT_QT_API', 'pyqt')) + # - first successful import of 'pyqt5', 'pyqt4', 'pyside2', 'pyside' + API = os.environ.get(QT_API, getattr(__config, 'DEFAULT_QT_API', '')) API = API.lower() -assert API in (PYQT5_API + PYQT4_API + PYSIDE_API + PYSIDE2_API) - -if API in PYQT4_API: - try: - import sip - - sip.setapi('QString', 2) - sip.setapi('QVariant', 2) - sip.setapi('QDate', 2) - sip.setapi('QDateTime', 2) - sip.setapi('QTextStream', 2) - sip.setapi('QTime', 2) - sip.setapi('QUrl', 2) - from PyQt4.QtCore import PYQT_VERSION_STR as PYQT_VERSION # analysis:ignore - from PyQt4.QtCore import QT_VERSION_STR as QT_VERSION # analysis:ignore - - PYSIDE_VERSION = None - PYQT5 = False - PYQT4 = True - API = os.environ['QT_API'] = 'pyqt' # in case the original was "pyqt4" - except ImportError: - __log.debug('Cannot import PyQt4. Trying with PyQt5') - API = os.environ['QT_API'] = 'pyqt5' +msg = "Unknown Qt API '{}'".format(API) +assert (API in (PYQT5_API + PYQT4_API + PYSIDE_API + PYSIDE2_API + [''])), msg -if API in PYQT5_API: +if not API or API in PYQT5_API: try: from PyQt5.QtCore import PYQT_VERSION_STR as PYQT_VERSION from PyQt5.QtCore import QT_VERSION_STR as QT_VERSION PYSIDE_VERSION = None + PYQT4 = PYSIDE = PYSIDE2 = False + PYQT5 = True + API = os.environ['QT_API'] = 'pyqt5' if sys.platform == 'darwin': macos_version = LooseVersion(platform.mac_ver()[0]) @@ -124,17 +108,44 @@ class PythonQtError(RuntimeError): del macos_version except ImportError: - __log.debug('Cannot import PyQt5. Trying with PySide2') - API = os.environ['QT_API'] = 'pyside2' + if API: # if an specific API was requested, fail with import error + raise PythonQtError('Cannot import PyQt5') + # if no specific API was requested, allow trying other bindings + __log.debug('Cannot import PyQt5') -if API in PYSIDE2_API: +if not API or API in PYQT4_API: + try: + import sip + + sip.setapi('QString', 2) + sip.setapi('QVariant', 2) + sip.setapi('QDate', 2) + sip.setapi('QDateTime', 2) + sip.setapi('QTextStream', 2) + sip.setapi('QTime', 2) + sip.setapi('QUrl', 2) + from PyQt4.QtCore import PYQT_VERSION_STR as PYQT_VERSION # analysis:ignore + from PyQt4.QtCore import QT_VERSION_STR as QT_VERSION # analysis:ignore + + PYSIDE_VERSION = None + PYQT5 = PYSIDE = PYSIDE2 = False + PYQT4 = True + API = os.environ['QT_API'] = 'pyqt' # "pyqt4" is forced to "pyqt" + except ImportError: + if API: # if an specific API was requested, fail with import error + raise PythonQtError('Cannot import PyQt4') + # if no specific API was requested, allow trying other bindings + __log.debug('Cannot import PyQt4') + +if not API or API in PYSIDE2_API: try: from PySide2 import __version__ as PYSIDE_VERSION # analysis:ignore from PySide2.QtCore import __version__ as QT_VERSION # analysis:ignore PYQT_VERSION = None - PYQT5 = False + PYQT5 = PYQT4 = PYSIDE = False PYSIDE2 = True + API = os.environ['QT_API'] = 'pyside2' if sys.platform == 'darwin': macos_version = LooseVersion(platform.mac_ver()[0]) @@ -147,20 +158,29 @@ class PythonQtError(RuntimeError): del macos_version except ImportError: - __log.debug('Cannot import PyQt5. Trying with PySide') - API = os.environ['QT_API'] = 'pyside' + if API: # if an specific API was requested, fail with import error + raise PythonQtError('Cannot import PySide2') + # if no specific API was requested, allow trying other bindings + __log.debug('Cannot import PySide2') -if API in PYSIDE_API: +if not API or API in PYSIDE_API: try: from PySide import __version__ as PYSIDE_VERSION # analysis:ignore from PySide.QtCore import __version__ as QT_VERSION # analysis:ignore PYQT_VERSION = None - PYQT5 = False + PYQT5 = PYQT4 = PYSIDE = False PYSIDE = True + API = os.environ['QT_API'] = 'pyside' + except ImportError: + if API: # if an specific API was requested, fail with import error + raise PythonQtError('Cannot import PySide') + # if no specific API was requested, allow trying other bindings __log.debug('Cannot import PySide') - raise PythonQtError('No Qt bindings could be found') + +if not API: + raise PythonQtError('No Qt bindings could be imported') API_NAME = {'pyqt5': 'PyQt5', 'pyqt': 'PyQt4', 'pyqt4': 'PyQt4', 'pyside': 'PySide', 'pyside2': 'PySide2'}[API] diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index 28b17991d..af7f9957e 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -97,8 +97,10 @@ # Qt configuration # ---------------------------------------------------------------------------- -#: Set preferred API (if one is not already loaded) -DEFAULT_QT_API = 'pyqt' +#: Set preferred API (if one is not already loaded). Accepted values are +#: pyqt5, pyqt, pyside2, pyside. Set to an empty string to let taurus choose +#: the first that works from the accepted values. +DEFAULT_QT_API = '' #: Auto initialize Qt logging to python logging QT_AUTO_INIT_LOG = True From d6c5ae2b7bd93e261ba5137d605f3d0c1437fdf9 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 3 Jul 2020 15:57:41 +0200 Subject: [PATCH 343/373] Add unittests for taurus.external.qt Test selection of Qt binding in taurus.external.qt shim --- lib/taurus/external/qt/__init__.py | 18 ++--- lib/taurus/external/test/res/PyQt4/QtCore.py | 1 + .../external/test/res/PyQt4/__init__.py | 6 ++ lib/taurus/external/test/res/PyQt5/QtCore.py | 1 + .../external/test/res/PyQt5/__init__.py | 7 ++ lib/taurus/external/test/res/PySide/QtCore.py | 1 + .../external/test/res/PySide/__init__.py | 8 +++ .../external/test/res/PySide2/QtCore.py | 1 + .../external/test/res/PySide2/__init__.py | 8 +++ lib/taurus/external/test/res/__init__.py | 0 lib/taurus/external/test/test_qt_selection.py | 68 +++++++++++++++++++ 11 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 lib/taurus/external/test/res/PyQt4/QtCore.py create mode 100644 lib/taurus/external/test/res/PyQt4/__init__.py create mode 100644 lib/taurus/external/test/res/PyQt5/QtCore.py create mode 100644 lib/taurus/external/test/res/PyQt5/__init__.py create mode 100644 lib/taurus/external/test/res/PySide/QtCore.py create mode 100644 lib/taurus/external/test/res/PySide/__init__.py create mode 100644 lib/taurus/external/test/res/PySide2/QtCore.py create mode 100644 lib/taurus/external/test/res/PySide2/__init__.py create mode 100644 lib/taurus/external/test/res/__init__.py create mode 100644 lib/taurus/external/test/test_qt_selection.py diff --git a/lib/taurus/external/qt/__init__.py b/lib/taurus/external/qt/__init__.py index 2888c9e86..4c74e74b0 100644 --- a/lib/taurus/external/qt/__init__.py +++ b/lib/taurus/external/qt/__init__.py @@ -37,7 +37,7 @@ class PythonQtError(RuntimeError): - """Error raise if no bindings could be selected.""" + """Error raised if no bindings could be selected.""" pass @@ -78,8 +78,10 @@ class PythonQtError(RuntimeError): API = os.environ.get(QT_API, getattr(__config, 'DEFAULT_QT_API', '')) API = API.lower() -msg = "Unknown Qt API '{}'".format(API) -assert (API in (PYQT5_API + PYQT4_API + PYSIDE_API + PYSIDE2_API + [''])), msg + +if API not in (PYQT5_API + PYQT4_API + PYSIDE_API + PYSIDE2_API + ['']): + raise ImportError("Unknown Qt API '{}'".format(API)) + if not API or API in PYQT5_API: try: @@ -109,7 +111,7 @@ class PythonQtError(RuntimeError): del macos_version except ImportError: if API: # if an specific API was requested, fail with import error - raise PythonQtError('Cannot import PyQt5') + raise ImportError('Cannot import PyQt5') # if no specific API was requested, allow trying other bindings __log.debug('Cannot import PyQt5') @@ -133,7 +135,7 @@ class PythonQtError(RuntimeError): API = os.environ['QT_API'] = 'pyqt' # "pyqt4" is forced to "pyqt" except ImportError: if API: # if an specific API was requested, fail with import error - raise PythonQtError('Cannot import PyQt4') + raise ImportError('Cannot import PyQt4') # if no specific API was requested, allow trying other bindings __log.debug('Cannot import PyQt4') @@ -159,7 +161,7 @@ class PythonQtError(RuntimeError): del macos_version except ImportError: if API: # if an specific API was requested, fail with import error - raise PythonQtError('Cannot import PySide2') + raise ImportError('Cannot import PySide2') # if no specific API was requested, allow trying other bindings __log.debug('Cannot import PySide2') @@ -175,12 +177,12 @@ class PythonQtError(RuntimeError): except ImportError: if API: # if an specific API was requested, fail with import error - raise PythonQtError('Cannot import PySide') + raise ImportError('Cannot import PySide') # if no specific API was requested, allow trying other bindings __log.debug('Cannot import PySide') if not API: - raise PythonQtError('No Qt bindings could be imported') + raise ImportError('No Qt bindings could be imported') API_NAME = {'pyqt5': 'PyQt5', 'pyqt': 'PyQt4', 'pyqt4': 'PyQt4', 'pyside': 'PySide', 'pyside2': 'PySide2'}[API] diff --git a/lib/taurus/external/test/res/PyQt4/QtCore.py b/lib/taurus/external/test/res/PyQt4/QtCore.py new file mode 100644 index 000000000..3572960ed --- /dev/null +++ b/lib/taurus/external/test/res/PyQt4/QtCore.py @@ -0,0 +1 @@ +PYQT_VERSION_STR = QT_VERSION_STR = 4.99 \ No newline at end of file diff --git a/lib/taurus/external/test/res/PyQt4/__init__.py b/lib/taurus/external/test/res/PyQt4/__init__.py new file mode 100644 index 000000000..19ee76dd5 --- /dev/null +++ b/lib/taurus/external/test/res/PyQt4/__init__.py @@ -0,0 +1,6 @@ +import os + + +available = os.environ["AVAILABLE_QT_MOCKS"] +if available != "all" and __name__ not in available.split(","): + raise ImportError("{} mock not enabled".format(__name__)) diff --git a/lib/taurus/external/test/res/PyQt5/QtCore.py b/lib/taurus/external/test/res/PyQt5/QtCore.py new file mode 100644 index 000000000..aa5dd917a --- /dev/null +++ b/lib/taurus/external/test/res/PyQt5/QtCore.py @@ -0,0 +1 @@ +PYQT_VERSION_STR = QT_VERSION_STR = 5.99 \ No newline at end of file diff --git a/lib/taurus/external/test/res/PyQt5/__init__.py b/lib/taurus/external/test/res/PyQt5/__init__.py new file mode 100644 index 000000000..fffc74886 --- /dev/null +++ b/lib/taurus/external/test/res/PyQt5/__init__.py @@ -0,0 +1,7 @@ +import os + + +available = os.environ["AVAILABLE_QT_MOCKS"] +if available != "all" and __name__ not in available.split(","): + raise ImportError("{} mock not enabled".format(__name__)) + diff --git a/lib/taurus/external/test/res/PySide/QtCore.py b/lib/taurus/external/test/res/PySide/QtCore.py new file mode 100644 index 000000000..69d2b1999 --- /dev/null +++ b/lib/taurus/external/test/res/PySide/QtCore.py @@ -0,0 +1 @@ +__version__ = 4.99 \ No newline at end of file diff --git a/lib/taurus/external/test/res/PySide/__init__.py b/lib/taurus/external/test/res/PySide/__init__.py new file mode 100644 index 000000000..e8880cd30 --- /dev/null +++ b/lib/taurus/external/test/res/PySide/__init__.py @@ -0,0 +1,8 @@ +import os + + +available = os.environ["AVAILABLE_QT_MOCKS"] +if available != "all" and __name__ not in available.split(","): + raise ImportError("{} mock not enabled".format(__name__)) + +__version__ = 4.99 \ No newline at end of file diff --git a/lib/taurus/external/test/res/PySide2/QtCore.py b/lib/taurus/external/test/res/PySide2/QtCore.py new file mode 100644 index 000000000..ce3195f44 --- /dev/null +++ b/lib/taurus/external/test/res/PySide2/QtCore.py @@ -0,0 +1 @@ +__version__ = 5.99 \ No newline at end of file diff --git a/lib/taurus/external/test/res/PySide2/__init__.py b/lib/taurus/external/test/res/PySide2/__init__.py new file mode 100644 index 000000000..e8880cd30 --- /dev/null +++ b/lib/taurus/external/test/res/PySide2/__init__.py @@ -0,0 +1,8 @@ +import os + + +available = os.environ["AVAILABLE_QT_MOCKS"] +if available != "all" and __name__ not in available.split(","): + raise ImportError("{} mock not enabled".format(__name__)) + +__version__ = 4.99 \ No newline at end of file diff --git a/lib/taurus/external/test/res/__init__.py b/lib/taurus/external/test/res/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/taurus/external/test/test_qt_selection.py b/lib/taurus/external/test/test_qt_selection.py new file mode 100644 index 000000000..74c3db4dc --- /dev/null +++ b/lib/taurus/external/test/test_qt_selection.py @@ -0,0 +1,68 @@ +import os +import sys +import pytest +from taurus import tauruscustomsettings as ts + + +@pytest.mark.parametrize( + "qt_api, default_qt_api, available, expected", + [ + # no explicit selection, all bindings available + ("", "", "all", "pyqt5"), + (None, "", "all", "pyqt5"), + # no explicit selection, only one binding available + (None, "", "PyQt5", "pyqt5"), + (None, "", "PyQt4", "pyqt"), + (None, "", "PySide2", "pyside2"), + (None, "", "PySide", "pyside"), + # no explicit selection, with default selection, all available + (None, "pyqt5", "all", "pyqt5"), + (None, "pyqt", "all", "pyqt"), + (None, "pyqt4", "all", "pyqt"), + (None, "pyside2", "all", "pyside2"), + (None, "pyside", "all", "pyside"), + # explicit selection, all bindings available + ("pyqt5", "", "all", "pyqt5"), + ("pyqt", "", "all", "pyqt"), + ("pyqt4", "", "all", "pyqt"), + ("pyside2", "", "all", "pyside2"), + ("pyside", "", "all", "pyside"), + ("pyqt5", "pyqt4", "all", "pyqt5"), + ("pyqt", "pyqt5", "all", "pyqt"), + ("pyqt4", "pyqt5", "all", "pyqt"), + # unsupported selection + ("unsupported_binding", "", "all", ImportError), + (None, "unsupported_binding", "all", ImportError), + ("pyqt5", "", "none", ImportError), + ("pyqt5", "", "PyQt4", ImportError), + ("pyqt", "", "PyQt5", ImportError), + ("pyside2", "", "PySide", ImportError), + ("pyside", "", "PySide2", ImportError), + ] +) +@pytest.mark.forked # run in separate process to avoid side-effects +def test_foo(monkeypatch, qt_api, default_qt_api, available, expected): + # temporarily remove qt bindings from sys.modules + for binding in "PyQt5", "PyQt4", "PySide2", "PySide": + monkeypatch.delitem(sys.modules, binding, raising=False) + monkeypatch.delitem(sys.modules, binding+".QtCore", raising=False) + # monkeypatch QT_API and DEFAULT_QT_API + if qt_api is None: + monkeypatch.delenv("QT_API", raising=False) + else: + monkeypatch.setenv("QT_API", qt_api) + monkeypatch.setattr(ts, "DEFAULT_QT_API", default_qt_api) + # avoid initializations + monkeypatch.setattr(ts, "QT_AUTO_INIT_LOG", False) + monkeypatch.setattr(ts, "QT_AUTO_REMOVE_INPUTHOOK", False) + monkeypatch.setattr(ts, "QT_AVOID_ABORT_ON_EXCEPTION", False) + # provide importable mocks for all supported bindings + monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "res")) + monkeypatch.setenv("AVAILABLE_QT_MOCKS", available) + # test the shim + if not isinstance(expected, str): + with pytest.raises(expected): + from taurus.external.qt import API + else: + from taurus.external.qt import API + assert API == expected From 874b78bcb89057b08acccde01d1644506f7bacb0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 3 Jul 2020 16:36:28 +0200 Subject: [PATCH 344/373] Add pytest-forked to tox.ini dependencies --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 0d73fdf96..70094ef3d 100644 --- a/tox.ini +++ b/tox.ini @@ -39,6 +39,7 @@ conda_deps= pytest >= 3.6.3 pytest-xvfb pytest-qt + pytest-forked flaky pyvirtualdisplay=0.2 docs: sphinx From e77281602583dff7f1f97c4ee1b582b4f8b2252e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 3 Jul 2020 17:46:33 +0200 Subject: [PATCH 345/373] Remove __init__.py from resource dir taurus/external/test/res does not need to be a package Avoid test_import error by removing the __init__.py --- lib/taurus/external/test/res/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/taurus/external/test/res/__init__.py diff --git a/lib/taurus/external/test/res/__init__.py b/lib/taurus/external/test/res/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 598f7e9330466303c61a6ea3093af5a3faf34a26 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 3 Jul 2020 17:49:25 +0200 Subject: [PATCH 346/373] m, doc --- lib/taurus/external/test/test_qt_selection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/taurus/external/test/test_qt_selection.py b/lib/taurus/external/test/test_qt_selection.py index 74c3db4dc..eec7d7003 100644 --- a/lib/taurus/external/test/test_qt_selection.py +++ b/lib/taurus/external/test/test_qt_selection.py @@ -41,7 +41,8 @@ ] ) @pytest.mark.forked # run in separate process to avoid side-effects -def test_foo(monkeypatch, qt_api, default_qt_api, available, expected): +def test_qt_select(monkeypatch, qt_api, default_qt_api, available, expected): + """Check that the selection of Qt binding by taurus.external.qt works""" # temporarily remove qt bindings from sys.modules for binding in "PyQt5", "PyQt4", "PySide2", "PySide": monkeypatch.delitem(sys.modules, binding, raising=False) From cb9f44fe4a9e6366265d05a332540f3b0ce59e66 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 3 Jul 2020 18:15:10 +0200 Subject: [PATCH 347/373] Temporarily "unimport" taurus.external.qt before test The test_qt_select fails when executes as part of the taurussuite because another test has already imported `taurus.external.qt`. Avoid this by temporarily removing it from sys.modules --- lib/taurus/external/test/test_qt_selection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/taurus/external/test/test_qt_selection.py b/lib/taurus/external/test/test_qt_selection.py index eec7d7003..e883089f3 100644 --- a/lib/taurus/external/test/test_qt_selection.py +++ b/lib/taurus/external/test/test_qt_selection.py @@ -44,10 +44,11 @@ def test_qt_select(monkeypatch, qt_api, default_qt_api, available, expected): """Check that the selection of Qt binding by taurus.external.qt works""" # temporarily remove qt bindings from sys.modules + monkeypatch.delitem(sys.modules, "taurus.external.qt", raising=False) for binding in "PyQt5", "PyQt4", "PySide2", "PySide": monkeypatch.delitem(sys.modules, binding, raising=False) monkeypatch.delitem(sys.modules, binding+".QtCore", raising=False) - # monkeypatch QT_API and DEFAULT_QT_API + # monkeypatch QT_API and DEFAULT_QT_API with the values from arguments if qt_api is None: monkeypatch.delenv("QT_API", raising=False) else: @@ -60,7 +61,7 @@ def test_qt_select(monkeypatch, qt_api, default_qt_api, available, expected): # provide importable mocks for all supported bindings monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "res")) monkeypatch.setenv("AVAILABLE_QT_MOCKS", available) - # test the shim + # Now that the environment is clean, test the shim if not isinstance(expected, str): with pytest.raises(expected): from taurus.external.qt import API From 2e00d9ac8dd879eb3bde40cf83a3bf9ff5587139 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 3 Jul 2020 18:34:24 +0200 Subject: [PATCH 348/373] Blackify newly-created files --- lib/taurus/external/test/res/PyQt4/QtCore.py | 2 +- lib/taurus/external/test/res/PyQt5/QtCore.py | 2 +- lib/taurus/external/test/res/PyQt5/__init__.py | 1 - lib/taurus/external/test/res/PySide/QtCore.py | 2 +- lib/taurus/external/test/res/PySide/__init__.py | 2 +- lib/taurus/external/test/res/PySide2/QtCore.py | 2 +- lib/taurus/external/test/res/PySide2/__init__.py | 2 +- lib/taurus/external/test/test_qt_selection.py | 13 +++++++------ 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/taurus/external/test/res/PyQt4/QtCore.py b/lib/taurus/external/test/res/PyQt4/QtCore.py index 3572960ed..f4484975b 100644 --- a/lib/taurus/external/test/res/PyQt4/QtCore.py +++ b/lib/taurus/external/test/res/PyQt4/QtCore.py @@ -1 +1 @@ -PYQT_VERSION_STR = QT_VERSION_STR = 4.99 \ No newline at end of file +PYQT_VERSION_STR = QT_VERSION_STR = 4.99 diff --git a/lib/taurus/external/test/res/PyQt5/QtCore.py b/lib/taurus/external/test/res/PyQt5/QtCore.py index aa5dd917a..226ac257d 100644 --- a/lib/taurus/external/test/res/PyQt5/QtCore.py +++ b/lib/taurus/external/test/res/PyQt5/QtCore.py @@ -1 +1 @@ -PYQT_VERSION_STR = QT_VERSION_STR = 5.99 \ No newline at end of file +PYQT_VERSION_STR = QT_VERSION_STR = 5.99 diff --git a/lib/taurus/external/test/res/PyQt5/__init__.py b/lib/taurus/external/test/res/PyQt5/__init__.py index fffc74886..19ee76dd5 100644 --- a/lib/taurus/external/test/res/PyQt5/__init__.py +++ b/lib/taurus/external/test/res/PyQt5/__init__.py @@ -4,4 +4,3 @@ available = os.environ["AVAILABLE_QT_MOCKS"] if available != "all" and __name__ not in available.split(","): raise ImportError("{} mock not enabled".format(__name__)) - diff --git a/lib/taurus/external/test/res/PySide/QtCore.py b/lib/taurus/external/test/res/PySide/QtCore.py index 69d2b1999..81f12df50 100644 --- a/lib/taurus/external/test/res/PySide/QtCore.py +++ b/lib/taurus/external/test/res/PySide/QtCore.py @@ -1 +1 @@ -__version__ = 4.99 \ No newline at end of file +__version__ = 4.99 diff --git a/lib/taurus/external/test/res/PySide/__init__.py b/lib/taurus/external/test/res/PySide/__init__.py index e8880cd30..cc8617e5f 100644 --- a/lib/taurus/external/test/res/PySide/__init__.py +++ b/lib/taurus/external/test/res/PySide/__init__.py @@ -5,4 +5,4 @@ if available != "all" and __name__ not in available.split(","): raise ImportError("{} mock not enabled".format(__name__)) -__version__ = 4.99 \ No newline at end of file +__version__ = 4.99 diff --git a/lib/taurus/external/test/res/PySide2/QtCore.py b/lib/taurus/external/test/res/PySide2/QtCore.py index ce3195f44..ddf1b58c1 100644 --- a/lib/taurus/external/test/res/PySide2/QtCore.py +++ b/lib/taurus/external/test/res/PySide2/QtCore.py @@ -1 +1 @@ -__version__ = 5.99 \ No newline at end of file +__version__ = 5.99 diff --git a/lib/taurus/external/test/res/PySide2/__init__.py b/lib/taurus/external/test/res/PySide2/__init__.py index e8880cd30..cc8617e5f 100644 --- a/lib/taurus/external/test/res/PySide2/__init__.py +++ b/lib/taurus/external/test/res/PySide2/__init__.py @@ -5,4 +5,4 @@ if available != "all" and __name__ not in available.split(","): raise ImportError("{} mock not enabled".format(__name__)) -__version__ = 4.99 \ No newline at end of file +__version__ = 4.99 diff --git a/lib/taurus/external/test/test_qt_selection.py b/lib/taurus/external/test/test_qt_selection.py index e883089f3..6df2a2991 100644 --- a/lib/taurus/external/test/test_qt_selection.py +++ b/lib/taurus/external/test/test_qt_selection.py @@ -17,10 +17,10 @@ (None, "", "PySide", "pyside"), # no explicit selection, with default selection, all available (None, "pyqt5", "all", "pyqt5"), - (None, "pyqt", "all", "pyqt"), - (None, "pyqt4", "all", "pyqt"), - (None, "pyside2", "all", "pyside2"), - (None, "pyside", "all", "pyside"), + (None, "pyqt", "all", "pyqt"), + (None, "pyqt4", "all", "pyqt"), + (None, "pyside2", "all", "pyside2"), + (None, "pyside", "all", "pyside"), # explicit selection, all bindings available ("pyqt5", "", "all", "pyqt5"), ("pyqt", "", "all", "pyqt"), @@ -38,7 +38,7 @@ ("pyqt", "", "PyQt5", ImportError), ("pyside2", "", "PySide", ImportError), ("pyside", "", "PySide2", ImportError), - ] + ], ) @pytest.mark.forked # run in separate process to avoid side-effects def test_qt_select(monkeypatch, qt_api, default_qt_api, available, expected): @@ -47,7 +47,7 @@ def test_qt_select(monkeypatch, qt_api, default_qt_api, available, expected): monkeypatch.delitem(sys.modules, "taurus.external.qt", raising=False) for binding in "PyQt5", "PyQt4", "PySide2", "PySide": monkeypatch.delitem(sys.modules, binding, raising=False) - monkeypatch.delitem(sys.modules, binding+".QtCore", raising=False) + monkeypatch.delitem(sys.modules, binding + ".QtCore", raising=False) # monkeypatch QT_API and DEFAULT_QT_API with the values from arguments if qt_api is None: monkeypatch.delenv("QT_API", raising=False) @@ -67,4 +67,5 @@ def test_qt_select(monkeypatch, qt_api, default_qt_api, available, expected): from taurus.external.qt import API else: from taurus.external.qt import API + assert API == expected From 4d0ecdba7726667bf6df0f069ff345dfe1241bbc Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 6 Jul 2020 10:55:55 +0200 Subject: [PATCH 349/373] Test with pre-imported binding The binding selection by taurus.external.qt starts by checking if a binding is already installed. Cover this case in the tests as well. --- lib/taurus/external/test/test_qt_selection.py | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/lib/taurus/external/test/test_qt_selection.py b/lib/taurus/external/test/test_qt_selection.py index 6df2a2991..ec84fccbe 100644 --- a/lib/taurus/external/test/test_qt_selection.py +++ b/lib/taurus/external/test/test_qt_selection.py @@ -1,47 +1,61 @@ import os import sys +import importlib import pytest from taurus import tauruscustomsettings as ts @pytest.mark.parametrize( - "qt_api, default_qt_api, available, expected", + "qt_api, default_qt_api, installed, imported, expected", [ - # no explicit selection, all bindings available - ("", "", "all", "pyqt5"), - (None, "", "all", "pyqt5"), - # no explicit selection, only one binding available - (None, "", "PyQt5", "pyqt5"), - (None, "", "PyQt4", "pyqt"), - (None, "", "PySide2", "pyside2"), - (None, "", "PySide", "pyside"), - # no explicit selection, with default selection, all available - (None, "pyqt5", "all", "pyqt5"), - (None, "pyqt", "all", "pyqt"), - (None, "pyqt4", "all", "pyqt"), - (None, "pyside2", "all", "pyside2"), - (None, "pyside", "all", "pyside"), - # explicit selection, all bindings available - ("pyqt5", "", "all", "pyqt5"), - ("pyqt", "", "all", "pyqt"), - ("pyqt4", "", "all", "pyqt"), - ("pyside2", "", "all", "pyside2"), - ("pyside", "", "all", "pyside"), - ("pyqt5", "pyqt4", "all", "pyqt5"), - ("pyqt", "pyqt5", "all", "pyqt"), - ("pyqt4", "pyqt5", "all", "pyqt"), + # no explicit selection, all bindings installed + ("", "", "all", None, "pyqt5"), + (None, "", "all", None, "pyqt5"), + # no explicit selection, only one binding installed + (None, "", "PyQt5", None, "pyqt5"), + (None, "", "PyQt4", None, "pyqt"), + (None, "", "PySide2", None, "pyside2"), + (None, "", "PySide", None, "pyside"), + # no explicit selection, with default selection, all installed + (None, "pyqt5", "all", None, "pyqt5"), + (None, "pyqt", "all", None, "pyqt"), + (None, "pyqt4", "all", None, "pyqt"), + (None, "pyside2", "all", None, "pyside2"), + (None, "pyside", "all", None, "pyside"), + # explicit selection, all bindings installed + ("pyqt5", "", "all", None, "pyqt5"), + ("pyqt", "", "all", None, "pyqt"), + ("pyqt4", "", "all", None, "pyqt"), + ("pyside2", "", "all", None, "pyside2"), + ("pyside", "", "all", None, "pyside"), + ("pyqt5", "pyqt4", "all", None, "pyqt5"), + ("pyqt", "pyqt5", "all", None, "pyqt"), + ("pyqt4", "pyqt5", "all", None, "pyqt"), # unsupported selection - ("unsupported_binding", "", "all", ImportError), - (None, "unsupported_binding", "all", ImportError), - ("pyqt5", "", "none", ImportError), - ("pyqt5", "", "PyQt4", ImportError), - ("pyqt", "", "PyQt5", ImportError), - ("pyside2", "", "PySide", ImportError), - ("pyside", "", "PySide2", ImportError), + ("unsupported_binding", "", "all", None, ImportError), + (None, "unsupported_binding", "all", None, ImportError), + ("pyqt5", "", "none", None, ImportError), + ("pyqt5", "", "PyQt4", None, ImportError), + ("pyqt", "", "PyQt5", None, ImportError), + ("pyside2", "", "PySide", None, ImportError), + ("pyside", "", "PySide2", None, ImportError), + # previously imported binding + (None, "", "all", "PyQt5", "pyqt5"), + (None, "", "all", "PyQt4", "pyqt"), + (None, "", "all", "PySide2", "pyside2"), + (None, "", "all", "PySide", "pyside"), + ("pyqt5", "pyqt4", "all", "PySide2", "pyside2"), ], ) @pytest.mark.forked # run in separate process to avoid side-effects -def test_qt_select(monkeypatch, qt_api, default_qt_api, available, expected): +def test_qt_select( + monkeypatch, + qt_api, + default_qt_api, + installed, + imported, + expected +): """Check that the selection of Qt binding by taurus.external.qt works""" # temporarily remove qt bindings from sys.modules monkeypatch.delitem(sys.modules, "taurus.external.qt", raising=False) @@ -60,8 +74,11 @@ def test_qt_select(monkeypatch, qt_api, default_qt_api, available, expected): monkeypatch.setattr(ts, "QT_AVOID_ABORT_ON_EXCEPTION", False) # provide importable mocks for all supported bindings monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "res")) - monkeypatch.setenv("AVAILABLE_QT_MOCKS", available) - # Now that the environment is clean, test the shim + monkeypatch.setenv("AVAILABLE_QT_MOCKS", installed) + # emulate an already-imported binding + if imported is not None: + importlib.import_module(imported) + # Now that the environment is clean and ready, test the shim if not isinstance(expected, str): with pytest.raises(expected): from taurus.external.qt import API From 475410120c610626009b855cd882405385b538c0 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 6 Jul 2020 11:14:37 +0200 Subject: [PATCH 350/373] Add test with imported QtCore Test also the case in which the binding is imported with, e.g. "from PyQt4 import QtCore", etc Also blackify. --- lib/taurus/external/test/test_qt_selection.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/taurus/external/test/test_qt_selection.py b/lib/taurus/external/test/test_qt_selection.py index ec84fccbe..62c01f980 100644 --- a/lib/taurus/external/test/test_qt_selection.py +++ b/lib/taurus/external/test/test_qt_selection.py @@ -40,21 +40,18 @@ ("pyside2", "", "PySide", None, ImportError), ("pyside", "", "PySide2", None, ImportError), # previously imported binding - (None, "", "all", "PyQt5", "pyqt5"), - (None, "", "all", "PyQt4", "pyqt"), - (None, "", "all", "PySide2", "pyside2"), - (None, "", "all", "PySide", "pyside"), - ("pyqt5", "pyqt4", "all", "PySide2", "pyside2"), + (None, "", "all", ("PyQt5",), "pyqt5"), + (None, "", "all", ("PyQt4",), "pyqt"), + (None, "", "all", ("PySide2",), "pyside2"), + (None, "", "all", ("PySide",), "pyside"), + (None, "", "all", ("PySide.QtCore",), "pyside"), + (None, "", "all", (".QtCore", "PyQt4"), "pyqt"), + ("pyqt5", "pyqt4", "all", ("PySide2",), "pyside2"), ], ) @pytest.mark.forked # run in separate process to avoid side-effects def test_qt_select( - monkeypatch, - qt_api, - default_qt_api, - installed, - imported, - expected + monkeypatch, qt_api, default_qt_api, installed, imported, expected ): """Check that the selection of Qt binding by taurus.external.qt works""" # temporarily remove qt bindings from sys.modules @@ -77,7 +74,7 @@ def test_qt_select( monkeypatch.setenv("AVAILABLE_QT_MOCKS", installed) # emulate an already-imported binding if imported is not None: - importlib.import_module(imported) + importlib.import_module(*imported) # Now that the environment is clean and ready, test the shim if not isinstance(expected, str): with pytest.raises(expected): From 62b828abf5fb418bef21aa8530199abfe782d3c8 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 6 Jul 2020 11:40:53 +0200 Subject: [PATCH 351/373] Fix #1113 (TangoAttribute not reenabling polling) TangoAttribute calls `.disablePolling()` when unsubscribing from Change events (this happens automatically e.g. when removing its last listener). This is problematic because it permanently disables the polling (e.g. it won't e re-activated if a listener is lately added to the same attribute). Prevent this by "deactivating" instead of "disabling" the polling. --- lib/taurus/core/tango/tangoattribute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index fc21af821..fa6a0e914 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -746,7 +746,7 @@ def _unsubscribeChangeEvents(self): else: self.debug("Failed: %s", df.args[0].desc) self.trace(str(df)) - self.disablePolling() + self._deactivatePolling() self.__subscription_state = SubscriptionState.Unsubscribed def _subscribeConfEvents(self): From 191f3e08cc77a5630fd108189b1681752f3c1605 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 10 Jul 2020 19:26:40 +0200 Subject: [PATCH 352/373] Allow tango not to auto-subscribe to conf events TangoAttribute subscribes to configuration events automatically. This sometimes may not be wanted/needed(see https://github.com/taurus-org/taurus/issues/1118 ). Add an entry in tauruscustomsettings, allowing to disable this autosubscrition (but maintain the current behaviour by default). --- lib/taurus/core/tango/tangoattribute.py | 5 ++++- lib/taurus/tauruscustomsettings.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index fc21af821..bc8ddd4a2 100644 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -317,8 +317,11 @@ def __init__(self, name='', parent=None, **kwargs): self._decodeAttrInfoEx(attr_info) # subscribe to configuration events (unsubscription done at cleanup) + auto_subscribe_conf = getattr( + tauruscustomsettings, "TANGO_AUTOSUBSCRIBE_CONF", True + ) self.__cfg_evt_id = None - if self.factory().is_tango_subscribe_enabled(): + if auto_subscribe_conf and self.factory().is_tango_subscribe_enabled(): self._subscribeConfEvents() def __del__(self): diff --git a/lib/taurus/tauruscustomsettings.py b/lib/taurus/tauruscustomsettings.py index 28b17991d..3016c4bbb 100644 --- a/lib/taurus/tauruscustomsettings.py +++ b/lib/taurus/tauruscustomsettings.py @@ -85,6 +85,14 @@ #: 'Serial', 'Concurrent', or 'TangoSerial' (default) TANGO_SERIALIZATION_MODE = 'TangoSerial' +#: Whether TangoAttribute is subscribed to configuration events by default. +#: Setting to True (or not setting it) makes the TangoAttribute auto-subscribe +#: Setting to False avoids this subscription, which prevents issues such as +#: https://github.com/taurus-org/taurus/issues/1118 +#: but it also prevents clients to be notified when configurations (e.g., +#: units, format) change. +TANGO_AUTOSUBSCRIBE_CONF = True + #: PLY (lex/yacc) optimization: 1=Active (default) , 0=disabled. #: Set PLY_OPTIMIZE = 0 if you are getting yacc exceptions while loading #: synoptics From 5d141f066c9afd2effa0a1cc3765e01f0becda00 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Mon, 13 Jul 2020 10:51:44 +0200 Subject: [PATCH 353/373] Add len support to taurus enumeration taurus.core.util.enumeration.Enumeration stopped supporting len() operations at some point (probably since it was adapted for py3). This triggered a regression in taurusgui (see #1106). Add explicit len support. Fixes #1106 --- lib/taurus/core/util/enumeration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/taurus/core/util/enumeration.py b/lib/taurus/core/util/enumeration.py index 82f275a7e..ad16aff1d 100644 --- a/lib/taurus/core/util/enumeration.py +++ b/lib/taurus/core/util/enumeration.py @@ -137,6 +137,9 @@ def __call__(self, i): # Enumeration member. return self.lookup[self.whatis(i)] + def __len__(self): + return len(self.lookup) + def _generateUniqueId(self): if self._flaggable: n = 2 ** self._uniqueId From 6775d67eec31409cf79e894aa207fa6c0dfb3eec Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 14 Jul 2020 10:19:37 +0200 Subject: [PATCH 354/373] Bump version 4.6.5-alpha to 4.7.0-alpha --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index cd5bc215a..88cc5df0a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.6.5-alpha +current_version = 4.7.0-alpha parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index 52f80175c..3fc6a78c2 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.6.5-alpha' +version = '4.7.0-alpha' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: From 5022898b54c008ce0b6306cc8d534f9c5a4033da Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 14 Jul 2020 13:49:01 +0200 Subject: [PATCH 355/373] Update CHANGELOG (complete revision until date) --- CHANGELOG.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab399b74..530bf9321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Note: changes in the [support-3.x] branch (which was split from the master branch after [3.7.1] and maintained in parallel to the develop branch) won't be reflected in this file. -## [Unreleased] +## [4.7.0-a1] - 2020-07-15 [unreleased] ### Added - `plot`, `trend`, `trend2d`, `image` first-level taurus subcommands (#1120) @@ -15,17 +15,20 @@ develop branch) won't be reflected in this file. implementations of `plot`, `trend`, `trend2d`, `image` (#1120) - check-deps subcommand (#988) - `taurus.cli.register_subcommands()` and `taurus.cli.taurus_cmd()` (#991) +- Support for `ÂșC` as degree celsius in units (#1033) - Support for spyder v4 in `taurus.qt.qtgui.editor` (#1038) - Entry-point ("taurus.qt.formatters") for registering formatters via plugins (#1039) - New `worker_cls` argument for `taurus.core.util.ThreadPool` costructor (#1081) - `taurus.core.util.lazymodule` for delayed entry-point loading of modules (#1090) - `"taurus.form.item_factories"` entry-point group (#1108) -- Documentation about (experimental) entry-point based plugin support (#1120) +- `tauruscustomsettings.TANGO_AUTOSUBSCRIBE_CONF` to allow skipping of config + event subscription by `TangoAttribute` - -### Removed ### Changed +- Improved Qt binding selection mechanism (and prioritize PyQt5 over PyQt4) (#1121) - Qt theme no longer set to TangoIcons by default (for coherence with docs) (#1012) +- Improved Configuration options for TaurusGui (#940) +- Passing `cmd_line_parser=None` to TaurusApplication is no longer required (#1089) - (for developers) Support of tox and change to pytest. More platforms being now automatically tested by travis (#994) - TaurusForm provides more debugging info when failing to handle a model (#1049) @@ -33,31 +36,40 @@ develop branch) won't be reflected in this file. - Modules registered with `"taurus.qt.qtgui"` entry-point are now lazy-loaded (#1090) - The control over which custom widgets are to be used in a TaurusForm is now done by registering factories to `"taurus.form.item_factories"` entry-point (#1108) +- Allow case-insensitive values for the `taurus --log-level` option (#1112) - Qt unit tests reimplemented using pytest-qt (#1114) - `"'taurus.qt.qtgui.panel.TaurusModelSelector.items"` entry-point group renamed to `"taurus.model_selector.items"` +- Added support for 3rd party widgets in TaurusValue config settings (#1066) +- Improved documentation (#1044, #1056, #1059, #1120) ### Deprecated +- `qwt5` and `guiqwt` CLI subcommands (#1120) - `TaurusBaseWidget.showFormatterDlg()` (#1039) - Custom widget API in TaurusForm, TaurusValue and TaurusGui (#1108) - `tauruscustomsettings.T_FORM_CUSTOM_WIDGET_MAP` (#1108) - `BaseWidgetTestCase` and `GenericWidgetTestCase` (#1114) - `TimeOut` Device Server (#1114) -- `qwt5` and `guiqwt` CLI subcommands (#1120) ### Fixed - Several issues in TaurusWheelEdit (#1010, #1021) -- Several issues affecting synoptics (#1005, #1029) +- Several issues affecting synoptics (#1005, #1029, #1082) +- Issues with TaurusValueComboBox (#1102, #1032) +- Issues with TaurusValueLineEdit (#1072) +- TaurusValueLineEdit could not be enabled (#1117) - Support dns aliases for the authority name in tango model names (#998) - Py3 exception in `TaurusModelChooser.getListedModels()` (#1008) -- Thread safety issues in `TaurusPollingTimer`'s add/remove attributes API (#1022, #999) +- Thread safety issues in `TaurusPollingTimer`'s add/remove attributes API (#1002) +- Problem preventing editing existing GUIs with Taurus New Gui wizard (#1126) - (for py2) Improved backwards compatibility of `taurus.qt.qtgui.plot` (#1027) -- Exception in DelayedSubscriber (#1030) +- Issues with events and subscriptions in Tango (#1030, #1061, #1113) - Compatibility issue in deprecated TangoAttribute's `isScalar()` and `isSpectrum()` (#1034) +- Tooltip issues in case of device connection problems (#1087) - Some issues in taurus v3 to v4 migration support (#1059) -- Some CI test issues (#1075, #1069, #1109, #1114) +- Some CI test issues (#1042, #1069, #1073, #1075, #1109, #1114) + -## [4.6.1] - 2019-08-19 +## 4.6.1 - 2019-08-19 Hotfix for auto-deployment in PyPI with Travis. No other difference from 4.6.0. ### Fixed @@ -521,6 +533,7 @@ and several other places](https://sf.net/p/tauruslib/tickets/milestone/Jul15/) [TEP14]: http://www.taurus-scada.org/tep/?TEP14.md [TEP15]: http://www.taurus-scada.org/tep/?TEP15.md [Unreleased]: https://github.com/taurus-org/taurus/tree/develop +[4.7.0]: https://github.com/taurus-org/taurus/tree/release-4.7.0 [4.6.0]: https://github.com/taurus-org/taurus/tree/release-4.6.0 [4.5.1]: https://github.com/taurus-org/taurus/tree/release-4.5.1 [4.5.0]: https://github.com/taurus-org/taurus/tree/release-4.5.0 From 713550911d5888cf450068ab6706f18430224470 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 14 Jul 2020 13:51:14 +0200 Subject: [PATCH 356/373] Declare support for Py 3.6 and py3.7 in pypi classifiers --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index cc3ca49dc..54fc55b27 100644 --- a/setup.py +++ b/setup.py @@ -173,6 +173,8 @@ def get_release_info(): 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Scientific/Engineering', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: User Interfaces', From 356f9aba337685cca7bbd1a7f85986bc547b2571 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 14 Jul 2020 15:18:01 +0200 Subject: [PATCH 357/373] Update how_to_release manual test checklist --- doc/how_to_release.md | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/doc/how_to_release.md b/doc/how_to_release.md index 7e2ad1952..6d468286c 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -71,18 +71,21 @@ http://taurus-scada.org/users/getting_started.html - [ ] For LCD: Test the foreground roles and the background role - [ ] For Led: Test the colors, ON color, Off color. (hint: you can use `eval:False` as a model for testing) -### taurus guiqwt image +### taurus image -- [ ] Execute `taurus guiqwt image --demo` +- [ ] Execute `taurus image --demo` - [ ] try to resize the image and pan it using the mouse. - [ ] check the cross section tools, the color maps, etc. - [ ] replace the image using the "Change Taurus Model" button (choose , eg, sys/tg_test/1/double_image_ro) -### taurus guiqwt trend2d +### taurus trend2d -- [ ] Execute: `taurus --polling-period 333 guiqwt trend2d --demo` -- [ ] Execute: `taurus --polling-period 333 guiqwt trend2d -xt --demo` **(known to fail in 4.1.0)** -- [ ] Execute: `taurus --polling-period 333 guiqwt trend2d -xe --demo` +- [ ] Execute: `taurus --polling-period 333 trend2d --demo` +- [ ] Execute: `taurus --polling-period 333 trend2d -xt --demo` **(known to fail in 4.1.0)** +- [ ] Execute: `taurus --polling-period 333 trend2d -xe --demo` +- [ ] Execute: `taurus --polling-period 333 trend2d --demo -b 10` + (deactivate auto-scale bottom axis and see that the plot is limited to the + last 10 values ) - [ ] Test auto-scroll and auto-scale tools (from context menu) ### taurus designer @@ -148,8 +151,9 @@ http://taurus-scada.org/users/getting_started.html - [ ] Check that tooltips give info on each icon - [ ] Click on some icons and check that they give a bigger view of the icon and more info. -### taurus tpg plot (needs taurus_pyqtgraph installed) -- [ ] Execute: `taurus tpg plot "eval:Q(rand(333),'mm')" sys/tg_test/1/wave` +### taurus_pyqtgraph plot (needs taurus_pyqtgraph installed) +- [ ] Execute: `taurus plot "eval:Q(rand(333),'mm')" sys/tg_test/1/wave` + (if using py2 and Qt4, you may need to use `--use-alt=tpg` to select the tpg implementation) - [ ] Check zoom / panning (drag with right / left button), and Use (A) button to auto-range - [ ] Test inspector tool - [ ] Move curves between axes by using the plot configuration option in context menu @@ -159,9 +163,11 @@ http://taurus-scada.org/users/getting_started.html - [ ] Open the "Input data selection" dialog and add/remove/reorder/edit models. Try adding models both for X and Y - [ ] NOT YET READY Test Save & restore config (change curve properties, zoom, etc & check that everything is restored) -### taurus tpg trend (needs taurus_pyqtgraph installed) -- [ ] Execute: `taurus tpg trend "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` -- [ ] Execute: `taurus tpg trend -xe "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` +### taurus_pyqtgraph trend (needs taurus_pyqtgraph installed) +- [ ] Execute: `taurus trend "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` + (if using py2 and Qt4, you may need to use `--use-alt=tpg` to select the tpg implementation) +- [ ] Execute: `taurus trend -xe "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` + (if using py2 and Qt4, you may need to use `--use-alt=tpg` to select the tpg implementation) - [ ] Check zoom / panning (drag with right / left button), and Use (A) button to auto-range - [ ] Test inspector tool - [ ] Move curves between axes by using the plot configuration option in context menu @@ -175,7 +181,7 @@ http://taurus-scada.org/users/getting_started.html ### taurus qwt5 plot _Only if using py2 qt4_ (basically try all features described in the [user's guide](http://taurus-scada.org/en/latest/users/ui/index.html) -- [ ] Execute: `taurus qwt5 plot "eval:Q(rand(333),'mm')" sys/tg_test/1/wave` +- [ ] Execute: `taurus plot --use-alt=qwt5 "eval:Q(rand(333),'mm')" sys/tg_test/1/wave` - [ ] Check region Zoom in and out with region zoom and go back stacked zoom levels with the mouse middle button - [ ] Check mouse wheel Zoom - [ ] Test panning (dragging with CTRL pressed) @@ -193,8 +199,8 @@ http://taurus-scada.org/users/getting_started.html ### taurus qwt5 trend _Only if using py2 qt4_ (basically try all features described in the [user's guide](http://taurus-scada.org/en/latest/users/ui/index.html) -- [ ] Execute: `taurus qwt5 trend "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` -- [ ] Execute: `taurus qwt5 trend -xn "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` +- [ ] Execute: `taurus trend --use-alt=qwt5 "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` +- [ ] Execute: `taurus trend --use-alt=qwt5 -xn "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` - [ ] Check region Zoom in and out with region zoom and go back stacked zoom levels with the mouse middle button - [ ] Check mouse wheel Zoom - [ ] Test panning (dragging with CTRL pressed) From a2cdbdd042d6f4f230ca96901c3c254b8b067be6 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 14 Jul 2020 17:44:44 +0200 Subject: [PATCH 358/373] Update how_to_release.md --- doc/how_to_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to_release.md b/doc/how_to_release.md index 6d468286c..aee62804d 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -59,7 +59,7 @@ http://taurus-scada.org/users/getting_started.html - [ ] Install taurus-pyqtgraph from the master branch of its repo: `pip install https://github.com/taurus-org/taurus_pyqtgraph/archive/master.zip`. - [ ] Check installed version of taurus: `taurus --version` -- [ ] Check installed version of taurus_pyqtgraph: `taurus tpg --version` +- [ ] Check installed version of taurus_pyqtgraph: `python -c "import taurus_pyqtgraph; print(taurus_pyqtgraph.__version__)"` ### taurus demo From 9b87dcf06d07e866fa6028666489514d7f36cb83 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 14 Jul 2020 18:38:34 +0200 Subject: [PATCH 359/373] taurus trend2d CLI: replace -xn by -xe option The only current implementation of tren2d is guiqwt-based, and it does not support the "n" mode for the x_axis_mode option. Instead it supports "e". Change it in the CLI definition. --- lib/taurus/cli/alt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index 41cbe544d..f3a5054fa 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -344,7 +344,7 @@ def trend_cmd( @click.command("trend2d") @taurus.cli.common.model -@x_axis_mode_option(["d", "t", "n"]) +@x_axis_mode_option(["d", "t", "e"]) @taurus.cli.common.demo @taurus.cli.common.window_name("TaurusTrend2D") @taurus.cli.common.use_alternative From fb2b5654fd8301b989947a66c7166735efe45ef2 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 15 Jul 2020 14:02:40 +0200 Subject: [PATCH 360/373] Update manual tests checklist --- doc/how_to_release.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/how_to_release.md b/doc/how_to_release.md index aee62804d..835da0ee5 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -152,6 +152,7 @@ http://taurus-scada.org/users/getting_started.html - [ ] Click on some icons and check that they give a bigger view of the icon and more info. ### taurus_pyqtgraph plot (needs taurus_pyqtgraph installed) +- [ ] Execute `taurus plot --ls-alt` (check that it lists "qwt5" and "tpg") - [ ] Execute: `taurus plot "eval:Q(rand(333),'mm')" sys/tg_test/1/wave` (if using py2 and Qt4, you may need to use `--use-alt=tpg` to select the tpg implementation) - [ ] Check zoom / panning (drag with right / left button), and Use (A) button to auto-range @@ -159,11 +160,12 @@ http://taurus-scada.org/users/getting_started.html - [ ] Move curves between axes by using the plot configuration option in context menu - [ ] With curves in Y1 and Y2, test zooms and panning on separate axes (drag with right/left on the axis) - [ ] Test plot configuration dialog -- [ ] Test changing curve titles -- [ ] Open the "Input data selection" dialog and add/remove/reorder/edit models. Try adding models both for X and Y +- [ ] Test changing curve titles (NOT YET READY: See taurus-org/taurus_pyqtgraph#31) +- [ ] Open the "Model selection" dialog and add/remove/reorder/edit models. Try adding models both for X and Y - [ ] NOT YET READY Test Save & restore config (change curve properties, zoom, etc & check that everything is restored) ### taurus_pyqtgraph trend (needs taurus_pyqtgraph installed) +- [ ] Execute `taurus trend --ls-alt` (check that it lists "qwt5" and "tpg") - [ ] Execute: `taurus trend "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` (if using py2 and Qt4, you may need to use `--use-alt=tpg` to select the tpg implementation) - [ ] Execute: `taurus trend -xe "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` From dcc8d246f96d4f6d3f04e554f003544624834070 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 15 Jul 2020 23:32:02 +0200 Subject: [PATCH 361/373] Update manual tests checklist --- doc/how_to_release.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/how_to_release.md b/doc/how_to_release.md index 835da0ee5..87ff81a94 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -82,6 +82,7 @@ http://taurus-scada.org/users/getting_started.html - [ ] Execute: `taurus --polling-period 333 trend2d --demo` - [ ] Execute: `taurus --polling-period 333 trend2d -xt --demo` **(known to fail in 4.1.0)** +- [ ] Execute: `taurus --polling-period 333 trend2d -xn --demo` - [ ] Execute: `taurus --polling-period 333 trend2d -xe --demo` - [ ] Execute: `taurus --polling-period 333 trend2d --demo -b 10` (deactivate auto-scale bottom axis and see that the plot is limited to the @@ -168,8 +169,9 @@ http://taurus-scada.org/users/getting_started.html - [ ] Execute `taurus trend --ls-alt` (check that it lists "qwt5" and "tpg") - [ ] Execute: `taurus trend "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` (if using py2 and Qt4, you may need to use `--use-alt=tpg` to select the tpg implementation) -- [ ] Execute: `taurus trend -xe "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` +- [ ] Execute: `taurus trend -xn "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` (if using py2 and Qt4, you may need to use `--use-alt=tpg` to select the tpg implementation) + - THIS IS NOT YET SUPPORTED: You should get a *"X mode "n" not yet supported"* message - [ ] Check zoom / panning (drag with right / left button), and Use (A) button to auto-range - [ ] Test inspector tool - [ ] Move curves between axes by using the plot configuration option in context menu @@ -183,7 +185,7 @@ http://taurus-scada.org/users/getting_started.html ### taurus qwt5 plot _Only if using py2 qt4_ (basically try all features described in the [user's guide](http://taurus-scada.org/en/latest/users/ui/index.html) -- [ ] Execute: `taurus plot --use-alt=qwt5 "eval:Q(rand(333),'mm')" sys/tg_test/1/wave` +- [ ] Execute: `QT_API=pyqt taurus plot "eval:Q(rand(333),'mm')" sys/tg_test/1/wave` - [ ] Check region Zoom in and out with region zoom and go back stacked zoom levels with the mouse middle button - [ ] Check mouse wheel Zoom - [ ] Test panning (dragging with CTRL pressed) @@ -201,8 +203,8 @@ http://taurus-scada.org/users/getting_started.html ### taurus qwt5 trend _Only if using py2 qt4_ (basically try all features described in the [user's guide](http://taurus-scada.org/en/latest/users/ui/index.html) -- [ ] Execute: `taurus trend --use-alt=qwt5 "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` -- [ ] Execute: `taurus trend --use-alt=qwt5 -xn "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` +- [ ] Execute: `QT_API=pyqt taurus trend "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` +- [ ] Execute: `QT_API=pyqt taurus trend --use-alt=qwt5 -xn "eval:Q(rand(),'mm')" sys/tg_test/1/ampli` - [ ] Check region Zoom in and out with region zoom and go back stacked zoom levels with the mouse middle button - [ ] Check mouse wheel Zoom - [ ] Test panning (dragging with CTRL pressed) From f7af362f4f06de9997dea9b869e52d9764272c90 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 15 Jul 2020 23:34:45 +0200 Subject: [PATCH 362/373] Allow "n" (="e") in trend2d x_axis_mode Allow to use `-xn` with the same effect as `-xe` as an option of trend2d --- lib/taurus/cli/alt.py | 2 +- lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index f3a5054fa..f59855e5b 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -344,7 +344,7 @@ def trend_cmd( @click.command("trend2d") @taurus.cli.common.model -@x_axis_mode_option(["d", "t", "e"]) +@x_axis_mode_option(["d", "t", "n", "e"]) @taurus.cli.common.demo @taurus.cli.common.window_name("TaurusTrend2D") @taurus.cli.common.use_alternative diff --git a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py index 3efd3fa93..f49422a68 100644 --- a/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py +++ b/lib/taurus/qt/qtgui/extra_guiqwt/taurustrend2d.py @@ -74,7 +74,8 @@ def __init__(self, parent=None, designMode=False, toolbar=True, TaurusBaseWidget.__init__(self, "TaurusTrend2DDialog") # support x_axis_mode values (map them to stackMode values) stackMode = dict( - t='datetime', d='deltatime', e='event').get(stackMode, stackMode) + t='datetime', d='deltatime', e='event', n='event' + ).get(stackMode, stackMode) self.trendItem = None self.buffersize = buffersize self._useArchiving = False From 7650e0079522da6c9d463cdc9c2e064fb5b5ee3e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 4 Aug 2020 17:39:50 +0200 Subject: [PATCH 363/373] Update how_to_release.md --- doc/how_to_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to_release.md b/doc/how_to_release.md index 87ff81a94..a4700905b 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -134,7 +134,7 @@ http://taurus-scada.org/users/getting_started.html - [ ] Create a new panel (a TaurusForm) and drag and drop several models from other forms - [ ] Move panels around (with view unlocked!) and hide ("close") and re-show them - [ ] Test saving and restoring perspectives -- [ ] Test drag&drop from a form to a trend (won't work if using the tpg.TaurusTrend, until [this](https://github.com/taurus-org/taurus_pyqtgraph/issues/25)) is fixed +- [ ] Test drag&drop from a form to a trend - [ ] Test drag&drop from a form to a plot - [ ] Test clicking on "example01 synoptic" elements and check that the panels raised - [ ] Test that selecting a panel changes the selection on "example01 synoptic" From 8eb21293bc0fa9d19098574032acf6aa0c8cdb67 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 4 Aug 2020 18:01:22 +0200 Subject: [PATCH 364/373] Update how_to_release.md --- doc/how_to_release.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/how_to_release.md b/doc/how_to_release.md index a4700905b..b22aa0416 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -142,6 +142,7 @@ http://taurus-scada.org/users/getting_started.html - [ ] Create a new TaurusGui (call it `foogui`) with `taurus newgui` (follow the wizard) - [ ] Install `foogui` with pip (using a virtualenv may be a good idea) - [ ] launch `foogui` using the script that has been installed +- [ ] edit the just created gui by relaunching the wizard (`taurus newgui`) and selecting the same directory - [ ] ... other features from [user's guide](http://taurus-scada.org/users/ui/index.html) ### taurus config From 583fc4fb38b342874e9db6488243c3d294d42ce7 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 4 Aug 2020 19:39:17 +0200 Subject: [PATCH 365/373] Use .tobytes() instead of deprecated .tostring() Numpy Array's tostring is a deprecated alias for tobytes (also in py2.7). Use tobytes instead. --- lib/taurus/core/util/codecs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taurus/core/util/codecs.py b/lib/taurus/core/util/codecs.py index 686090667..8574ed9a2 100644 --- a/lib/taurus/core/util/codecs.py +++ b/lib/taurus/core/util/codecs.py @@ -577,7 +577,7 @@ def encode(self, data, *args, **kwargs): # frameNumber, unknown then -1 height, width = data[1].shape header = self.__packHeader(imgMode, -1, width, height) - buffer = data[1].tostring() + buffer = data[1].tobytes() return fmt, header + buffer def decode(self, data, *args, **kwargs): From efc9bb424314a6b89d1ed5df9ad5c0fbc7003830 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Wed, 5 Aug 2020 18:04:58 +0200 Subject: [PATCH 366/373] Fix '--config' option in plot and trend commands An out of place "exit" statement prevented the --config option to work in the plot and trend subcommands. Fix it. --- lib/taurus/cli/alt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/taurus/cli/alt.py b/lib/taurus/cli/alt.py index f59855e5b..bc3d36fa7 100644 --- a/lib/taurus/cli/alt.py +++ b/lib/taurus/cli/alt.py @@ -232,7 +232,7 @@ def plot_cmd( epname, e, ) - sys.exit(1) + sys.exit(1) if models: w.setModel(models) @@ -316,7 +316,7 @@ def trend_cmd( epname, e, ) - sys.exit(1) + sys.exit(1) # max buffer size option if max_buffer_size is not None: From deee23499bdbd6b1d68ebe9357c5ca3d981ef06e Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 6 Aug 2020 23:22:39 +0200 Subject: [PATCH 367/373] Add conda recipe Add conda dir and meta.yaml file to create taurus conda packages --- conda/meta.yaml | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 conda/meta.yaml diff --git a/conda/meta.yaml b/conda/meta.yaml new file mode 100644 index 000000000..4db63810c --- /dev/null +++ b/conda/meta.yaml @@ -0,0 +1,62 @@ +{% set data = load_setup_py_data(setup_file="../setup.py", + from_recipe_dir=True) %} + +{% set GIT_HASHTAG = os.popen("git log --pretty=format:'%h' -n 1").read().strip()%} +package: + name: {{data.get('name').lower().replace(' ', '_')}} + version: {{ data.get('version').replace('-alpha', 'a0') }} + +source: + path: .. + +build: + number: {{environ.get('BUILD_NUMBER',0)}} + string: {{ PKG_BUILDNUM }}_g{{GIT_HASHTAG}} + noarch: python + script: '{{PYTHON}} setup.py install --single-version-externally-managed + --record=record.txt' + entry_points: {{data.get('entry_points', {}).get('console_scripts', [])}} + +requirements: + host: + - python {{data.get('python_requires','')}} + - setuptools + run: + - python {{data.get('python_requires','')}} + - numpy + - pint + - future + - click + - pyqt + - lxml + - guidata + - guiqwt + - ipython + - pillow + - ply + - pythonqwt + - scipy + - pymca + +test: + imports: + - taurus + - taurus.core + - taurus.qt + - taurus.external + - taurus.cli + commands: + - taurus --help + requires: + {% for dep in data.get('tests_require',[]) %} + - {{ dep.lower()}} + {% endfor %} + - pytest >= 3.6.3 + - pytest-qt + - pytest-forked + +about: + home: {{ data.get('url')}} + license: {{ data.get('license')}} + summary: {{ data.get('description')}} + author: {{ data.get('author')}} From 1ae6f337596d6155f7ccd3db1b1c0920294460a7 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 6 Aug 2020 23:24:51 +0200 Subject: [PATCH 368/373] Add python_requires in setup.py Indicate that taurus is supported for python>=2.7 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 54fc55b27..ff98b7f46 100644 --- a/setup.py +++ b/setup.py @@ -200,6 +200,7 @@ def get_release_info(): include_package_data=True, entry_points=entry_points, provides=provides, + python_requires='>=2.7', install_requires=install_requires, extras_require=extras_require, test_suite='taurus.test.testsuite.get_taurus_suite', From cc4771fdb1db5b16689be338ff081612d1a2d310 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Thu, 6 Aug 2020 23:27:58 +0200 Subject: [PATCH 369/373] Clean and modernize setup.py - avoid using deprecated imp module (use importlib) - avoid importing distutils - stop supporting setuptools <20.2 - remove obsolete comments --- setup.py | 57 +++++++++++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/setup.py b/setup.py index ff98b7f46..b8848f47d 100644 --- a/setup.py +++ b/setup.py @@ -23,20 +23,30 @@ ## ############################################################################## -import os -import imp import sys -from distutils.version import LooseVersion -from setuptools import setup, find_packages, __version__ +from setuptools import setup, find_packages def get_release_info(): - name = "release" - setup_dir = os.path.dirname(os.path.abspath(__file__)) - release_dir = os.path.join(setup_dir, 'lib', 'taurus', 'core') - data = imp.find_module(name, [release_dir]) - ret = imp.load_module(name, *data) - return ret + if sys.version_info >= (3, 5): + from importlib.util import spec_from_file_location, module_from_spec + from pathlib import Path + spec = spec_from_file_location( + 'release', + Path(__file__).parent / 'lib' / 'taurus' / 'core' / 'release.py' + ) + module = module_from_spec(spec) + spec.loader.exec_module(module) + else: # for py27 + import os + import imp + module_name = "release" + setup_dir = os.path.dirname(os.path.abspath(__file__)) + release_dir = os.path.join(setup_dir, 'lib', 'taurus', 'core') + data = imp.find_module(module_name, [release_dir]) + module = imp.load_module(module_name, *data) + return module + release = get_release_info() @@ -46,15 +56,6 @@ def get_release_info(): provides = [ 'taurus', - # 'taurus.core', - # 'taurus.qt', - # 'Taurus-Tango', # [Taurus-Tango] - # 'Taurus-Qt', # [Taurus-Qt] - # 'Taurus-Qt-PyQwt', # [Taurus-Qt-Plot] - # 'Taurus-Qt-Synoptic', # [Taurus-Qt-Synoptic] - # 'Taurus-Qt-TaurusGUI', # [Taurus-Qt-TaurusGUI] - # 'Taurus-Qt-Editor', # [Taurus-Qt-Editor] --> or maybe move it to sardana - # 'Taurus-Qt-Guiqwt', # [Taurus-Qt-Guiqwt] ] install_requires = [ @@ -62,23 +63,15 @@ def get_release_info(): 'pint>=0.8', 'future', 'click', + 'enum34;python_version<"3.4"', ] -#Workaround for old setuptools - -if LooseVersion(__version__) < LooseVersion('20.2'): - if sys.version_info < (3, 4): - install_requires.append('enum34') -else: - install_requires.append('enum34;python_version<"3.4"') - - extras_require = { 'taurus-qt': [# 'PyQt4 >=4.8', - # 'PyQt4.Qwt5 >=5.2.0', # [Taurus-Qt-Plot] - 'ply >=2.3', # [Taurus-Qt-Synoptic] - 'lxml >=2.1', # [Taurus-Qt-TaurusGUI] - 'guiqwt >=3', # [Taurus-Qt-Guiqwt] + # 'PyQt4.Qwt5 >=5.2.0', + 'ply >=2.3', # synoptics + 'lxml >=2.1', # taurusgui + 'guiqwt >=3', # extra_guiqwt ], 'taurus-tango': ['PyTango >=7.1', ], From 78fcb097a2992586386c79c77ba328f57c63b6c4 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 7 Aug 2020 01:38:12 +0200 Subject: [PATCH 370/373] Fix problem with pathlib in py35 In py35, the pathlib.Path object cannot be used directly with spec_from_file_location() and this breaks setup.py. Fix by converting to a str with as_posix() --- setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index b8848f47d..6005ba7e9 100644 --- a/setup.py +++ b/setup.py @@ -31,10 +31,8 @@ def get_release_info(): if sys.version_info >= (3, 5): from importlib.util import spec_from_file_location, module_from_spec from pathlib import Path - spec = spec_from_file_location( - 'release', - Path(__file__).parent / 'lib' / 'taurus' / 'core' / 'release.py' - ) + path = Path(__file__).parent / 'lib' / 'taurus' / 'core' / 'release.py' + spec = spec_from_file_location('release', path.as_posix()) module = module_from_spec(spec) spec.loader.exec_module(module) else: # for py27 From 6a22ceb834bd7cee70094d7690f963a4ca17bc4b Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 7 Aug 2020 09:40:18 +0200 Subject: [PATCH 371/373] Add automated conda build and deployment on releases Use the taurus-org/publish-conda-action just implemented in order to deploy to the taurus-org conda channel. --- .github/workflows/publish_conda.yml | 21 +++++++++++++++++++++ conda/meta.yaml | 12 +++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish_conda.yml diff --git a/.github/workflows/publish_conda.yml b/.github/workflows/publish_conda.yml new file mode 100644 index 000000000..18a2e8d07 --- /dev/null +++ b/.github/workflows/publish_conda.yml @@ -0,0 +1,21 @@ +name: publish_conda + +on: + release: + types: [published] + workflow_dispatch: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: publish-to-conda + uses: taurus-org/publish-conda-action@v2 + with: + subdir: 'conda' + anacondatoken: ${{ secrets.ANACONDA_TOKEN }} + platforms: 'noarch' diff --git a/conda/meta.yaml b/conda/meta.yaml index 4db63810c..ae3678500 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -1,7 +1,13 @@ {% set data = load_setup_py_data(setup_file="../setup.py", from_recipe_dir=True) %} -{% set GIT_HASHTAG = os.popen("git log --pretty=format:'%h' -n 1").read().strip()%} +{% set BUILD_NUMBER = environ.get('GITHUB_RUN_NUMBER','0') %} + +{% set GIT_HASHTAG = environ.get('GITHUB_SHA', + os.popen("git log --pretty=format:'%h' -n 1").read().strip() + )[:7] %} + + package: name: {{data.get('name').lower().replace(' ', '_')}} version: {{ data.get('version').replace('-alpha', 'a0') }} @@ -10,8 +16,8 @@ source: path: .. build: - number: {{environ.get('BUILD_NUMBER',0)}} - string: {{ PKG_BUILDNUM }}_g{{GIT_HASHTAG}} + number: {{BUILD_NUMBER}} + string: {{ PKG_BUILDNUM }}_{{GIT_HASHTAG}} noarch: python script: '{{PYTHON}} setup.py install --single-version-externally-managed --record=record.txt' From bb61daf6d49c1e170f7d1fab1ebbbbe5fd08491f Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 7 Aug 2020 21:16:34 +0200 Subject: [PATCH 372/373] Update CHANGELOG for releasing 4.7.0 --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 530bf9321..198c628c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ Note: changes in the [support-3.x] branch (which was split from the master branch after [3.7.1] and maintained in parallel to the develop branch) won't be reflected in this file. -## [4.7.0-a1] - 2020-07-15 [unreleased] + +## [4.7.0] - 2020-08-07 +[Jul20 milestone](https://github.com/taurus-org/taurus/milestone/15) ### Added - `plot`, `trend`, `trend2d`, `image` first-level taurus subcommands (#1120) @@ -23,6 +25,8 @@ develop branch) won't be reflected in this file. - `"taurus.form.item_factories"` entry-point group (#1108) - `tauruscustomsettings.TANGO_AUTOSUBSCRIBE_CONF` to allow skipping of config event subscription by `TangoAttribute` +- Official taurus packages are now automatically published in the taurus-org + Anaconda channel ### Changed - Improved Qt binding selection mechanism (and prioritize PyQt5 over PyQt4) (#1121) From 42fbf7d97ede877c88c414965a81120abf90eeb6 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Fri, 7 Aug 2020 21:16:55 +0200 Subject: [PATCH 373/373] Bump version 4.7.0-alpha to 4.7.0 --- .bumpversion.cfg | 2 +- lib/taurus/core/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 88cc5df0a..773c7fcfa 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 4.7.0-alpha +current_version = 4.7.0 parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/lib/taurus/core/release.py b/lib/taurus/core/release.py index 3fc6a78c2..fa777b701 100644 --- a/lib/taurus/core/release.py +++ b/lib/taurus/core/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '4.7.0-alpha' +version = '4.7.0' # generate version_info and revision (**deprecated** since version 4.0.2-dev). if '-' in version: