add option name's validation and rename Option method with objimpl_
[tiramisu.git] / tiramisu / setting.py
index 90918f0..de698e9 100644 (file)
 # 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 tiramisu.error import RequirementRecursionError, PropertiesOptionError
+from tiramisu.i18n import _
+
+
+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:
@@ -30,13 +40,13 @@ class _const:
 
     def __setattr__(self, name, value):
         if name in self.__dict__:
-            raise self.ConstError, "Can't rebind group ({})".format(name)
+            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 NameError(name)
+            raise self.ConstError, _("Can't unbind group ({})").format(name)
+        raise ValueError(name)
 
 
 # ____________________________________________________________
@@ -133,113 +143,247 @@ def populate_multitypes():
 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):
+        if not propname in self._properties:
+            self._properties.append(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
+
+
 #____________________________________________________________
 class Setting(object):
     "``Config()``'s configuration options"
-    __slots__ = ('properties', 'permissives', 'owner')
+    __slots__ = ('context', '_properties', '_permissives', '_owner', '_cache')
 
-    def __init__(self):
+    def __init__(self, context):
         # properties attribute: the name of a property enables this property
         # key is None for global properties
-        self.properties = {None: []}  # ['hidden', 'disabled', 'mandatory', 'frozen', 'validator']}
+        self._properties = {None: ['expire']}
         # permissive properties
-        self.permissives = {}
+        self._permissives = {}
         # generic owner
-        self.owner = owners.user
+        self._owner = owners.user
+        self.context = context
+        self._cache = {}
 
     #____________________________________________________________
     # properties methods
-    def has_properties(self, opt=None):
-        "has properties means the Config's properties attribute is not empty"
-        return bool(len(self.get_properties(opt)))
+    def __contains__(self, propname):
+        return propname in self._get_properties()
 
-    def get_properties(self, opt=None):
+    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 _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
         if opt is None:
             default = []
         else:
+            if is_apply_req:
+                apply_requires(opt, self.context)
             default = list(opt._properties)
-        return self.properties.get(opt, default)
+        props = self._properties.get(opt, default)
+        if opt is not None:
+            self._set_cache(opt, props)
+        return props
 
-    def has_property(self, propname, opt=None):
-        """has property propname in the Config's properties attribute
-        :param property: string wich is the name of the property"""
-        return propname in self.get_properties(opt)
-
-    def enable_property(self, propname):
+    def append(self, propname):
         "puts property propname in the Config's properties attribute"
-        props = self.get_properties()
-        if propname not in props:
-            props.append(propname)
-        self.set_properties(props)
+        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"
-        props = self.get_properties()
-        if propname in props:
-            props.remove(propname)
-        self.set_properties(props)
+        Property(self, self._get_properties()).remove(propname)
 
-    def set_properties(self, properties, opt=None):
+    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
+            self._properties[opt] = properties
         else:
             if opt._properties == properties:
-                if opt in self.properties:
-                    del(self.properties[opt])
+                if opt in self._properties:
+                    del(self._properties[opt])
             else:
-                self.properties[opt] = properties
-
-    def add_property(self, propname, opt):
-        properties = self.get_properties(opt)
-        if not propname in properties:
-            properties.append(propname)
-            self.set_properties(properties, opt)
-
-    def del_property(self, propname, opt):
-        properties = self.get_properties(opt)
-        if propname in properties:
-            properties.remove(propname)
-            self.set_properties(properties, opt)
+                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)
 
     #____________________________________________________________
-    def get_permissive(self, config=None):
-        return self.permissives.get(config, [])
-
-    def set_permissive(self, permissive, config=None):
-        if not isinstance(permissive, list):
-            raise TypeError('permissive must be a list')
-        self.permissives[config] = permissive
+    def validate_properties(self, opt_or_descr, is_descr, is_write,
+                            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 to {0} for '
+                               'option {1} 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):
+        if not isinstance(permissive, tuple):
+            raise TypeError(_('permissive must be a tuple'))
+        self._permissives[opt] = 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
+            raise TypeError(_("invalid generic owner {0}").format(str(owner)))
+        self._owner = owner
 
     def getowner(self):
-        return self.owner
+        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.enable_property('everything_frozen')
-        self.enable_property('frozen')  # can be usefull...
-        self.disable_property('hidden')
-        self.enable_property('disabled')
-        self.enable_property('mandatory')
-        self.enable_property('validator')
-        self.disable_property('permissive')
+        self._read(ro_remove, ro_append)
 
     def read_write(self):
         "convenience method to freeze, hidde and disable"
-        self.disable_property('everything_frozen')
-        self.enable_property('frozen')  # can be usefull...
-        self.enable_property('hidden')
-        self.enable_property('disabled')
-        self.disable_property('mandatory')
-        self.disable_property('validator')
-        self.disable_property('permissive')
+        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])
+        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:
+        # 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().objimpl_get_path_by_opt(opt)
+        for requires in trigger_actions.values():
+            matches = False
+            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))
+                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))
+                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)