diff options
Diffstat (limited to 'lib/taurus/qt/qtcore/configuration/configuration.py')
-rw-r--r-- | lib/taurus/qt/qtcore/configuration/configuration.py | 439 |
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 |