Merge branch 'master' into orm
[tiramisu.git] / tiramisu / setting.py
index 836278f..fc4241d 100644 (file)
 # ____________________________________________________________
 from time import time
 from copy import copy
+import weakref
 from tiramisu.error import (RequirementError, PropertiesOptionError,
                             ConstError, ConfigError)
 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')
-default_properties = ('expire', 'validator')
+"""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:
 
-class StorageType:
-    default_storage = 'dictionary'
-    storage_type = None
+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
 
-    def set_storage(self, name):
-        if self.storage_type is not None:
-            raise ConfigError(_('storage_type is already set, cannot rebind it'))
-        self.storage_type = name
+* Special Config properties:
 
-    def get_storage(self):
-        if self.storage_type is None:
-            self.storage_type = self.default_storage
-        storage = self.storage_type
-        return 'tiramisu.storage.{0}.storage'.format(
-            storage)
+cache
+    if set, enable cache settings and values
 
+expire
+    if set, settings and values in cache expire after ``expires_time``
 
-storage_type = StorageType()
+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')
 
-class _NameSpace:
+"""Config can be in two defaut mode:
+
+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(object):
     """convenient class that emulates a module
-    and builds constants (that is, unique names)"""
+    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__:
@@ -72,7 +122,6 @@ class _NameSpace:
         raise ValueError(name)
 
 
-# ____________________________________________________________
 class GroupModule(_NameSpace):
     "emulates a module to manage unique group (OptionDescription) names"
     class GroupType(str):
@@ -90,21 +139,8 @@ class GroupModule(_NameSpace):
         *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(_NameSpace):
     """emulates a module to manage unique owner names.
 
@@ -118,28 +154,6 @@ class OwnerModule(_NameSpace):
     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(_NameSpace):
@@ -156,18 +170,87 @@ class MultiTypeModule(_NameSpace):
     class SlaveMultiType(MultiType):
         pass
 
-multitypes = MultiTypeModule()
+
+# ____________________________________________________________
+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
+
+    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 addowner(name):
+        """
+        :param name: the name of the new owner
+        """
+        setattr(owners, name, owners.Owner(name))
+    setattr(owners, 'addowner', addowner)
 
 
 def populate_multitypes():
-    "populates the master/slave namespace"
+    """all multi option should have a type, this type is automaticly set do
+    not touch this
+
+    default
+        default's multi option set if not master or slave
+
+    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
+
+    """
     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 Undefined():
+    pass
+
+
+undefined = Undefined()
+
+
+# ____________________________________________________________
 class Property(object):
     "a property is responsible of the option's value access rules"
     __slots__ = ('_setting', '_properties', '_opt', '_path')
@@ -179,20 +262,43 @@ class Property(object):
         self._properties = prop
 
     def append(self, propname):
-        if self._opt is not None and self._opt._calc_properties is not None \
+        """Appends a property named propname
+
+        :param propname: a predefined or user defined property name
+        :type propname: string
+        """
+        if self._opt is not None and self._opt.impl_getrequires() 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))
+                                   propname, self._opt.impl_getname()))
         self._properties.add(propname)
         self._setting._setproperties(self._properties, self._opt, self._path)
 
     def remove(self, propname):
+        """Removes a property named propname
+
+        :param propname: a predefined or user defined property name
+        :type propname: string
+        """
         if propname in self._properties:
             self._properties.remove(propname)
-            self._setting._setproperties(self._properties, self._opt, self._path)
+            self._setting._setproperties(self._properties, self._opt,
+                                         self._path)
+
+    def extend(self, propnames):
+        """Extends properties to the existing properties
+
+        :param propnames: an iterable made of property names
+        :type propnames: iterable of string
+        """
+        for propname in propnames:
+            self.append(propname)
 
     def reset(self):
+        """resets the properties (does not **clear** the properties,
+        default properties are still present)
+        """
         self._setting.reset(_path=self._path)
 
     def __contains__(self, propname):
@@ -202,44 +308,10 @@ class Property(object):
         return str(list(self._properties))
 
 
-def set_storage(name, **args):
-    storage_type.set_storage(name)
-    settings = __import__(storage_type.get_storage(), globals(), locals(),
-                          ['Setting'], -1).Setting()
-    for option, value in args.items():
-        try:
-            getattr(settings, option)
-            setattr(settings, option, value)
-        except AttributeError:
-            raise ValueError(_('option {0} not already exists in storage {1}'
-                               '').format(option, name))
-
-
-def get_storage(context, session_id, persistent):
-    def gen_id(config):
-        return str(id(config)) + str(time())
-
-    if session_id is None:
-        session_id = gen_id(context)
-    a=__import__(storage_type.get_storage(), globals(), locals(),
-                      ['Storage'], -1).Storage(session_id, persistent)
-    return a
-
-
-def list_sessions():
-    return __import__(storage_type.get_storage(), globals(), locals(),
-                      ['list_sessions'], -1).list_sessions()
-
-
-def delete_session(session_id):
-    return __import__(storage_type.get_storage(), globals(), locals(),
-                      ['delete_session'], -1).delete_session(session_id)
-
-
 #____________________________________________________________
 class Settings(object):
-    "``Config()``'s configuration options"
-    __slots__ = ('context', '_owner', '_p_')
+    "``config.Config()``'s configuration options settings"
+    __slots__ = ('context', '_owner', '_p_', '__weakref__')
 
     def __init__(self, context, storage):
         """
@@ -253,10 +325,19 @@ class Settings(object):
         """
         # generic owner
         self._owner = owners.user
-        self.context = context
-        import_lib = 'tiramisu.storage.{0}.setting'.format(storage.storage)
-        self._p_ = __import__(import_lib, globals(), locals(), ['Settings'],
-                              -1).Settings(storage)
+        self.context = weakref.ref(context)
+        self._p_ = storage
+
+    def _getcontext(self):
+        """context could be None, we need to test it
+        context is None only if all reference to `Config` object is deleted
+        (for example we delete a `Config` and we manipulate a reference to
+        old `SubConfig`, `Values`, `Multi` or `Settings`)
+        """
+        context = self.context()
+        if context is None:
+            raise ConfigError(_('the context does not exist anymore'))
+        return context
 
     #____________________________________________________________
     # properties methods
@@ -268,7 +349,7 @@ class Settings(object):
         return str(list(self._getproperties()))
 
     def __getitem__(self, opt):
-        path = self._get_opt_path(opt)
+        path = self._get_path_by_opt(opt)
         return self._getitem(opt, path)
 
     def _getitem(self, opt, path):
@@ -282,33 +363,36 @@ class Settings(object):
             raise ValueError(_('opt and all_properties must not be set '
                                'together in reset'))
         if all_properties:
-            self._p_.reset_all_propertives()
+            self._p_.reset_all_properties()
         else:
             if opt is not None and _path is None:
-                _path = self._get_opt_path(opt)
+                _path = self._get_path_by_opt(opt)
             self._p_.reset_properties(_path)
-        self.context.cfgimpl_reset_cache()
+        self._getcontext().cfgimpl_reset_cache()
 
     def _getproperties(self, opt=None, path=None, is_apply_req=True):
         if opt is None:
-            props = self._p_.getproperties(path, default_properties)
+            props = copy(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 '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)
+                    return copy(props)
+            props = copy(self._p_.getproperties(path, opt._properties))
             if is_apply_req:
                 props |= self.apply_requires(opt, path)
-            if 'expire' in self:
-                if ntime is None:
-                    ntime = time()
-                self._p_.setcache('property', path, props, ntime + expires_time)
+            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):
@@ -331,24 +415,29 @@ class Settings(object):
         if opt is None:
             self._p_.setproperties(None, properties)
         else:
-            if opt._calc_properties is not None:
-                properties -= opt._calc_properties
-            if set(opt._properties) == properties:
-                self._p_.reset_properties(path)
-            else:
-                self._p_.setproperties(path, properties)
-        self.context.cfgimpl_reset_cache()
+            #if opt._calc_properties is not None:
+            #    properties -= opt._calc_properties
+            #if set(opt._properties) == properties:
+            #    self._p_.reset_properties(path)
+            #else:
+            #    self._p_.setproperties(path, properties)
+            self._p_.setproperties(path, properties)
+        self._getcontext().cfgimpl_reset_cache()
 
     #____________________________________________________________
     def validate_properties(self, opt_or_descr, is_descr, is_write, path,
                             value=None, force_permissive=False,
-                            force_properties=None):
+                            force_properties=None, force_permissives=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 force_properties: set() with properties that is force to add
+                                 in global properties
+        :param force_permissives: set() with permissives that is force to add
+                                 in global permissives
         :param is_descr: we have to know if we are in an option description,
                          just because the mandatory property
                          doesn't exist here
@@ -358,13 +447,17 @@ class Settings(object):
                          (typically with the `frozen` property)
         """
         # opt properties
-        properties = copy(self._getproperties(opt_or_descr, path))
+        properties = self._getproperties(opt_or_descr, path)
+        self_properties = self._getproperties()
         # remove opt permissive
+        # permissive affect option's permission with or without permissive
+        # global property
         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()
+        if force_permissives is not None:
+            properties -= force_permissives
 
         # global properties
         if force_properties is not None:
@@ -377,7 +470,7 @@ class Settings(object):
             properties -= frozenset(('mandatory', 'frozen'))
         else:
             if 'mandatory' in properties and \
-                    not self.context.cfgimpl_get_values()._isempty(
+                    not self._getcontext().cfgimpl_get_values()._isempty(
                         opt_or_descr, value):
                 properties.remove('mandatory')
             if is_write and 'everything_frozen' in self_properties:
@@ -391,15 +484,26 @@ class Settings(object):
                 raise PropertiesOptionError(_('cannot change the value for '
                                               'option {0} this option is'
                                               ' frozen').format(
-                                                  opt_or_descr._name),
+                                                  opt_or_descr.impl_getname()),
                                             props)
             else:
                 raise PropertiesOptionError(_("trying to access to an option "
                                               "named: {0} with properties {1}"
-                                              "").format(opt_or_descr._name,
+                                              "").format(opt_or_descr.impl_getname(),
                                                          str(props)), props)
 
-    def setpermissive(self, permissive, path=None):
+    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_path_by_opt(opt)
         if not isinstance(permissive, tuple):
             raise TypeError(_('permissive must be a tuple'))
         self._p_.setpermissive(path, permissive)
@@ -422,18 +526,23 @@ class Settings(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 reset_cache(self, only_expired):
+        """reset all settings in cache
+
+        :param only_expired: if True reset only expired cached values
+        :type only_expired: boolean
+        """
         if only_expired:
-            self._p_.reset_expired_cache('property', time())
+            self._p_.reset_expired_cache(int(time()))
         else:
-            self._p_.reset_all_cache('property')
+            self._p_.reset_all_cache()
 
     def apply_requires(self, opt, path):
         """carries out the jit (just in time) requirements between options
@@ -445,12 +554,13 @@ class Settings(object):
 
         let's have a look at all the tuple's items:
 
-        - **option** is the target option's name or path
+        - **option** is the target option's
 
-        - **expected** is the target option's value that is going to trigger an action
+        - **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
+        - **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
@@ -483,20 +593,20 @@ class Settings(object):
 
         # filters the callbacks
         calc_properties = set()
+        context = self._getcontext()
         for requires in opt._requires:
             for require in requires:
                 option, expected, action, inverse, \
                     transitive, same_action = require
-                reqpath = self._get_opt_path(option)
+                reqpath = self._get_path_by_opt(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:
+                    value = context._getattr(reqpath, force_permissive=True)
+                except PropertiesOptionError as err:
                     if not transitive:
                         continue
                     properties = err.proptype
@@ -516,7 +626,32 @@ class Settings(object):
                     calc_properties.add(action)
                     # the calculation cannot be carried out
                     break
-            return calc_properties
+        return calc_properties
+
+    def _get_path_by_opt(self, opt):
+        """just a wrapper to get path in optiondescription's cache
+
+        :param opt: `Option`'s object
+        :returns: path
+        """
+        return self._getcontext().cfgimpl_get_description().impl_get_path_by_opt(opt)
+
+    def get_modified_properties(self):
+        return self._p_.get_modified_properties()
+
+    def get_modified_permissives(self):
+        return self._p_.get_modified_permissives()
+
+    def __getstate__(self):
+        return {'_p_': self._p_, '_owner': str(self._owner)}
 
-    def _get_opt_path(self, opt):
-        return self.context.cfgimpl_get_description().impl_get_path_by_opt(opt)
+    def _impl_setstate(self, storage):
+        self._p_._storage = storage
+
+    def __setstate__(self, states):
+        self._p_ = states['_p_']
+        try:
+            self._owner = getattr(owners, states['_owner'])
+        except AttributeError:
+            owners.addowner(states['_owner'])
+            self._owner = getattr(owners, states['_owner'])