.. default-role:: literal

.. currentmodule:: tiramisu

The global consistency
===========================

Identical option names
----------------------

If an :class:`~option.Option()` happens to be defined twice in the
:term:`schema` (e.g. the :class:`~option.OptionDescription()`),
that is the two options actually have the same name, an exception is raised.

The calculation is currently carried out in the samespace, for example
if `config.gc.name` is defined, another option in `gc` with the name
`name` is **not** allowed, whereas `config.whateverelse.name` is still
allowed.

Option's values type validation
--------------------------------

When a value is set to the option, the value is validated by the
option's :class:`option.Option()` validator's type.

Notice that if the option is `multi`, that is the `multi` attribute is set to
`True`, then the validation of the option value accepts a list of values
of the same type.

For example, an :class:`option.IntOption` validator waits for an `int` object of 
course, an :class:`option.StrOption` validator waits for an `str`, vs...

Where are located the values
-------------------------------

The entry point of the acces to the values is the :class:`setting.Setting()` of 
the root configuration object, but the values are actually located in the 
:class:`value.Values()` object, in order to be delegated in some kind of a 
`tiramisu.storage`, which can be a in-memory storage, or a persistent (for the 
time being, a sqlite3) storage.

:class:`value.Values()` is also responsible of the owners and the calculation 
of the options that have callbacks.

Requirements
------------

Configuration options can specify requirements as parameters at the init
time, the specification of some links between options or groups allows
to carry out a dependencies calculation. For example, an option can ben
hidden if another option has been set with some expected value. This is
just an example, the possibilities are hudge.

A requirement is a list of dictionaries that have fairly this form::

    [{'option': a, 'expected': False, 'action': 'disabled', 'inverse': True,
    'transitive':True, 'same_action': True}]

Actually a transformation is made to this dictionary during the validation of 
this requires at the :class:`~option.Option()`'s init. The dictionairy becomes 
a tuple, wich is passed to the :meth:`~setting.Settings.apply_requires()` 
method. Take a look at the code to fully understand the exact meaning of the 
requirements:

.. automethod:: tiramisu.setting.Settings.apply_requires


The path of the option is required, the second element is the value wich is 
expected to trigger the callback, it is required too, and the third one is the 
callback's action name (`hide`, `show`...), wich is a 
:class:`~setting.Property()`. Requirements are validated in 
:class:`setting.Setting`.


Let's create an option wich has requirements::

    >>> from tiramisu.option import *
    >>> from tiramisu.config import *
    >>> var2 = UnicodeOption('var2', '', u'oui')
    >>> var1 = UnicodeOption('var1', '', u'value', requires=[{'option':var2, 'expected':u'non', 'action':'hidden'}])
    >>> var3 = UnicodeOption('var3', '', u'value', requires=[{'option':var2, 'expected':u'non', 'action':'hidden'}, {'option':var2, 'expected':u'non', 'action':'disabled'}])
    >>> var4 = UnicodeOption('var4', '', u'oui')
    >>> od1 = OptionDescription('od1', '', [var1, var2, var3])
    >>> od2 = OptionDescription('od2', '', [var4], requires=[{'option':od1.var2, 'expected':u'oui', 'action':'hidden', 'inverse':True}])
    >>> rootod = OptionDescription('rootod', '', [od1, od2])
    >>> c = Config(rootod)
    >>> c.read_write()

The requirement here is the dict `{'option':var2, 'expected':u'non', 
'action':'hidden'}` wich means that is the option `'od1.var2'` is set to 
`'non'`, the option `'od1.var1'` is gonna be hidden. On the other hand, if the 
option `'od1.var2'` is different from `'non'`, the option `'od1.var1'` is not 
hidden any more::

    >>> print c.cfgimpl_get_settings()[rootod.od1.var1]
    []
    >>> print c.od1.var1
    value
    >>> print c.od1.var2
    oui
    >>> c.od1.var2 = u'non'
    >>> print c.cfgimpl_get_settings()[rootod.od1.var1]
    ['hidden']
    >>> print c.od1.var1
    Traceback (most recent call last):
    [...]
    tiramisu.error.PropertiesOptionError: trying to access to an option named: 
    var1 with properties ['hidden']
    >>> c.od1.var2 = u'oui'
    >>> print c.cfgimpl_get_settings()[rootod.od1.var1]
    []
    >>> print c.od1.var1
    value
    
The requirement on `od2` is `{'option':od1.var2, 'expected':u'oui', 
'action':'hidden', 'inverse':True}`, which means that if the option `od1.var2` 
is set to `oui`, the option is not hidden (because of the `True` at the end of 
the tuple wich means 'inverted', take a look at the :doc:`consistency` 
document.)::

    >>> print c.od2.var4
    oui
    >>> c.od1.var2 = u'non'
    >>> print c.od2.var4
    Traceback (most recent call last):
    [...]
    tiramisu.error.PropertiesOptionError: trying to access to an option named: od2 with properties ['hidden']
    >>> c.od1.var2 = u'oui'
    >>> print c.od2.var4
    oui
    
Requirements can be accumulated 

    >>> print c.cfgimpl_get_settings()[rootod.od1.var3]
    []
    >>> c.od1.var2 = u'non'
    >>> print c.cfgimpl_get_settings()[rootod.od1.var3]
    ['disabled', 'hidden']
    >>> c.od1.var2 = u'oui'
    >>> print c.cfgimpl_get_settings()[rootod.od1.var3]
    []

Requirements can be accumulated for different or identical properties (inverted 
or not)::

    >>> a = UnicodeOption('var3', '', u'value', requires=[{'option':od1.var2, 
        'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'expected':'oui', 
        'action':'hidden'}])
    >>> a = UnicodeOption('var3', '', u'value', requires=[{'option':od1.var2, 
        'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'excepted':'oui', 
        'action':'disabled', 'inverse':True}])
    
But it is not possible to have inverted requirements on the same property. 
Here is an impossible situation::

    >>> a = UnicodeOption('var3', '', u'value', requires=[{'option':od1.var2, 
        'expected':'non', 'action':'hidden'}, {'option':od1.var1, 'expected':'oui', 
        'hidden', True}])
    
    Traceback (most recent call last):
    [...]
    ValueError: inconsistency in action types for option: var3 action: hidden
            
Validation upon a whole configuration object
----------------------------------------------

An option's integrity can be validated towards a whole configuration.

This type of validation is very open. Let's take a use case : an option
has a certain value, and the value of this option can change the owner
of another option or option group... Everything is possible.

.. currentmodule:: tiramisu.option

Other hooks are availables to validate upon a whole configuration at any time, 
for example the consistency between two options (typically, an 
:class:`IPOption` and a :class:`NetworkOption`).

Let's define validator (wich is a normal python function)::

    >>> def valid_a(value, letter=''):
    ...     return value.startswith(letter)
    
Here is an option wich uses this validator::

    >>> var1 = UnicodeOption('var1', '', u'oui', validator=valid_a, validator_args={'letter': 'o'})
    >>>
    >>> od1 = OptionDescription('od1', '', [var1])
    >>>
    >>> rootod = OptionDescription('rootod', '', [od1])
    >>>
    >>> c = Config(rootod)
    >>> c.read_write()
    
The validation is applied at the modification time::

    >>> c.od1.var1 = u'non'
    Traceback (most recent call last):
    [...]
    ValueError: invalid value non for option var1
    >>> c.od1.var1 = u'oh non'
    
    Il est possible de désactiver la validation :
    
    >>> c.cfgimpl_get_settings().remove('validator')
    >>> c.od1.var1 = u'non'
    

Values that are calculated
--------------------------------

An option that have a callback is considered to have a value that is to be 
calculated. 

An option's property with a `force_store_value` attribute is considered to be 
modified at the first calculation.

.. automodule:: tiramisu.autolib
    :members:

This is the typically protocol for accessing a option's for a calculated value, 
but some twisted ways are also possible, take a look at the `force_store_value` 
attribute.

.. glossary:: 

    force store value 

        A calculated value (that is, an option that has a callback) with the 
        attribute `force_store_value` enabled is considered to be modified at 
        the first calculation

Let's create four calculation functions::

    def return_calc():
        #return an unicode value
        return u'calc'
    
    def return_value(value):
       return value
    
    def return_value_param(param=u''):
       return param
    
    def return_no_value_if_non(value):
        #if value is not u'non' return value
        if value == u'non':
            return None
        else:
            return value

Then we create four options using theses functions::

    >>> var1 = UnicodeOption('var1', '', callback=return_calc)
    >>> var2 = UnicodeOption('var2', '', callback=return_value, callback_params={'': (u'value',)})
    >>> var3 = UnicodeOption('var3', '', callback=return_value_param, callback_params={'param': (u'value_param',)})
    >>> var4 = UnicodeOption('var4', '', callback=return_no_value_if_non, callback_params={'': (('od1.var5', False),)})
    >>> var5 = UnicodeOption('var5', '', u'oui')
    >>> od1 = OptionDescription('od1', '', [var1, var2, var3, var4, var5])
    >>> rootod = OptionDescription('rootod', '', [od1])
    >>> c = Config(rootod)
    >>> c.read_write()

The first option `var1` returns the result of the `return_calc` function, wich 
is `u'calc'`::

    >>> print c.od1.var1
    calc

The second option `var2` returns the result of the `return_value` fucntion, 
wich is `value`. The parameter `u'value'` is passed to this function::

    >>> print c.od1.var2
    value

The third option `var3` returns the result of the function `return_value_param`
with the named parameter `param` and the value `u'value_param'`::

    >>> print c.od1.var3
    value_param

The fourth option `var4` returns the reslut of the function `return_no_value_if_non` 
that is the value of `od1.var5` exceptif the value is  u`non`::

    >>> print c.od1.var4
    oui
    >>> c.od1.var5 = u'new'
    >>> print c.od1.var4
    new
    >>> c.od1.var5 = u'non'
    >>> print c.od1.var4
    None
    
The calculation replaces the default value. 
If we modify the value, the calculation is not carried out any more::

    >>> print c.od1.var1
    calc
    >>> c.od1.var1 = u'new_value'
    >>> print c.od1.var1
    new_value
    
To force the calculation to be carried out in some cases, one must add the 
`frozen` and the `force_default_on_freeze` properties::

    >>> c.cfgimpl_get_settings()[rootod.od1.var1].append('frozen')
    >>> c.cfgimpl_get_settings()[rootod.od1.var1].append('force_default_on_freeze')
    >>> print c.od1.var1
    calc
    >>> c.cfgimpl_get_settings()[rootod.od1.var1].remove('frozen')
    >>> c.cfgimpl_get_settings()[rootod.od1.var1].remove('force_default_on_freeze')
    >>> print c.od1.var1
    new_value