#!/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 <http://www.gnu.org/licenses/>.
#
# ###########################################################################
"""This module provides the set of base classes designed to provide
configuration features to the classes that inherit from them
"""
__docformat__ = "restructuredtext"
[docs]
class ConfigurationError(Exception):
pass
[docs]
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
[docs]
def createConfig(self, allowUnpickable=False):
"""returns value returned by the fget function of this property. the
allowUnpickable parameter is ignored
"""
if isinstance(
self.fget, str
): # fget is not a method but a method name...
result = getattr(self._obj, self.fget)()
else:
result = self.fget()
return result
[docs]
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, str
): # fget is not a method but a method name...
getattr(self._obj, self.fset)(value)
else:
self.fset(value)
[docs]
def objectName(self):
"""returns the name of this property"""
return self.name
[docs]
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'
)
# the TaurusForm already handles its own configuration!
self.registerConfigDelegate(self.form)
...
# and we can retrieve the configuration doing:
b1 = MyBox()
# checked is a registered property of MyBox class
b1.setChecked(True)
# modifiableByUser is a registered property of a TaurusForm
b1.form.setModifiableByUser(True)
# we get the configuration as a dictionary
cfg = b1.createConfig()
...
b2 = MyBox()
# now b2 has the same configuration as b1 when cfg was created
b2.applyConfig(cfg)
: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 = tuple()
def __init__(self, **kwargs):
self.resetConfigurableItems()
[docs]
@staticmethod
def isTaurusConfig(x):
"""Checks if the given argument has the structure of a configdict
:param x: object to test
:type x: object
:return: True if it is a configdict, False otherwise.
:rtype: bool
"""
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
[docs]
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())
:param alllowUnpickable: 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).
:type alllowUnpickable: bool
:return: configurations (which can be loaded with :meth:`applyConfig`).
:rtype: dict<str,object>
.. seealso: :meth:`applyConfig` , :meth:`registerConfigurableItem`,
meth:`createQConfig`, meth:`saveConfigFile`
"""
if len(self._supportedConfigVersions) > 0:
version = self._supportedConfigVersions[-1]
else:
version = "{}.0".format(self.__class__.__name__)
configdict = {
"ConfigVersion": version,
"__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
[docs]
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:
:type configdict: dict
:param depth: 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).
:type depth: int
.. 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 ConfigurationError("configuration version not supported")
# delegate restoring the configuration of any registered configurable
# item
if depth != 0:
apply_config_failed = False
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:
try:
self.__configurableItems[key].applyConfig(
itemcfgs[key], depth=depth - 1
)
except Exception:
self.warning(
"can not apply configuration of {}".format(key),
exc_info=True,
)
apply_config_failed = True
if apply_config_failed:
raise ConfigurationError(
"can not apply configuration of some of configurable item"
)
[docs]
def getConfigurableItemNames(self):
"""returns an ordered list of the names of currently registered
configuration items (delegates and properties)
:return:
:rtype: list<unicode>
"""
return self.__configurableItemNames
[docs]
def resetConfigurableItems(self):
"""clears the record of configurable items depending of this object
.. seealso:: :meth:`registerConfigurableItem`
"""
self.__configurableItemNames = []
self.__configurableItems = {}
[docs]
def registerConfigurableItem(self, item, name=None):
print(
(
"Deprecation WARNING: %s.registerConfigurableItem() "
+ "has been deprecated. Use registerConfigDelegate() instead"
)
% repr(self)
)
self._registerConfigurableItem(item, name=name)
[docs]
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: The delegate object to be registered.
:type delegate: BaseConfigurableClass
:param name: 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.
:type name: str
.. note:: the registration order will be used when restoring
configurations
.. seealso:: :meth:`unregisterConfigurableItem`,
:meth:`registerConfigProperty`, :meth:`createConfig`
"""
return self._registerConfigurableItem(delegate, name=name)
[docs]
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 name of a method) that gets no arguments and
returns the value of a property.
:type fget: method or str
:param fset: method (or name of a method) that gets as an argument the
value of a property, and sets it
:type fset: method or str
:param name: The name to be used as a key for this property in the
configuration dictionary
:type name: str
.. note:: the registration order will be used when
restoring configurations
.. seealso:: :meth:`unregisterConfigurableItem`,
:meth:`registerConfigDelegate`, :meth:`createConfig`
"""
if isinstance(fget, str) or isinstance(fset, str):
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: The object that should be registered.
:type item: object
:param name: 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.
:type name: str
.. 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
[docs]
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: The object that should be unregistered. Alternatively, the
name under which the object was registered can be passed as a
python string.
:type item: object or str
:param raiseOnError: If True (default), it raises a KeyError exception
if item was not registered. If False, it just logs a debug message
:type raiseOnError: bool
.. seealso:: :meth:`registerConfigProperty`,
:meth:`registerConfigDelegate`
"""
if isinstance(item, str):
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
[docs]
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: configuration dictionary to check
:type configdict: dict
:param showDialog: whether to show a QtWarning dialog if check failed
(false by default)
:type showDialog: bool
:param supportedVersions: supported version numbers, if None given, the
versions supported by this widget will be used (i.e., those defined
in self._supportedConfigVersions)
:type supportedVersions: sequence<str>, or None
:return: returns True if the configdict is of the right version
:rtype: bool
"""
if supportedVersions is None:
supportedVersions = self._supportedConfigVersions
if len(supportedVersions) == 0:
supportedVersions = ["{}.0".format(self.__class__.__name__)]
version = configdict.get("ConfigVersion", None)
if version == "__UNVERSIONED__":
msg = (
"Deprecated Config Version __UNVERSIONED__. "
+ "Will be auto-fixed when saving settings."
)
try:
self.deprecated(msg=msg)
except AttributeError:
from taurus import deprecated
msg += " Class using Deprecated Config Version: {}.".format(
self.__class__.__name__
)
deprecated(msg=msg)
return True
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
[docs]
def createQConfig(self):
"""
returns the current configuration status encoded as a QByteArray. This
state can therefore be easily stored using QSettings
:return: (in the current implementation this is just a pickled
configdict encoded as a QByteArray
:rtype: QByteArray
.. seealso:: :meth:`restoreQConfig`
"""
from taurus.external.qt import Qt
import pickle
configdict = self.createConfig(allowUnpickable=False)
return Qt.QByteArray(pickle.dumps(configdict))
[docs]
def applyQConfig(self, qstate):
"""
restores the configuration from a qstate generated by
:meth:`getQState`.
:param qstate:
:type qstate: QByteArray
.. seealso:: :meth:`createQConfig`
"""
if qstate.isNull():
return
import pickle
configdict = pickle.loads(qstate.data())
self.applyConfig(configdict)
[docs]
def saveConfigFile(self, ofile=None):
"""Stores the current configuration on a file
:param ofile: file or filename to store the configuration
:type ofile: file or string
:return: file name used
:rtype: str
"""
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
self.info(
"Saving current settings in '%s'", getattr(ofile, "name", ofile)
)
configdict = self.createConfig(allowUnpickable=False)
if isinstance(ofile, str):
with open(ofile, "wb") as ofile:
pickle.dump(configdict, ofile)
else:
pickle.dump(configdict, ofile)
return ofile.name
[docs]
def loadConfigFile(self, ifile=None):
"""Reads a file stored by :meth:`saveConfig` and applies the settings
:param ifile: file or filename from where to read the configuration
:type ifile: file or string
:return: file name used
:rtype: str
"""
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, str):
with open(ifile, "rb") as ifile:
configdict = pickle.load(ifile)
else:
configdict = pickle.load(ifile)
self.applyConfig(configdict)
return ifile.name