# 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"

    class ConfigurationError(Exception):
        pass

    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, str
            ):  # 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, str
            ):  # 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'
                    )
                    # 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()

        @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

        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

        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"
                    )

        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

        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: 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)

        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

        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

        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

        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))

        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)

        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

        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