* config herite from BaseInformation class
[tiramisu.git] / tiramisu / setting.py
index c2140a9..9c3c313 100644 (file)
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 "sets the options of the configuration objects Config object itself"
-# Copyright (C) 2012 Team tiramisu (see AUTHORS for all contributors)
+# Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
 # the whole pypy projet is under MIT licence
 # ____________________________________________________________
+from time import time
+from copy import copy
+from tiramisu.error import RequirementRecursionError, PropertiesOptionError
+from tiramisu.i18n import _
 
-# available group_type values
-_group_name = ('default', 'family', 'group')
-groups_has_master = ('group', )
-class Group(str): pass
-group_types = tuple(Group(i) for i in _group_name)
 
-class 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')
+
+
+class _const:
+    """convenient class that emulates a module
+    and builds constants (that is, unique names)"""
+    class ConstError(TypeError):
+        pass
+
+    def __setattr__(self, name, value):
+        if name in self.__dict__:
+            raise self.ConstError, _("Can't rebind group ({})").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 ValueError(name)
+
+
+# ____________________________________________________________
+class GroupModule(_const):
+    "emulates a module to manage unique group (OptionDescription) names"
+    class GroupType(str):
+        """allowed normal group (OptionDescription) names
+        *normal* means : groups that are not master
+        """
+        pass
+
+    class DefaultGroupType(GroupType):
+        """groups that are default (typically 'default')"""
+        pass
+
+    class MasterGroupType(GroupType):
+        """allowed normal group (OptionDescription) names
+        *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):
+    """emulates a module to manage unique owner names.
+
+    owners are living in `Config._cfgimpl_value_owners`
+    """
+    class Owner(str):
+        """allowed owner names
+        """
+        pass
+
+    class DefaultOwner(Owner):
+        """groups that are default (typically 'default')"""
+        pass
+# setting.owners (emulates a module)
+owners = OwnerModule()
+
+
+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
+    """
+    setattr(owners, 'default', owners.DefaultOwner('default'))
+    setattr(owners, 'user', owners.Owner('user'))
+
+    def add_owner(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()
+
+
+class MultiTypeModule(_const):
+    class MultiType(str):
+        pass
+
+    class DefaultMultiType(MultiType):
+        pass
+
+    class MasterMultiType(MultiType):
+        pass
+
+    class SlaveMultiType(MultiType):
+        pass
+
+multitypes = MultiTypeModule()
+
+
+def populate_multitypes():
+    setattr(multitypes, 'default', multitypes.DefaultMultiType('default'))
+    setattr(multitypes, 'master', multitypes.MasterMultiType('master'))
+    setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave'))
+
+populate_multitypes()
+
+
+class Property(object):
+    __slots__ = ('_setting', '_properties', '_opt')
+
+    def __init__(self, setting, prop, opt=None):
+        self._opt = opt
+        self._setting = setting
+        self._properties = prop
+
+    def append(self, propname):
+        self._properties.add(propname)
+        self._setting._set_properties(self._properties, self._opt)
+        self._setting.context.cfgimpl_reset_cache()
+
+    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()
+
+    def __contains__(self, propname):
+        return propname in self._properties
+
+    def __repr__(self):
+        return str(list(self._properties))
+
+
+#____________________________________________________________
+class Setting(object):
     "``Config()``'s configuration options"
-    # properties attribute: the name of a property enables this property
-    properties = ['hidden', 'disabled']
-    # overrides the validations in the acces of the option values
-    permissive = []
-    # a mandatory option must have a value that is not None
-    mandatory = True
-    frozen = True
-    # enables validation function for options if set
-    validator = False
-    # generic owner. 'default' is the general config owner after init time
-    owner = 'user'
-    # ____________________________________________________________
+    __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: set(('expire',))}
+        # permissive properties
+        self._permissives = {}
+        # generic owner
+        self._owner = owners.user
+        self.context = context
+        self._cache = {}
+
+    #____________________________________________________________
     # properties methods
-    def has_properties(self):
-        "has properties means the Config's properties attribute is not empty"
-        return bool(len(self.properties))
+    def __contains__(self, propname):
+        return propname in self._get_properties()
+
+    def __repr__(self):
+        return str(list(self._get_properties()))
+
+    def __getitem__(self, opt):
+        return Property(self, self._get_properties(opt), opt)
+
+    def __setitem__(self, opt, value):
+        raise ValueError('you must only append/remove properties')
 
-    def has_property(self, propname):
-        """has property propname in the Config's properties attribute
-        :param property: string wich is the name of the property"""
-        return propname in self.properties
+    def _get_properties(self, opt=None, is_apply_req=True):
+        if opt is None:
+            props = self._properties.get(opt, set())
+        else:
+            exp = None
+            if opt in self._cache:
+                exp = time()
+                props, created = self._cache[opt]
+                if exp < created:
+                    return props
+            if is_apply_req:
+                apply_requires(opt, self.context)
+            props = self._properties.get(opt, opt._properties)
+            self._set_cache(opt, props, exp)
+        return props
 
-    def enable_property(self, propname):
+    def append(self, propname):
         "puts property propname in the Config's properties attribute"
-        if propname not in self.properties:
-            self.properties.append(propname)
+        Property(self, self._get_properties()).append(propname)
 
-    def disable_property(self, propname):
+    def remove(self, propname):
         "deletes property propname in the Config's properties attribute"
-        if self.has_property(propname):
-            self.properties.remove(propname)
+        Property(self, self._get_properties()).remove(propname)
 
-    def set_permissive(self, permissive):
-        if not isinstance(permissive, list):
-            raise TypeError('permissive must be a list')
-        self.permissive = permissive
+    def _set_properties(self, properties, opt=None):
+        """save properties for specified opt
+        (never save properties if same has option properties)
+        """
+        if opt is None:
+            self._properties[opt] = properties
+        else:
+            if opt._properties == properties:
+                if opt in self._properties:
+                    del(self._properties[opt])
+            else:
+                self._properties[opt] = properties
+
+    #____________________________________________________________
+    def validate_properties(self, opt_or_descr, is_descr, is_write,
+                            value=None, force_permissive=False,
+                            force_properties=None):
+        #opt properties
+        properties = copy(self._get_properties(opt_or_descr))
+        #remove opt permissive
+        properties -= self._get_permissive(opt_or_descr)
+        #remove global permissive if need
+        self_properties = copy(self._get_properties())
+        if force_permissive is True or 'permissive' in self_properties:
+            properties -= self._get_permissive()
+
+        #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()._is_empty(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')
+
+        if properties != frozenset():
+            if 'frozen' in properties:
+                raise_text = 'cannot change the value for option {0} this option is frozen'
+            else:
+                raise_text = "trying to access to an option named: {0} with properties {1}"
+            raise PropertiesOptionError(_(raise_text).format(opt_or_descr._name,
+                                        str(properties)),
+                                        list(properties))
+
+    def _get_permissive(self, opt=None):
+        return self._permissives.get(opt, frozenset())
+
+    def set_permissive(self, permissive, opt=None):
+        if not isinstance(permissive, tuple):
+            raise TypeError(_('permissive must be a tuple'))
+        self._permissives[opt] = frozenset(permissive)
+
+    #____________________________________________________________
+    def setowner(self, owner):
+        ":param owner: sets the default value for owner at the Config level"
+        if not isinstance(owner, owners.Owner):
+            raise TypeError(_("invalid generic owner {0}").format(str(owner)))
+        self._owner = owner
+
+    def getowner(self):
+        return self._owner
+
+    #____________________________________________________________
+    def _read(self, remove, append):
+        for prop in remove:
+            self.remove(prop)
+        for prop in append:
+            self.append(prop)
 
     def read_only(self):
         "convenience method to freeze, hidde and disable"
-        self.freeze()
-        self.disable_property('hidden')
-        self.enable_property('disabled')
-        self.mandatory = True
-        self.validator = True
+        self._read(ro_remove, ro_append)
 
     def read_write(self):
         "convenience method to freeze, hidde and disable"
-        self.freeze()
-        self.enable_property('hidden')
-        self.enable_property('disabled')
-        self.mandatory = False
-
-    def non_mandatory(self):
-        """mandatory at the Config level means that the Config raises an error
-        if a mandatory option is found"""
-        self.mandatory = False
-
-    def mandatory(self):
-        """mandatory at the Config level means that the Config raises an error
-        if a mandatory option is found"""
-        self.mandatory = True
-
-    def is_mandatory(self):
-        "all mandatory Options shall have a value"
-        return self.mandatory
-
-    def freeze(self):
-        "cannot modify the frozen `Option`'s"
-        self.frozen = True
-
-    def unfreeze(self):
-        "can modify the Options that are frozen"
-        self.frozen = False
-
-    def is_frozen(self):
-        "freeze flag at Config level"
-        return self.frozen
-
-    def set_owner(self, owner):
-        ":param owner: sets the default value for owner at the Config level"
-        self.owner = owner
+        self._read(rw_remove, rw_append)
+
+    def _set_cache(self, opt, props, exp):
+        if 'expire' in self:
+            if exp is None:
+                exp = time()
+            self._cache[opt] = (props, time() + expires_time)
+
+    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])
+        else:
+            self._cache.clear()
+
 
-# Setting is actually a singleton
-settings = Setting()
+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:
+        # filters the callbacks
+        settings = config.cfgimpl_get_settings()
+        setting = Property(settings, settings._get_properties(opt, False), opt)
+        trigger_actions = build_actions(opt._requires)
+        descr = config.cfgimpl_get_context().cfgimpl_get_description()
+        optpath = descr.impl_get_path_by_opt(opt)
+        for requires in trigger_actions.values():
+            matches = False
+            for require in requires:
+                if len(require) == 3:
+                    option, expected, action = require
+                    inverse = False
+                elif len(require) == 4:
+                    option, expected, action, inverse = require
+                path = descr.impl_get_path_by_opt(option)
+                if path == optpath or path.startswith(optpath + '.'):
+                    raise RequirementRecursionError(_("malformed requirements "
+                                                    "imbrication detected for option: '{0}' "
+                                                    "with requirement on: '{1}'").format(optpath, path))
+                try:
+                    value = config.cfgimpl_get_context()._getattr(path, force_permissive=True)
+                except PropertiesOptionError, err:
+                    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)