summaryrefslogtreecommitdiff
path: root/lib/taurus/qt/qtcore/configuration/configuration.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/taurus/qt/qtcore/configuration/configuration.py')
-rw-r--r--lib/taurus/qt/qtcore/configuration/configuration.py439
1 files changed, 439 insertions, 0 deletions
diff --git a/lib/taurus/qt/qtcore/configuration/configuration.py b/lib/taurus/qt/qtcore/configuration/configuration.py
new file mode 100644
index 00000000..0227f35b
--- /dev/null
+++ b/lib/taurus/qt/qtcore/configuration/configuration.py
@@ -0,0 +1,439 @@
+#!/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"""
+
+__all__ = ["configurableProperty", "BaseConfigurableClass"]
+
+__docformat__ = 'restructuredtext'
+
+class configurableProperty:
+ '''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, basestring):# 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, basestring):# 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:
+ '''
+ 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
+ _supportedConfigVersions = ("__UNVERSIONED__",)#the latest element of this list is considered the current version
+
+ def __init__(self):
+ 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<str,object>) 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.iteritems():
+ 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__"]
+ for key in configdict["__orderedConfigNames__"]: #we use the sorted item names that was stored in the configdict
+ 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<unicode>)
+ '''
+ 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,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: (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:
+ raise ValueError('_registerConfigurableItem: An object with name "%s" is already registered'%name) #abort if duplicated names
+ 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,basestring): 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<str>, 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 cPickle as 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 cPickle as 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 cPickle as pickle
+ if ofile is None:
+ from taurus.external.qt import Qt
+ ofile = unicode(Qt.QFileDialog.getSaveFileName( self, 'Save Configuration', '%s.pck'%self.__class__.__name__, 'Configuration File (*.pck)'))
+ if not ofile: return
+ if not isinstance(ofile,file): ofile=open(ofile,'w')
+ 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 cPickle as pickle
+ if ifile is None:
+ from taurus.external.qt import Qt
+ ifile = unicode(Qt.QFileDialog.getOpenFileName( self, 'Load Configuration', '', 'Configuration File (*.pck)'))
+ if not ifile: return
+ if not isinstance(ifile,file): ifile=open(ifile,'r')
+ configdict=pickle.load(ifile)
+ self.applyConfig(configdict)
+ return ifile.name
+ \ No newline at end of file