comment tiramisu/setting.py
[tiramisu.git] / tiramisu / setting.py
index abeb4a7..1dba144 100644 (file)
 # the whole pypy projet is under MIT licence
 # ____________________________________________________________
 from time import time
-from tiramisu.error import RequirementRecursionError, PropertiesOptionError
+from copy import copy
+import weakref
+from tiramisu.error import (RequirementError, PropertiesOptionError,
+                            ConstError)
 from tiramisu.i18n import _
 
 
+"Default encoding for display a Config if raise UnicodeEncodeError"
+default_encoding = 'utf-8'
+
+"""If cache and expire is enable, time before cache is expired.
+This delay start first time value/setting is set in cache, even if
+user access several time to value/setting
+"""
 expires_time = 5
-ro_remove = ('permissive', 'hidden')
-ro_append = ('frozen', 'disabled', 'validator', 'everything_frozen', 'mandatory')
-rw_remove = ('permissive', 'everything_frozen', 'mandatory')
-rw_append = ('frozen', 'disabled', 'validator', 'hidden')
+"""List of default properties (you can add new one if needed).
+
+For common properties and personalise properties, if a propery is set for
+an Option and for the Config together, Setting raise a PropertiesOptionError
+
+* Common properties:
+
+hidden
+    option with this property can only get value in read only mode. This
+    option is not available in read write mode.
+
+disabled
+    option with this property cannot be set/get
+
+frozen
+    cannot set value for option with this properties if 'frozen' is set in
+    config
+
+mandatory
+    should set value for option with this properties if 'mandatory' is set in
+    config
+
+
+* Special property:
+
+permissive
+    option with 'permissive' cannot raise PropertiesOptionError for properties
+    set in permissive
+    config with 'permissive', whole option in this config cannot raise
+    PropertiesOptionError for properties set in permissive
+
+* Special Config properties:
+
+cache
+    if set, enable cache settings and values
+
+expire
+    if set, settings and values in cache expire after ``expires_time``
+
+everything_frozen
+    whole option in config are frozen (even if option have not frozen
+    property)
+
+validator
+    launch validator set by user in option (this property has no effect
+    for internal validator)
+"""
+default_properties = ('cache', 'expire', 'validator')
 
+"""Config can be in two defaut mode:
 
-class _const:
+read_only
+    you can get all variables not disabled but you cannot set any variables
+    if a value has a callback without any value, callback is launch and value
+    of this variable can change
+    you cannot access to mandatory variable without values
+
+read_write
+    you can get all variables not disabled and not hidden
+    you can set all variables not frozen
+"""
+ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen',
+                'mandatory'])
+ro_remove = set(['permissive', 'hidden'])
+rw_append = set(['frozen', 'disabled', 'validator', 'hidden'])
+rw_remove = set(['permissive', 'everything_frozen', 'mandatory'])
+
+
+# ____________________________________________________________
+class _NameSpace:
     """convenient class that emulates a module
-    and builds constants (that is, unique names)"""
-    class ConstError(TypeError):
-        pass
+    and builds constants (that is, unique names)
+    when attribute is added, we cannot delete it
+    """
 
     def __setattr__(self, name, value):
         if name in self.__dict__:
-            raise self.ConstError, _("Can't rebind group ({})").format(name)
+            raise ConstError(_("can't rebind {0}").format(name))
         self.__dict__[name] = value
 
     def __delattr__(self, name):
         if name in self.__dict__:
-            raise self.ConstError, _("Can't unbind group ({})").format(name)
+            raise ConstError(_("can't unbind {0}").format(name))
         raise ValueError(name)
 
 
-# ____________________________________________________________
-class GroupModule(_const):
+class GroupModule(_NameSpace):
     "emulates a module to manage unique group (OptionDescription) names"
     class GroupType(str):
         """allowed normal group (OptionDescription) names
@@ -67,22 +139,9 @@ class GroupModule(_const):
         *master* means : groups that have the 'master' attribute set
         """
         pass
-# setting.groups (emulates a module)
-groups = GroupModule()
-
-
-def populate_groups():
-    "populates the available groups in the appropriate namespaces"
-    groups.master = groups.MasterGroupType('master')
-    groups.default = groups.DefaultGroupType('default')
-    groups.family = groups.GroupType('family')
-
-# names are in the module now
-populate_groups()
 
 
-# ____________________________________________________________
-class OwnerModule(_const):
+class OwnerModule(_NameSpace):
     """emulates a module to manage unique owner names.
 
     owners are living in `Config._cfgimpl_value_owners`
@@ -95,212 +154,309 @@ class OwnerModule(_const):
     class DefaultOwner(Owner):
         """groups that are default (typically 'default')"""
         pass
-# setting.owners (emulates a module)
-owners = OwnerModule()
+
+
+class MultiTypeModule(_NameSpace):
+    "namespace for the master/slaves"
+    class MultiType(str):
+        pass
+
+    class DefaultMultiType(MultiType):
+        pass
+
+    class MasterMultiType(MultiType):
+        pass
+
+    class SlaveMultiType(MultiType):
+        pass
+
+
+# ____________________________________________________________
+def populate_groups():
+    """populates the available groups in the appropriate namespaces
+
+    groups.default
+        default group set when creating a new optiondescription
+
+    groups.master
+        master group is a special optiondescription, all suboptions should be
+        multi option and all values should have same length, to find master's
+        option, the optiondescription's name should be same than de master's
+        option
+
+    groups.family
+        example of group, no special behavior with this group's type
+    """
+    groups.default = groups.DefaultGroupType('default')
+    groups.master = groups.MasterGroupType('master')
+    groups.family = groups.GroupType('family')
 
 
 def populate_owners():
     """populates the available owners in the appropriate namespaces
 
-    - 'user' is the generic is the generic owner.
-    - 'default' is the config owner after init time
+    default
+        is the config owner after init time
+
+    user
+        is the generic is the generic owner
     """
     setattr(owners, 'default', owners.DefaultOwner('default'))
     setattr(owners, 'user', owners.Owner('user'))
 
-    def add_owner(name):
+    def addowner(name):
         """
         :param name: the name of the new owner
         """
         setattr(owners, name, owners.Owner(name))
-    setattr(owners, 'add_owner', add_owner)
-
-# names are in the module now
-populate_owners()
+    setattr(owners, 'addowner', addowner)
 
 
-class MultiTypeModule(_const):
-    class MultiType(str):
-        pass
-
-    class DefaultMultiType(MultiType):
-        pass
-
-    class MasterMultiType(MultiType):
-        pass
+def populate_multitypes():
+    """all multi option should have a type, this type is automaticly set do
+    not touch this
 
-    class SlaveMultiType(MultiType):
-        pass
+    default
+        default's multi option set if not master or slave
 
-multitypes = MultiTypeModule()
+    master
+        master's option in a group with master's type, name of this option
+        should be the same name of the optiondescription
 
+    slave
+        slave's option in a group with master's type
 
-def populate_multitypes():
+    """
     setattr(multitypes, 'default', multitypes.DefaultMultiType('default'))
     setattr(multitypes, 'master', multitypes.MasterMultiType('master'))
     setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave'))
 
+
+# ____________________________________________________________
+# populate groups, owners and multitypes with default attributes
+groups = GroupModule()
+populate_groups()
+owners = OwnerModule()
+populate_owners()
+multitypes = MultiTypeModule()
 populate_multitypes()
 
 
+# ____________________________________________________________
 class Property(object):
-    __slots__ = ('_setting', '_properties', '_opt')
+    "a property is responsible of the option's value access rules"
+    __slots__ = ('_setting', '_properties', '_opt', '_path')
 
-    def __init__(self, setting, prop, opt=None):
+    def __init__(self, setting, prop, opt=None, path=None):
         self._opt = opt
+        self._path = path
         self._setting = setting
         self._properties = prop
 
     def append(self, propname):
-        if not propname in self._properties:
-            self._properties.append(propname)
-            self._setting._set_properties(self._properties, self._opt)
-        self._setting.context.cfgimpl_reset_cache()
+        if self._opt is not None and self._opt._calc_properties is not None \
+                and propname in self._opt._calc_properties:
+            raise ValueError(_('cannot append {0} property for option {1}: '
+                               'this property is calculated').format(
+                                   propname, self._opt._name))
+        self._properties.add(propname)
+        self._setting._setproperties(self._properties, self._opt, self._path)
 
     def remove(self, propname):
         if propname in self._properties:
             self._properties.remove(propname)
-            self._setting._set_properties(self._properties, self._opt)
-        self._setting.context.cfgimpl_reset_cache()
+            self._setting._setproperties(self._properties, self._opt,
+                                         self._path)
+
+    def reset(self):
+        self._setting.reset(_path=self._path)
 
     def __contains__(self, propname):
         return propname in self._properties
 
     def __repr__(self):
-        return str(self._properties)
+        return str(list(self._properties))
 
 
 #____________________________________________________________
-class Setting(object):
+class Settings(object):
     "``Config()``'s configuration options"
-    __slots__ = ('context', '_properties', '_permissives', '_owner', '_cache')
-
-    def __init__(self, context):
-        # properties attribute: the name of a property enables this property
-        # key is None for global properties
-        self._properties = {None: ['expire']}
-        # permissive properties
-        self._permissives = {}
+    __slots__ = ('context', '_owner', '_p_', '__weakref__')
+
+    def __init__(self, context, storage):
+        """
+        initializer
+
+        :param context: the root config
+        :param storage: the storage type
+
+                        - dictionary -> in memory
+                        - sqlite3 -> persistent
+        """
         # generic owner
         self._owner = owners.user
-        self.context = context
-        self._cache = {}
+        self.context = weakref.ref(context)
+        self._p_ = storage
 
     #____________________________________________________________
     # properties methods
     def __contains__(self, propname):
-        return propname in self._get_properties()
+        "enables the pythonic 'in' syntaxic sugar"
+        return propname in self._getproperties()
 
     def __repr__(self):
-        return str(self._get_properties())
+        return str(list(self._getproperties()))
 
     def __getitem__(self, opt):
-        return Property(self, self._get_properties(opt), opt)
+        path = self._get_opt_path(opt)
+        return self._getitem(opt, path)
+
+    def _getitem(self, opt, path):
+        return Property(self, self._getproperties(opt, path), opt, path)
 
     def __setitem__(self, opt, value):
-        raise ValueError('you must only append/remove properties')
-
-    def _get_properties(self, opt=None, is_apply_req=True):
-        if opt is not None and opt in self._cache:
-            exp = time()
-            props, created = self._cache[opt]
-            if exp < created:
-                return props
+        raise ValueError('you should only append/remove properties')
+
+    def reset(self, opt=None, _path=None, all_properties=False):
+        if all_properties and (_path or opt):
+            raise ValueError(_('opt and all_properties must not be set '
+                               'together in reset'))
+        if all_properties:
+            self._p_.reset_all_propertives()
+        else:
+            if opt is not None and _path is None:
+                _path = self._get_opt_path(opt)
+            self._p_.reset_properties(_path)
+        self.context().cfgimpl_reset_cache()
+
+    def _getproperties(self, opt=None, path=None, is_apply_req=True):
         if opt is None:
-            default = []
+            props = self._p_.getproperties(path, default_properties)
         else:
+            if path is None:
+                raise ValueError(_('if opt is not None, path should not be'
+                                   ' None in _getproperties'))
+            ntime = None
+            if 'cache' in self and self._p_.hascache(path):
+                if 'expire' in self:
+                    ntime = int(time())
+                is_cached, props = self._p_.getcache(path, ntime)
+                if is_cached:
+                    return props
+            props = self._p_.getproperties(path, opt._properties)
             if is_apply_req:
-                apply_requires(opt, self.context)
-            default = list(opt._properties)
-        props = self._properties.get(opt, default)
-        if opt is not None:
-            self._set_cache(opt, props)
+                props |= self.apply_requires(opt, path)
+            if 'cache' in self:
+                if 'expire' in self:
+                    if  ntime is None:
+                        ntime = int(time())
+                    ntime = ntime + expires_time
+                self._p_.setcache(path, props, ntime)
         return props
 
     def append(self, propname):
         "puts property propname in the Config's properties attribute"
-        Property(self, self._get_properties()).append(propname)
+        props = self._p_.getproperties(None, default_properties)
+        props.add(propname)
+        self._setproperties(props, None, None)
 
     def remove(self, propname):
         "deletes property propname in the Config's properties attribute"
-        Property(self, self._get_properties()).remove(propname)
+        props = self._p_.getproperties(None, default_properties)
+        if propname in props:
+            props.remove(propname)
+            self._setproperties(props, None, None)
 
-    def _set_properties(self, properties, opt=None):
+    def _setproperties(self, properties, opt, path):
         """save properties for specified opt
         (never save properties if same has option properties)
         """
         if opt is None:
-            self._properties[opt] = properties
+            self._p_.setproperties(None, properties)
         else:
-            if opt._properties == properties:
-                if opt in self._properties:
-                    del(self._properties[opt])
+            if opt._calc_properties is not None:
+                properties -= opt._calc_properties
+            if set(opt._properties) == properties:
+                self._p_.reset_properties(path)
             else:
-                self._properties[opt] = properties
-
-    def _validate_frozen(self, opt, value, is_write):
-        if not is_write:
-            return False
-        if 'permissive' in self and 'frozen' in self._get_permissive():
-            return False
-        if 'everything_frozen' in self or (
-                'frozen' in self and 'frozen' in self[opt]):
-            return True
-        return False
-
-    def _validate_mandatory(self, opt, value, force_properties):
-        if 'permissive' in self and 'mandatory' in self._get_permissive():
-            return False
-        check_mandatory = 'mandatory' in self
-        if force_properties is not None:
-            check_mandatory = ('mandatory' in force_properties or
-                               check_mandatory)
-        if check_mandatory and 'mandatory' in self[opt] and \
-                self.context.cfgimpl_get_values()._is_empty(opt, value):
-            return True
-        return False
-
-    def _calc_properties(self, opt_or_descr, force_permissive, force_properties):
-        properties = set(self._get_properties(opt_or_descr))
-        #remove this properties, those properties are validate in after
-        properties = properties - set(['mandatory', 'frozen'])
-        set_properties = set(self._get_properties())
-        if force_properties is not None:
-            set_properties.update(set(force_properties))
-        properties = properties & set_properties
-        if force_permissive is True or 'permissive' in self:
-            properties = properties - set(self._get_permissive())
-        properties = properties - set(self._get_permissive(opt_or_descr))
-        return list(properties)
+                self._p_.setproperties(path, properties)
+        self.context().cfgimpl_reset_cache()
 
     #____________________________________________________________
-    def validate_properties(self, opt_or_descr, is_descr, is_write,
+    def validate_properties(self, opt_or_descr, is_descr, is_write, path,
                             value=None, force_permissive=False,
                             force_properties=None):
-        properties = self._calc_properties(opt_or_descr, force_permissive,
-                                           force_properties)
-        raise_text = _("trying to access"
-                       " to an option named: {0} with properties"
-                       " {1}")
-        if not is_descr:
-            if self._validate_mandatory(opt_or_descr, value, force_properties):
-                properties.append('mandatory')
-            if self._validate_frozen(opt_or_descr, value, is_write):
-                properties.append('frozen')
-                raise_text = _('cannot change the value for '
-                               'option {0} this option is frozen')
-        if properties != []:
-            raise PropertiesOptionError(raise_text.format(opt_or_descr._name,
-                                                          str(properties)),
-                                        properties)
-
-    def _get_permissive(self, opt=None):
-        return self._permissives.get(opt, [])
-
-    def set_permissive(self, permissive, opt=None):
+        """
+        validation upon the properties related to `opt_or_descr`
+
+        :param opt_or_descr: an option or an option description object
+        :param force_permissive: behaves as if the permissive property
+                                 was present
+        :param is_descr: we have to know if we are in an option description,
+                         just because the mandatory property
+                         doesn't exist here
+
+        :param is_write: in the validation process, an option is to be modified,
+                         the behavior can be different
+                         (typically with the `frozen` property)
+        """
+        # opt properties
+        properties = copy(self._getproperties(opt_or_descr, path))
+        # remove opt permissive
+        properties -= self._p_.getpermissive(path)
+        # remove global permissive if need
+        self_properties = copy(self._getproperties())
+        if force_permissive is True or 'permissive' in self_properties:
+            properties -= self._p_.getpermissive()
+
+        # global properties
+        if force_properties is not None:
+            self_properties.update(force_properties)
+
+        # calc properties
+        properties &= self_properties
+        # mandatory and frozen are special properties
+        if is_descr:
+            properties -= frozenset(('mandatory', 'frozen'))
+        else:
+            if 'mandatory' in properties and \
+                    not self.context().cfgimpl_get_values()._isempty(
+                        opt_or_descr, value):
+                properties.remove('mandatory')
+            if is_write and 'everything_frozen' in self_properties:
+                properties.add('frozen')
+            elif 'frozen' in properties and not is_write:
+                properties.remove('frozen')
+        # at this point an option should not remain in properties
+        if properties != frozenset():
+            props = list(properties)
+            if 'frozen' in properties:
+                raise PropertiesOptionError(_('cannot change the value for '
+                                              'option {0} this option is'
+                                              ' frozen').format(
+                                                  opt_or_descr._name),
+                                            props)
+            else:
+                raise PropertiesOptionError(_("trying to access to an option "
+                                              "named: {0} with properties {1}"
+                                              "").format(opt_or_descr._name,
+                                                         str(props)), props)
+
+    def setpermissive(self, permissive, opt=None, path=None):
+        """
+        enables us to put the permissives in the storage
+
+        :param path: the option's path
+        :param type: str
+        :param opt: if an option object is set, the path is extracted.
+                    it is better (faster) to set the path parameter
+                    instead of passing a :class:`tiramisu.option.Option()` object.
+        """
+        if opt is not None and path is None:
+            path = self._get_opt_path(opt)
         if not isinstance(permissive, tuple):
             raise TypeError(_('permissive must be a tuple'))
-        self._permissives[opt] = permissive
+        self._p_.setpermissive(path, permissive)
 
     #____________________________________________________________
     def setowner(self, owner):
@@ -320,76 +476,101 @@ class Setting(object):
             self.append(prop)
 
     def read_only(self):
-        "convenience method to freeze, hidde and disable"
+        "convenience method to freeze, hide and disable"
         self._read(ro_remove, ro_append)
 
     def read_write(self):
-        "convenience method to freeze, hidde and disable"
+        "convenience method to freeze, hide and disable"
         self._read(rw_remove, rw_append)
 
-    def _set_cache(self, opt, props):
-        if 'expire' in self:
-            self._cache[opt] = (props, time() + expires_time)
-            pass
-
     def reset_cache(self, only_expired):
         if only_expired:
-            exp = time()
-            keys = self._cache.keys()
-            for key in keys:
-                props, created = self._cache[key]
-                if exp > created:
-                    del(self._cache[key])
+            self._p_.reset_expired_cache(int(time()))
         else:
-            self._cache.clear()
-
-
-def apply_requires(opt, config):
-    "carries out the jit (just in time requirements between options"
-    def build_actions(requires):
-        "action are hide, show, enable, disable..."
-        trigger_actions = {}
-        for require in requires:
-            action = require[2]
-            trigger_actions.setdefault(action, []).append(require)
-        return trigger_actions
-    #for symlink
-    if hasattr(opt, '_requires') and opt._requires is not None:
+            self._p_.reset_all_cache()
+
+    def apply_requires(self, opt, path):
+        """carries out the jit (just in time) requirements between options
+
+        a requirement is a tuple of this form that comes from the option's
+        requirements validation::
+
+            (option, expected, action, inverse, transitive, same_action)
+
+        let's have a look at all the tuple's items:
+
+        - **option** is the target option's name or path
+
+        - **expected** is the target option's value that is going to trigger an action
+
+        - **action** is the (property) action to be accomplished if the target option
+          happens to have the expected value
+
+        - if **inverse** is `True` and if the target option's value does not
+          apply, then the property action must be removed from the option's
+          properties list (wich means that the property is inverted)
+
+        - **transitive**: but what happens if the target option cannot be
+          accessed ? We don't kown the target option's value. Actually if some
+          property in the target option is not present in the permissive, the
+          target option's value cannot be accessed. In this case, the
+          **action** have to be applied to the option. (the **action** property
+          is then added to the option).
+
+        - **same_action**: actually, if **same_action** is `True`, the
+          transitivity is not accomplished. The transitivity is accomplished
+          only if the target option **has the same property** that the demanded
+          action. If the target option's value is not accessible because of
+          another reason, because of a property of another type, then an
+          exception :exc:`~error.RequirementError` is raised.
+
+        And at last, if no target option matches the expected values, the
+        action must be removed from the option's properties list.
+
+        :param opt: the option on wich the requirement occurs
+        :type opt: `option.Option()`
+        :param path: the option's path in the config
+        :type path: str
+        """
+        if opt._requires is None:
+            return frozenset()
+
         # filters the callbacks
-        settings = config.cfgimpl_get_settings()
-        setting = Property(settings, settings._get_properties(opt, False), opt)
-        trigger_actions = build_actions(opt._requires)
-        optpath = config.cfgimpl_get_context().cfgimpl_get_description().optimpl_get_path_by_opt(opt)
-        for requires in trigger_actions.values():
-            matches = False
+        calc_properties = set()
+        for requires in opt._requires:
             for require in requires:
-                if len(require) == 3:
-                    path, expected, action = require
-                    inverse = False
-                elif len(require) == 4:
-                    path, expected, action, inverse = require
-                if path == optpath or path.startswith(optpath + '.'):
-                    raise RequirementRecursionError(_("malformed requirements "
-                                                    "imbrication detected for option: '{0}' "
-                                                    "with requirement on: '{1}'").format(optpath, path))
+                option, expected, action, inverse, \
+                    transitive, same_action = require
+                reqpath = self._get_opt_path(option)
+                if reqpath == path or reqpath.startswith(path + '.'):
+                    raise RequirementError(_("malformed requirements "
+                                             "imbrication detected for option:"
+                                             " '{0}' with requirement on: "
+                                             "'{1}'").format(path, reqpath))
                 try:
-                    value = config.cfgimpl_get_context()._getattr(path, force_permissive=True)
-                except PropertiesOptionError, err:
+                    value = self.context()._getattr(reqpath,
+                                                    force_permissive=True)
+                except PropertiesOptionError as err:
+                    if not transitive:
+                        continue
                     properties = err.proptype
-                    raise PropertiesOptionError(_("option '{0}' has requirement's property error: "
-                                                  "{1} {2}").format(opt._name, path, properties), properties)
-                except AttributeError:
-                    raise AttributeError(_("required option not found: "
-                                         "{0}").format(path))
-                if value == expected:
-                    if inverse:
-                        setting.remove(action)
-                    else:
-                        setting.append(action)
-                    matches = True
-            # no requirement has been triggered, then just reverse the action
-            if not matches:
-                if inverse:
-                    setting.append(action)
-                else:
-                    setting.remove(action)
+                    if same_action and action not in properties:
+                        raise RequirementError(_("option '{0}' has "
+                                                 "requirement's property "
+                                                 "error: "
+                                                 "{1} {2}").format(opt._name,
+                                                                   reqpath,
+                                                                   properties))
+                    # transitive action, force expected
+                    value = expected[0]
+                    inverse = False
+                if (not inverse and
+                        value in expected or
+                        inverse and value not in expected):
+                    calc_properties.add(action)
+                    # the calculation cannot be carried out
+                    break
+        return calc_properties
+
+    def _get_opt_path(self, opt):
+        return self.context().cfgimpl_get_description().impl_get_path_by_opt(opt)