Source code for taurus.qt.qtcore.configuration.configuration

#!/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