#!/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 provides the set of base classes designed to provide
configuration features to the classes that inherit from them"""
from __future__ import print_function
from future import standard_library
standard_library.install_aliases()
from builtins import str
from builtins import object
from future.utils import string_types
__all__ = ["configurableProperty", "BaseConfigurableClass"]
__docformat__ = 'restructuredtext'
class configurableProperty(object):
'''A dummy class used to handle properties with the configuration API
.. warning:: this class is intended for internal use by the configuration
package. Do not instantiate it directly in your code.
Use :meth:`BaseConfigurableClass.registerConfigProperty` instead.
'''
def __init__(self, name, fget, fset, obj=None):
self.name = name
self.fget = fget # this may either be a method or a method name
self.fset = fset # this may either be a method or a method name
self._obj = obj # obj is only needed if fset or fget are method names
def createConfig(self, allowUnpickable=False):
'''returns value returned by the fget function of this property. the allowUnpickable parameter is ignored'''
if isinstance(self.fget, string_types): # fget is not a method but a method name...
result = getattr(self._obj, self.fget)()
else:
result = self.fget()
return result
def applyConfig(self, value, depth=-1):
'''calls the fset function for this property with the given value. The depth parameter is ignored'''
if isinstance(self.fget, string_types): # fget is not a method but a method name...
getattr(self._obj, self.fset)(value)
else:
self.fset(value)
def objectName(self):
'''returns the name of this property'''
return self.name
class BaseConfigurableClass(object):
'''
A base class defining the API for configurable objects.
.. note:: One implicit requisite is that a configurable object must also provide a
`meth:`objectName` method which returns the object name. This is typically
fulfilled by inheriting from QObject.
Using objects that inherit from :class:`BaseConfigurableClass` automates
saving and restoring of application settings and also enables the use of
perspectives in Taurus GUIs.
The basic idea is that each object/widget in your application is responsible
for providing a dictionary containing information on its properties (see
:meth:`createConfig`). The same object/widget is also responsible for
restoring such properties when provided with a configuration dictionary (see
:meth:`applyConfig`).
For a certain property to be saved/restored it is usually enough to
*register* it using :meth:`registerConfigProperty`. When the objects are
structured in a hierarchical way (e.g. as the widgets in a Qt application),
the parent widget can (should) delegate the save/restore of its children to
the children themselves. This delegation is done by registering the children
using :meth:`registerConfigDelegate`.
Consider the following example: I am creating a groupbox container which
contains a :class:`TaurusForm` and I want to save/restore the state of the
checkbox and the properties of the form::
#The class looks like this:
class MyBox(Qt.QGroupBox, BaseConfigurableClass):
def __init__(self):
...
self.form = TaurusForm()
...
self.registerConfigProperty(self.isChecked, self.setChecked, 'checked')
self.registerConfigDelegate(self.form) #the TaurusForm already handles its own configuration!
...
#and we can retrieve the configuration doing:
b1 = MyBox()
b1.setChecked(True) #checked is a registered property of MyBox class
b1.form.setModifiableByUser(True) #modifiableByUser is a registered property of a TaurusForm
cfg = b1.createConfig() #we get the configuration as a dictionary
...
b2 = MyBox()
b2.applyConfig(cfg) #now b2 has the same configuration as b1 when cfg was created
:meth:`createConfig` and :meth:`applyConfig` methods use a dictionary for
passing the configuration, but :class:`BaseConfigurableClass` also provides
some other convenience methods for working with files
(:meth:`saveConfigFile` and :meth:`loadConfigFile`) or as QByteArrays
(:meth:`createQConfig` and :meth:`applyQConfig`)
Finally, we recommend to use :class:`TaurusMainWindow` for all Taurus GUIs
since it automates all the steps for *saving properties when closing* and
*restoring the settings on startup*. It also provides a mechanism for
implementing "perspectives" in your application.
'''
defaultConfigRecursionDepth = -1
# the latest element of this list is considered the current version
_supportedConfigVersions = ("__UNVERSIONED__",)
def __init__(self, **kwargs):
self.resetConfigurableItems()
@staticmethod
def isTaurusConfig(x):
'''Checks if the given argument has the structure of a configdict
:param x: (object) object to test
:return: (bool) True if it is a configdict, False otherwise.
'''
if not isinstance(x, dict):
return False
for k in ('__orderedConfigNames__', '__itemConfigurations__', 'ConfigVersion', '__pickable__'):
if k not in x:
return False
for k in x['__orderedConfigNames__']:
if k not in x['__itemConfigurations__']:
print('missing configuration for "%s" in %s' % (k, repr(x)))
return True
def createConfig(self, allowUnpickable=False):
'''
Returns a dictionary containing configuration information about the
current state of the object.
In most usual situations, using :meth:`registerConfigProperty` and
:meth:`registerConfigDelegate`, should be enough to cover all needs using
this method, although it can be reimplemented in children classes to support
very specific configurations.
By default, meth:`createQConfig` and meth:`saveConfigFile` call to this
method for obtaining the data.
Hint: The following code allows you to serialize the configuration
dictionary as a string (which you can store as a QSetting, or as a Tango
Attribute, provided that allowUnpickable==False)::
import pickle
s = pickle.dumps(widget.createConfig()) #s is a string that can be stored
:param alllowUnpickable: (bool) if False the returned dict is
guaranteed to be a pickable object. This is
the default and preferred option because it
allows the serialization as a string that can
be directly stored in a QSetting. If True, this
limitation is not enforced, which allows to
use more complex objects as values (but limits
its persistence).
:return: (dict) configurations (which can be loaded with :meth:`applyConfig`).
.. seealso: :meth:`applyConfig` , :meth:`registerConfigurableItem`, meth:`createQConfig`, meth:`saveConfigFile`
'''
configdict = {"ConfigVersion": self._supportedConfigVersions[-1],
"__pickable__": True}
# store the configurations for all registered configurable items as
# well
itemcfgs = {}
for k, v in self.__configurableItems.items():
itemcfgs[k] = v.createConfig(allowUnpickable=allowUnpickable)
configdict["__itemConfigurations__"] = itemcfgs
configdict["__orderedConfigNames__"] = self.__configurableItemNames
return configdict
def applyConfig(self, configdict, depth=None):
"""applies the settings stored in a configdict to the current object.
In most usual situations, using :meth:`registerConfigProperty` and
:meth:`registerConfigDelegate`, should be enough to cover all needs using
this method, although it can be reimplemented in children classes to support
very specific configurations.
:param configdict: (dict)
:param depth: (int) If depth = 0, applyConfig will only be called
for this object, and not for any other object registered
via :meth:`registerConfigurableItem`. If depth > 0,
applyConfig will be called recursively as many times as
the depth value. If depth < 0 (default, see note), no
limit is imposed to recursion (i.e., it will recurse for
as deep as there are registered items).
.. note:: the default recursion depth can be tweaked in derived classes
by changing the class property `defaultConfigRecursionDepth`
.. seealso:: :meth:`createConfig`
"""
if depth is None:
depth = self.defaultConfigRecursionDepth
if not self.checkConfigVersion(configdict):
raise ValueError(
'the given configuration is of unsupported version')
# delegate restoring the configuration of any registered configurable
# item
if depth != 0:
itemcfgs = configdict["__itemConfigurations__"]
# we use the sorted item names that was stored in the configdict
for key in configdict["__orderedConfigNames__"]:
if key in self.__configurableItems:
self.__configurableItems[key].applyConfig(
itemcfgs[key], depth=depth - 1)
def getConfigurableItemNames(self):
'''returns an ordered list of the names of currently registered
configuration items (delegates and properties)
:return: (list)
'''
return self.__configurableItemNames
def resetConfigurableItems(self):
''' clears the record of configurable items depending of this object
.. seealso:: :meth:`registerConfigurableItem`
'''
self.__configurableItemNames = []
self.__configurableItems = {}
def registerConfigurableItem(self, item, name=None):
print("Deprecation WARNING: %s.registerConfigurableItem() has been deprecated. Use registerConfigDelegate() instead" % repr(self))
self._registerConfigurableItem(item, name=name)
def registerConfigDelegate(self, delegate, name=None):
'''
Registers the given object as a delegate for configuration.
Delegates are typically other objects inheriting from BaseConfigurableClass
(or at least they must provide the following methods:
- `createConfig` (as provided by, e.g., BaseConfigurableClass)
- `applyConfig` (as provided by, e.g., BaseConfigurableClass)
- `objectName` (as provided by, e.g., QObject)
:param delegate: (BaseConfigurableClass) The delegate object to be registered.
:param name: (str) The name to be used as a key for this item in the configuration
dictionary. If None given, the object name is used by default.
.. note:: the registration order will be used when restoring configurations
.. seealso:: :meth:`unregisterConfigurableItem`, :meth:`registerConfigProperty`, :meth:`createConfig`
'''
return self._registerConfigurableItem(delegate, name=name)
def registerConfigProperty(self, fget, fset, name):
'''
Registers a certain property to be included in the config dictionary.
In this context a "property" is some named value that can be obtained
via a getter method and can be set via a setter method.
:param fget: (method or str) method (or name of a method) that gets no
arguments and returns the value of a property.
:param fset: (method or str) method (or name of a method) that gets as
an argument the value of a property, and sets it
:param name: (str) The name to be used as a key for this property in the configuration
dictionary
.. note:: the registration order will be used when restoring configurations
.. seealso:: :meth:`unregisterConfigurableItem`, :meth:`registerConfigDelegate`, :meth:`createConfig`
'''
if isinstance(fget, string_types) or isinstance(fset, string_types):
import weakref
obj = weakref.proxy(self)
else:
obj = None
p = configurableProperty(name, fget, fset, obj=obj)
return self._registerConfigurableItem(p, name=name)
def _registerConfigurableItem(self, item, name=None):
'''
Registers the given item as a configurable item which depends of this
Taurus widget.
.. note:: This method is not meant to be called directly. Use
:meth:`registerConfigProperty`, :meth:`registerConfigDelegate`
instead
Registered items are expected to implement the
following methods:
- `createConfig` (as provided by, e.g., BaseConfigurableClass)
- `applyConfig` (as provided by, e.g., BaseConfigurableClass)
- `objectName` (as provided by, e.g., QObject)
:param item: (object) The object that should be registered.
:param name: (str) The name to be used as a key for this item in the configuration
dictionary. If None given, the object name is used by default.
.. note:: the registration order will be used when restoring configurations
.. seealso:: :meth:`unregisterConfigurableItem`, :meth:`createConfig`
'''
if name is None:
name = item.objectName()
name = str(name)
if name in self.__configurableItemNames:
# abort if duplicated names
raise ValueError(
'_registerConfigurableItem: An object with name "%s" is already registered' % name)
self.__configurableItemNames.append(name)
self.__configurableItems[name] = item
def unregisterConfigurableItem(self, item, raiseOnError=True):
'''
unregisters the given item (either a delegate or a property) from the
configurable items record. It raises an exception if the item is not
registered
:param item: (object or str) The object that should be unregistered.
Alternatively, the name under which the object was registered
can be passed as a python string.
:param raiseOnError: (bool) If True (default), it raises a KeyError
exception if item was not registered. If False, it
just logs a debug message
.. seealso:: :meth:`registerConfigProperty`, :meth:`registerConfigDelegate`
'''
if isinstance(item, string_types):
name = str(item)
else:
name = str(item.objectName())
if name in self.__configurableItemNames and name in self.__configurableItems:
self.__configurableItemNames.remove(name)
self.__configurableItems.pop(name)
return True
elif raiseOnError:
raise KeyError('"%s" was not registered.' % name)
else:
self.debug('"%s" was not registered. Skipping' % name)
return False
def checkConfigVersion(self, configdict, showDialog=False, supportedVersions=None):
'''
Check if the version of configdict is supported. By default, the
BaseConfigurableClass objects have ["__UNVERSIONED__"] as their list of supported
versions, so unversioned config dicts will be accepted.
:param configdict: (dict) configuration dictionary to check
:param showDialog: (bool) whether to show a QtWarning dialog if check
failed (false by default)
:param supportedVersions: (sequence, or None) supported version
numbers, if None given, the versions supported
by this widget will be used (i.e., those
defined in self._supportedConfigVersions)
:return: (bool) returns True if the configdict is of the right version
'''
if supportedVersions is None:
supportedVersions = self._supportedConfigVersions
version = configdict.get("ConfigVersion", "__UNVERSIONED__")
if version not in supportedVersions:
msg = 'Unsupported Config Version %s. (Supported: %s)' % (
version, repr(supportedVersions))
self.warning(msg)
if showDialog:
from taurus.external.qt import Qt
Qt.QMessageBox.warning(
self, "Wrong Configuration Version", msg, Qt.QMessageBox.Ok)
return False
return True
def createQConfig(self):
'''
returns the current configuration status encoded as a QByteArray. This
state can therefore be easily stored using QSettings
:return: (QByteArray) (in the current implementation this is just a
pickled configdict encoded as a QByteArray
.. seealso:: :meth:`restoreQConfig`
'''
from taurus.external.qt import Qt
import pickle
configdict = self.createConfig(allowUnpickable=False)
return Qt.QByteArray(pickle.dumps(configdict))
def applyQConfig(self, qstate):
'''
restores the configuration from a qstate generated by :meth:`getQState`.
:param qstate: (QByteArray)
.. seealso:: :meth:`createQConfig`
'''
if qstate.isNull():
return
import pickle
configdict = pickle.loads(qstate.data())
self.applyConfig(configdict)
def saveConfigFile(self, ofile=None):
"""Stores the current configuration on a file
:param ofile: (file or string) file or filename to store the configuration
:return: (str) file name used
"""
import pickle
if ofile is None:
from taurus.external.qt import compat
ofile, _ = compat.getSaveFileName(
self, 'Save Configuration',
'%s.pck' % self.__class__.__name__,
'Configuration File (*.pck)'
)
if not ofile:
return
if isinstance(ofile, string_types):
ofile = open(ofile, 'wb')
configdict = self.createConfig(allowUnpickable=False)
self.info("Saving current settings in '%s'" % ofile.name)
pickle.dump(configdict, ofile)
return ofile.name
def loadConfigFile(self, ifile=None):
"""Reads a file stored by :meth:`saveConfig` and applies the settings
:param ifile: (file or string) file or filename from where to read the configuration
:return: (str) file name used
"""
import pickle
if ifile is None:
from taurus.external.qt import compat
ifile, _ = compat.getOpenFileName(
self, 'Load Configuration', '', 'Configuration File (*.pck)')
if not ifile:
return
if isinstance(ifile, string_types):
ifile = open(ifile, 'rb')
configdict = pickle.load(ifile)
self.applyConfig(configdict)
return ifile.name