key is now always path and change opt by path dictionary storage
[tiramisu.git] / tiramisu / setting.py
index 7477fd9..0b87fbe 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 copy import copy
+from tiramisu.error import RequirementError, PropertiesOptionError
+from tiramisu.i18n import _
+
+
+default_encoding = 'utf-8'
+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')
+default_properties = ('expire', 'validator')
+storage_type = 'dictionary'
+
+
 class _const:
     """convenient class that emulates a module
     and builds constants (that is, unique names)"""
-    class ConstError(TypeError): pass
+    class ConstError(TypeError):
+        pass
 
     def __setattr__(self, name, value):
-        if self.__dict__.has_key(name):
-            raise self.ConstError, "Can't rebind group (%s)"%name
+        if name in self.__dict__:
+            raise self.ConstError, _("can't rebind group ({})").format(name)
         self.__dict__[name] = value
 
     def __delattr__(self, name):
-        if self.__dict__.has_key(name):
-            raise self.ConstError, "Can't unbind group (%s)"%name
-        raise NameError, 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"
@@ -42,6 +62,7 @@ class GroupModule(_const):
         *normal* means : groups that are not master
         """
         pass
+
     class DefaultGroupType(GroupType):
         """groups that are default (typically 'default')"""
         pass
@@ -54,6 +75,7 @@ class GroupModule(_const):
 # setting.groups (emulates a module)
 groups = GroupModule()
 
+
 def populate_groups():
     "populates the available groups in the appropriate namespaces"
     groups.master = groups.MasterGroupType('master')
@@ -62,6 +84,8 @@ def populate_groups():
 
 # names are in the module now
 populate_groups()
+
+
 # ____________________________________________________________
 class OwnerModule(_const):
     """emulates a module to manage unique owner names.
@@ -72,12 +96,14 @@ class OwnerModule(_const):
         """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
 
@@ -85,7 +111,8 @@ def populate_owners():
     - 'default' is the config owner after init time
     """
     setattr(owners, 'default', owners.DefaultOwner('default'))
-    setattr(owners,'user', owners.Owner('user'))
+    setattr(owners, 'user', owners.Owner('user'))
+
     def add_owner(name):
         """
         :param name: the name of the new owner
@@ -96,142 +123,304 @@ def populate_owners():
 # names are in the module now
 populate_owners()
 
+
 class MultiTypeModule(_const):
+    "namespace for the master/slaves"
     class MultiType(str):
         pass
+
     class DefaultMultiType(MultiType):
         pass
+
     class MasterMultiType(MultiType):
         pass
+
     class SlaveMultiType(MultiType):
         pass
 
 multitypes = MultiTypeModule()
 
+
 def populate_multitypes():
+    "populates the master/slave namespace"
     setattr(multitypes, 'default', multitypes.DefaultMultiType('default'))
     setattr(multitypes, 'master', multitypes.MasterMultiType('master'))
     setattr(multitypes, 'slave', multitypes.SlaveMultiType('slave'))
 
 populate_multitypes()
 
+
+class Property(object):
+    "a property is responsible of the option's value access rules"
+    __slots__ = ('_setting', '_properties', '_opt', '_path')
+
+    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):
+        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._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(list(self._properties))
+
+
 #____________________________________________________________
-class Setting():
+class Settings(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
-    owner = owners.user
-    # in order to freeze everything, not **only** the frozen options
-    everything_frozen = False
-    # enables at build time to raise an exception if the option's name
-    # has the name of a config's method
-    valid_opt_names = True
+    __slots__ = ('context', '_owner', '_p_')
+
+    def __init__(self, context, storage):
+        """
+        initializer
+
+        :param context: the root config
+        :param storage: the storage type
+
+                        - dictionnary -> in memory
+                        - sqlite3 -> persistent
+        """
+        # generic owner
+        self._owner = owners.user
+        self.context = context
+        import_lib = 'tiramisu.storage.{0}.setting'.format(storage_type)
+        self._p_ = __import__(import_lib, globals(), locals(), ['Settings'],
+                              -1).Settings(storage)
+
     #____________________________________________________________
     # 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):
+        "enables the pythonic 'in' syntaxic sugar"
+        return propname in self._getproperties()
+
+    def __repr__(self):
+        return str(list(self._getproperties()))
+
+    def __getitem__(self, opt):
+        if opt is None:
+            path = None
+        else:
+            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):
-        return self.properties
+    def reset(self, opt=None, all_properties=False):
+        if all_properties and 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 None:
+                path = None
+            else:
+                path = self._get_opt_path(opt)
+            self._p_.reset_properties(path)
+        self.context.cfgimpl_reset_cache()
 
-    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 _getproperties(self, opt=None, path=None, is_apply_req=True):
+        if opt is None:
+            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 self._p_.hascache('property', path):
+                ntime = time()
+                is_cached, props = self._p_.getcache('property', path, ntime)
+                if is_cached:
+                    return props
+            if is_apply_req:
+                self.apply_requires(opt, path)
+            props = self._p_.getproperties(path, opt._properties)
+            if 'expire' in self:
+                if ntime is None:
+                    ntime = time()
+                self._p_.setcache('property', path, props, ntime + expires_time)
+        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._getproperties()).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)
-    #____________________________________________________________
-    def get_permissive(self):
-        return self.permissive
+        Property(self, self._getproperties()).remove(propname)
+
+    def _setproperties(self, properties, opt, path):
+        """save properties for specified opt
+        (never save properties if same has option properties)
+        """
+        if opt is None:
+            self._p_.setproperties(path, properties)
+        else:
+            if set(opt._properties) == properties:
+                self._p_.reset_properties(path)
+            else:
+                self._p_.setproperties(path, properties)
+        self.context.cfgimpl_reset_cache()
 
-    def set_permissive(self, permissive):
-        if not isinstance(permissive, list):
-            raise TypeError('permissive must be a list')
-        self.permissive = permissive
     #____________________________________________________________
-    # complete freeze methods
-    def freeze_everything(self):
-        """everything is frozen, not only the option that are tagged "frozen"
+    def validate_properties(self, opt_or_descr, is_descr, is_write, path,
+                            value=None, force_permissive=False,
+                            force_properties=None):
         """
-        self.everything_frozen = True
+        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 there
 
-    def un_freeze_everything(self):
-        """everything is frozen, not only the option that are tagged "frozen"
+        :param is_write: in the validation process, an option is to be modified,
+                         the behavior can be different (typically with the `frozen`
+                         property)
         """
-        self.everything_frozen = False
+        # 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, path=None):
+        if not isinstance(permissive, tuple):
+            raise TypeError(_('permissive must be a tuple'))
+        self._p_.setpermissive(path, permissive)
 
-    def is_frozen_for_everything(self):
-        """frozen for a whole config (not only the options
-        that have been set to frozen)"""
-        return self.everything_frozen
     #____________________________________________________________
+    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_everything()
-        self.freeze() # can be usefull...
-        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.un_freeze_everything()
-        self.freeze()
-        self.enable_property('hidden')
-        self.enable_property('disabled')
-        self.mandatory = False
-        self.validator = 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
+        self._read(rw_remove, rw_append)
 
-    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 reset_cache(self, only_expired):
+        if only_expired:
+            self._p_.reset_expired_cache('property', time())
+        else:
+            self._p_.reset_all_cache('property')
 
-    def getowner(self):
-        return self.owner
+    def apply_requires(self, opt, path):
+        "carries out the jit (just in time requirements between options"
+        if opt._requires is None:
+            return
+
+        # filters the callbacks
+        setting = Property(self, self._getproperties(opt, path, False), opt, path=path)
+        descr = self.context.cfgimpl_get_description()
+        for requires in opt._requires:
+            matches = False
+            for require in requires:
+                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 = self.context._getattr(reqpath, force_permissive=True)
+                except PropertiesOptionError, err:
+                    if not transitive:
+                        continue
+                    properties = err.proptype
+                    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
+                except AttributeError:
+                    raise AttributeError(_("required option not found: "
+                                           "{0}").format(reqpath))
+                if (not inverse and
+                        value in expected or
+                        inverse and value not in expected):
+                    matches = True
+                    setting.append(action)
+                    # the calculation cannot be carried out
+                    break
+            # no requirement has been triggered, then just reverse the action
+            if not matches:
+                setting.remove(action)
+
+    def _get_opt_path(self, opt):
+        return self.context.cfgimpl_get_description().impl_get_path_by_opt(opt)