import re

'''Class to define statically and manipulate a configuration tree,
with default values for empty trees.
'''

_sentinel = object()

def N_(x):
    return x

def _str2path(_path):
    '''Convert a string path to a tuple.

       Trailing and starting slashes for str are first removed, then str is
       splitted along slashes.
    '''
    if isinstance(_path, list) or isinstance(_path, tuple):
        return map(str.lower, _path)
    try:
        _path = str(_path)
    except:
        raise TypeError('path must be convertible to a string \
or be a sequence: path=%s' % _path)
    _path = _path.strip().lower()
    assert re.match(r'/?\w+(:\w*)?(/\w+(:\w*)?)*/?', _path)
    if _path[0] == '/':
        _path = _path[1:]
    if _path[-1] == '/':
        _path = _path[:-1]
    return _path.split('/')

def _checkstr(s, message, not_none = False):
    '''Checks that s is a non-empty string or None.

    If not_none is True, only allow non-empty strings.
    '''
    if (s is not None or not_none) \
            and (not isinstance(s, str) or str == ""):
                raise TypeError(message + ": " + str(type(s)))

class Base(object):
    '''Abstract base class for schema elements

    This is an abstract base class for schema elements, sub-classes must
    implement methods _getPathValue(self, value), _setPathValue(self,
    container, value), and check().

    Sub-classes representing not descendant of LeafBase must also implement
    _keys(self, value).

    Attributes:
        name: the name of the schema.
        title: the text to show to human in UI for this schema, it should be
        marked for translation using the _() operator.
        default: a default value for content respecting this schema.
        description: a longer text to show to human in UI, it should be marked
        for translation using the _() operator.
        help_url: an URL pointing to a documentation about this schema value.
        help_anchor: an anchor to add to the URL of the current schema or a
        parent shema, it used by UI generators when traversing a full schema.
        You can set a main URL on the root schema, and sub-schemas can point to
        their specific paragraphs using help_anchor.
        custom_check: a custom function for checking that a value conform to a
        schema. Currently it is only used by sub-classes of LeafBase.
        presentation_hint: a dictionnary defining hint for the GUI generating
        module, usual keys are 'widget' for widget type name, and 'args' for
        additional argument to give to the widget constructor.
    '''
    CHECK_ERROR = N_('Invalid value')

    def __init__(self,name,title,default=Ellipsis,description=None,help_url=None,
            help_anchor=None,custom_check=None,presentation_hint={}):
        '''Initialize a schema element.

        Setup basic attributes

        Args:
            name: set the corresponding attribute of the object, MUST be a
            non-empty string. It is converted to lower cases.
            title: set the corresponding attribute of the object, MUST be a
            non-empty string.
            default: set the corresponding attribute of the object, it must
            verify the check() function for the class.
            help_url: set the corresponding attribute of the object, MUST be
            a string or None.
            help_anchor: set the corresponding attribute of the object, MUST
            be a string or None.
            custom_check: set the corresponding attribute of the object, MUST
            be a callable or None.
        Raises:
            TypeError: if a parameter does not respect one of the constraints.
        '''
        _checkstr(name, 'A schema element name must be a simple string',
                not_none = True)
        self.name = name.lower()
        _checkstr(title, 'A schema element title must be a kind of string',
                not_none = True)
        self.title = title
        _checkstr(description, 'a schema description must be a string')
        self.description = description
        _checkstr(help_url, 'help_url must be a string or None')
        self.help_url = help_url
        _checkstr(help_anchor, 'help_anchor must be a string or None')
        self.help_anchor = help_anchor
        if not isinstance(presentation_hint, dict):
            raise TypeError('presentation_hint must be a dict')
        self.presentation_hint = presentation_hint
        if custom_check is not None and not callable(custom_check):
            raise TypeError('custom_check must be callable')
        self.custom_check = custom_check
        self._default = default
        if self._default is not Ellipsis and not callable(self._default):
            # automatic check of coherency between shema/default value
            if not self.check(self._default):
                raise TypeError("default value %s does not respect the schema %s" % (self.default, self))

    def get_default(self):
        if callable(self._default):
            return self._default()
        else:
            return self._default

    default = property(get_default)

    def check(self, value):
        '''Verify that a value is conform to this schema

        Args:
            value: a value to test agains the current schema.

        Returns:
            A boolean value.
        '''
        raise NotImplementedError('check must be overloaded')

    def _getPathValue(self, path, value):
        '''Abstract method, MUST be overloaded by sub-classes

           Contract is the same as getPathValue() but path will always be a list.
        '''
        raise NotImplementedError('_getPathValue must be overloaded')

    def getPathValue(self, path, value):
        '''Extract value for the path traversing value respecting the current
        schema.

        If value does not verify check() the default value is returned.
        If path does not exist in this schema, LookupError() is raised.
        This method is abstract, it MUST be overloaded by sub-classes.

        Args:
            path: a list representing the path, or a string that can be
            converted to a path using _str2path().
            value: any python value, it will be check before being accessed.

        Returns:
            A tuple (schema, ret) where schema is the schema which finally
            returned matched the path, and ret is extracted from value or the
            default for the final schema.

        Raises:
            LookupError: if path does not exist in this schema.
        '''
        path = _str2path(path)
        return self._getPathValue(path, value)

    def _setPathValue(self, path, container, value):
        '''Abstract method, MUST be overloaded by sub-classes

           Contract is the same as setPathValue() but path will always be a list.
        '''
        raise NotImplementedError('_setPathValue MUST be overloaded by subclasses')

    def setPathValue(self, path, container, value):
        '''Create the path inside a container and set the final path step to
        value.

        Args:
            path: a list, describing a path inside the current schema.
            container: a dictionary which contain current value for the schema or is empty.
            value: the new value to set at the target of the path.

        Raises:
            TypeError: if path is not a tuple or a list, or if value does not
            pass the check() for the schema targeted by the path.
            LookupError: if the path does not exist.
        '''
        path = _str2path(path)
        return self._setPathValue(path, container, value)

    def _start_str_(self):
        '''Helper method to implement __str__ in sub-classes, it extract base
        attributes of schema classes.
        '''
        ret = self.__class__.__name__ + '('
        ret += 'name = %r, title = %r' % (self.name, self.title)
        if self.default:
            ret += ', default = %r' % self.default
        if self.description:
            ret += ', description = %r' % self.description
        if self.help_url:
            ret += ', help_url = %r' % self.help_url
        if self.help_anchor:
            ret += ', help_anchor = %r' % self.help_anchor
        if self.custom_check:
            ret += ', custom_check = %r' % self.custom_check
        return ret

    def __str__(self):
        return self._start_str_() + ')'


class WithDefaultBase(Base):
    '''Mixin for schema classes which MUST enforce the presence of a default
    value
    '''
    def __init__(self, name, title, *args,**kwargs):
        if kwargs.get('default', Ellipsis) is Ellipsis:
            raise TypeError('%s\'s default value must be given' % self.__class__)
        return super(WithDefaultBase, self).__init__(name, title, *args, **kwargs)


# Leaf schema classes.


class LeafBase(WithDefaultBase):
    '''Abstract base class for schema classes representing simple values.

    That is schema that cannot contain sub-nodes, and only accept the empty
    sequence as an access path.

   Acessing using a non-empty path will raise LookupError.
   '''
    def __init__(self, name, title, *args, **kwargs):
        self.allow_none = kwargs.pop('allow_none', False)
        self.reset_to_default = kwargs.pop('reset_to_default', False)
        self.override = kwargs.pop('override', None)
        super(LeafBase, self).__init__(name, title, *args, **kwargs)

    def _getPathValue(self, path, value):
        if len(path):
            raise LookupError('path %s not found in value %s for schema %s' % (path, value, self))
        if callable(self.override):
            o=self.override()
            if o:
                v,_=o
                return (self, v)
        if value is not _sentinel and self.check(value):
            return (self, value)
        return (self, self.default)

    def _setPathValue(self, path, container, value):
        if len(path):
            raise LookupError('path %s not found in value %s for schema %s' % (path, value, self))
        if not self.check(value):
            raise TypeError('Cannot use %s as a new value for schema %s, it does not match' % (value, self))
        if self.reset_to_default and (not value or value == self.default):
            del container[self.name]
        else:
            container[self.name] = value

    def _check(self, value):
        '''Abstract method for _check(), sub-classes MUST overload it.'''
        raise NotImplementedError('_check MUST be overloaded')

    def check(self, value):
        '''Check that value conform to the schema.

        If self.custom_check exists, the value will also be check check using self.custom_check.

        Args:
            value: a value to check. Can be anything.

        Returns:
            A boolean value.
        '''
        if self.allow_none and value is None:
            return True
        if self.custom_check:
            return self._check(value) and self.custom_check(value) is True
        else:
            return self._check(value)

class Regexp(LeafBase):
    '''Match a string agains a pattern'''
    def __init__(self, name, title, *args, **kwargs):
        self.pattern = kwargs.pop('pattern')
        self.flags = kwargs.pop('flags', 0)
        try:
            self.compiled = re.compile(self.pattern, self.flags)
        except e:
            raise TypeError("Invalid pattern %r : %s" % (self.pattern, e))
        super(Regexp, self).__init__(name, title, *args, **kwargs)

    def _check(self, value):
        return self.compiled.match(value) is not None

class Boolean(LeafBase):
    '''A schema class for boolean values.'''
    def _check(self, value):
        return isinstance(value, bool)


class String(LeafBase):
    '''A schema class for ASCII string values.'''
    def _check(self, value):
        return isinstance(value, str)

class EMail(LeafBase):
    '''A schema class for email values.'''
    CHECK_ERROR = N_('Value must be an email')
    def _check(self, value):
        return isinstance(value, str) and re.match(r'\w+@\w+(\.\w+)*', value) is not None

class Integer(LeafBase):
    '''A schema class for integer values.'''
    CHECK_ERROR = N_('Value must be an integer')
    def _check(self, value):
        return isinstance(value, int)

class Range(LeafBase):
    '''A schema class for integer values inside a range

    Attributes:
        start: a value respecting this schema must be >= to self.start.
        end: a value respecting this schema must be < to self.end.
    '''

    def __init__(self, *args, **kwargs):
        start, end = kwargs.pop('start'), kwargs.pop('end')
        kwargs.setdefault('default', start)
        if isinstance(start, int) \
                and isinstance(end, int):
            self.start = start
            self.end = end
            super(Range, self).__init__(*args, **kwargs)
        else:
            raise TypeError()

    def _check(self, value):
        return self.start <= value < self.end


class Void(LeafBase):
    '''A schema for any value.

       We advise you agains using it. It removed any advantage of using the
       schema API'''
    def _check(self, value):
        return True


class Options(LeafBase):
    '''A schema describing one or multiple string value chosen among a set of
    possible values.

    Attributes:
        content - a dictionary describing the possible values, the format is:
            { 'aValue1': { 'key': aFormKey, 'description': aFormDescription}, ..},
              'aValue2': { ... }, ... }.

        multiple - a boolean stating if the schema represent only one value or
        any number in the list
    '''

    def __init__(self, name, title, *args, **kwargs):
        '''Initialize a new Options schema.

        If content is None, supplementary non keyword args are used as a list
        of strings for content values. The value are used as the key and
        description.

        Args:
            name: a string for the name of the schema.
            title: a short human description of the schema.
            content: a dictionary describing the possible values.
        '''
        content = kwargs.pop('content', None)
        multiple = kwargs.pop('multiple', False)
        self.content = {}
        self.multiple = multiple
        if content is None:
            for i, arg in enumerate(args):
                if self.content.get(arg):
                    raise TypeError('Options MUST contain options with different names')
                self.content[arg] = { 'key': i, 'description': arg }
        elif isinstance(content, dict):
            self.content = content
        else:
            raise TypeError()
        super(Options,self).__init__(name, title, *args, **kwargs)

    def _check(self, value):
        if self.multiple:
            if isinstance(value, list):
                for x in value:
                    if x not in self.content:
                        return False
            else:
                return False
        else:
            return (value in self.content.keys())

class File(LeafBase):
    def _check(self, value):
        return True

class Group(Base):
    '''A schema for a tree of other schemas.

    Attributes:
        content - a dictionary containing schema for subtrees indexed by their
        name.
        content_list - the list of containg schema as given on the constructor
        call.
    '''

    def __init__(self, name, title, *args, **kwargs):
        self.content = {}
        self.ordered = []
        for schema in args:
            if not isinstance(schema, Base):
                raise TypeError()
            if self.content.get(schema.name):
                raise TypeError('group content schema elements MUST have different names')
            self.content[schema.name] = schema
            self.ordered.append(schema.name)
        self.content_list = args
        default = kwargs.get('default')
        if default:
            raise TypeError('default: %s' % default)
        super(Group, self).__init__(name, title, **kwargs)

    def add_schema(self, schema, idx = -1):
        '''Append a new schema to the list of schemas'''
        self.content[schema.name] = schema
        if idx < 0:
            idx = len(self.ordered)
        self.ordered.insert(idx, schema.name)

    def check(self, value):
        if not isinstance(value, dict):
            return False
        for key in value:
            schema = self.content.get(key)
            if not schema:
                continue
            if not schema.check(value.get(key)):
                return False
        return True

    def _getPathValue(self, path, value):
        if not path:
            if not isinstance(value, dict):
                value = None
            return (self, value)
        key, tl = path[0], path[1:]
        schema = self.content.get(key)
        if not schema:
            raise LookupError()
        if isinstance(value, dict):
            value = value.get(key, _sentinel)
        else:
            value = _sentinel
        return schema._getPathValue(tl, value)

    def _setPathValue(self, path, container, value):
        if len(path):
            hd, tl = path[0], path[1:]
            schema = self.content.get(hd)
            if schema:
                c = container.get(self.name, {})
                schema._setPathValue(tl, c, value)
                container[self.name] = c
            else:
                raise LookupError()
        else:
            if self.check(value):
                container[self.name] = value
            else:
                raise TypeError('Cannot use %s as a new value for \
schema %s, it does not match' % (value, self))

    def __str__(self):
        ret = self._start_str_() + '\n'
        for schema in self.content:
            s = str(self.content[schema])
            s = re.sub(re.compile('^', re.M), '\t', s)
            ret += "%s,\n" % s
        ret += ')'
        return ret

    def _keys(self, value):
        return self.content.keys()


class Dict(Base):
    '''Represent a set of value indexed by a string key.

        content - the schema for the content of the dict'''

    content = None

    def __init__(self, name, title, schema, **kwargs):
        if kwargs.get('default') is not None:
            raise TypeError()
        if not isinstance(schema, Base):
            raise TypeError()
        self.content = schema
        super(Dict,self).__init__(name, title, **kwargs)

    def check(self, value):
        if not isinstance(value, dict):
            return False
        for x in value:
            if not self.content.check(value[x]):
                return False
        return True

    def _getPathValue(self, path, value):
        if not path:
            return (self, value)
        else:
            key, tl = path[0], path[1:]
            if not isinstance(value, dict):
                raise LookupError()
            value = value.get(key)
            if not value:
                raise LookupError()
            return self.content._getPathValue(tl, value)

    def _setPathValue(self, path, container, value):
        if len(path) == 0:
            if self.check(value):
                container[self.name] = value
            else:
                raise TypeError('Cannot use %s as a new value for \
schema %s, it does not match' % (value, self))
        elif len(path) == 1:
            c = container.get(self.name, {})
            if value is None:
                del container[self.name]
            elif self.content.check(value):
                c[path[0]] = value
                container[self.name] = c
            else:
                raise TypeError('Cannot use %s as a new value for \
schema %s, it does not match' % (value, self.content))
        else:
            hd, tl = path[0], path[1:]
            c1 = container.get(self.name, {})
            c2 = c1.get(hd, {})
            c3 = { self.content.name: c2 }
            self.content._setPathValue(tl, c3, value)
            c1[hd] = c3[self.content.name]
            container[self.name] = c1

    def __str__(self):
        ret = 'Dict(name = %r, title = %r,\n' % (self.name, self.title)
        s = str(self.content)
        s = re.sub(re.compile('^\t', re.M), '\t\t', s, re.M)
        ret += '\tschema = %s)' % s
        return ret

    def _keys(self, value):
        if isinstance(value, dict):
            return value.keys()
        else:
            return []


class Choice(Base):
    '''Can be any-one of the sub-schemas.

       for example source of data could be LDAP or SQL, you get the type
       through get('type'), then you can access the normal path of the
       sub-schema

       To set a field of one of the sub-schemas you must prefix the path starting at the subschema with the prefix 'schema-name:', for example:
       setPathValue('/choice/ldap:ldap_url', ...)'''

    def __init__(self, name, title, *args, **kwargs):
        self.content = {}
        default = None
        if kwargs.get('default'):
            default = kwargs.pop('default', None)
        # parse possible schemas
        for arg in args:
            if not isinstance(arg, Base):
                raise TypeError((name,title,args,kwargs, arg))
            self.content[arg.name] = arg
        if not isinstance(default, str):
            raise TypeError()
        else:
            if not default in self.content:
                raise TypeError('default must be one of the content name')
        super(Choice, self).__init__(name, title, **kwargs)
        self.__type_schema = Options('type', 'type of %s' % self.name, *self.content.keys())
        # pass over the check of the default value, since semantic of self.default is
        # different from all other schema classes.
        self._default = default

    def _match_sub(self, value):
        for k, schema in self.content.iteritems():
            if schema.check(value):
                return schema
        return None

    def check(self, value):
        # match schema for Choice
        if isinstance(value, dict) and value.get('_type'):
            key = value.get('_type')
            if not isinstance(key, str):
                return False
            schema = self.content.get(key)
            if not schema:
                return False
            return schema.check(value.get('_value'))
        # one of the sub-schemas
        schema = self._match_sub(value)
        if schema:
            return True
        return False

    def _getPathValue(self, path, value):
        if isinstance(value, dict):
            key = value.get('_type')
            value = value.get('_value')
        else:
            key = self.default
            value = None
        if len(path) == 1 and path[0] == 'type':
            return (self.__type_schema, key)
        schema = self.content.get(key)
        if not schema:
            key = self.default
            value = None
            schema = self.content.get(key)
        if path:
            return schema._getPathValue(path, value)
        else:
            return (self, None)

    def _setPathValue(self, path, container, value):
        # no path, value conform to Choice value or to one of the possible
        # schemas
        schema = None
        c1 = container.get(self.name, {})
        if len(path) == 0:
            if isinstance(value, 'dict') and value.get('_type'):
                LeafBase._setPathValue(self, path, container, value)
                return
            tl = []
        else:
            # a path, type of the schema if encoded in the head of the path,
            # or must be found by looking into subschemas.
            hd, tl = path[0], path[1:]
            if ':' in hd:
                # extract the encoded type
                prefix, suffix = hd.split(':', 1)
                if suffix:
                    tl = [ suffix ] + tl
                schema = self.content.get(prefix)
            else:
                # no encoded type ? try to find the path using guessing of sub-path
                tl = path
                for k, subschema in self.content.iteritems():
                    if hd in subschema._keys(c1.get('_value', {})):
                        if not schema:
                            schema = subschema
                        else:
                            raise LookupError('Could not disambiguate the path without a type')
        if not schema:
            # if no prefix was used try to make a best guess using check
            schema = self._match_sub(value)
        if not schema:
            raise LookupError()
        if c1.get('_type') == schema.name:
            c2 = { schema.name: c1.get('_value') }
        else:
            c2 = {}
        schema._setPathValue(tl, c2, value)
        c1['_type'] = schema.name
        c1['_value'] = c2[schema.name]
        container[self.name] = c1

    def _keys(self, value):
        '''Return keys for the default sub-schema or the schema currently
        selected.
        '''
        if isinstance(value, dict):
            key = value.get('_type')
            value = value.get('_value')
        else:
            key = self.default
            value = None
        schema = self.content.get(key)
        if not schema:
            key = self.default
            value = None
            schema = self.content.get(key)
        return schema._keys(value)

class Configuration(object):
    '''A movable view of a configuration tree.

    Value are indexed by paths, you can get and set them and the
    configuration will verify that the setted values match the
    configuration schema.

    You can move the view to a subtree using self.cd(path) or self.get(path), cd
    will verify that you only try to access interior nodes (directories)
    not leafs of the tree, self.get(path) WILL NOT.

    parent() will get you to the parent node, or raise if no parent node exists.

    root() will get you to root of the configuration tree.

    Attributes:
        schema: the schema describing the configuration tree.
        value: a configuration tree
        prefix: when moved to a sub-tree, it contains the path to this subtree.
        _root: a tuple (schema, value) keeping the root of the configuration
        tree when the view is moved to sub-schemas.'''

    def __init__(self, schema, value, check = False, prefix = [], _root = None):
        if check and not schema.check(value):
                raise TypeError('%s is not compatible with %s' % (value, schema))
        self._schema = schema
        self.value = value
        if _root:
            self._root = _root
        else:
            self._root = (schema, value)
        self.prefix = prefix

    def _handle_path(self, path, prefix = False):
        '''Convert path from string to list, and preprend prefix to it if
        asked.
        '''
        path = _str2path(path)
        if self.prefix and prefix:
            path = self.prefix + path
        return path

    def set(self, path, value):
        path = self._handle_path(path, prefix = True)
        container = { self._root[0].name: self._root[1] }
        self._root[0].setPathValue(path, container, value)
        self._schema, self.value = self._root[0].getPathValue(self.prefix, self._root[1])

    def get(self, path):
        '''Returns the value at path.

        Returns:
            A value contained in the configuration tree of the default for this
            path if path target a leaf schema. If path target a sub-tree, a new
            Configuration object pointing at this sub-tree is returned.

        Raises:
            LookupError: if path does not exist.
        '''
        path = self._handle_path(path)
        realpath = self._handle_path(path, prefix = True)
        schema, value = self._schema.getPathValue(path, self.value)
        if isinstance(schema, LeafBase):
            return value
        else:
            return self.__class__(schema, value, check = False, prefix = realpath, _root = self._root)

    def keys(self, path = []):
        '''List keys in the current view of for a sub-view at path, if path is not empty.

        Returns:
            a list of keys.

        Raises:
            LookupError: if path does not exist.
        '''
        path = self._handle_path(path)
        if path:
            schema, value = self._schema.getPathValue(path, self.value)
        else:
            schema, value = self._schema, self.value
        return schema._keys(value)

    def schema(self, path = []):
        '''Returns the schema handling this path.

        Returns:
            an instance of Base.

        Raises:
            LookupError: if path does not exist.
        '''
        path = self._handle_path(path)
        if path:
            schema, value = self._schema.getPathValue(path, self.value)
        else:
            schema = self._schema
        return schema

    def match(self, path, to_match):
        '''Check if value at path match to_match.

        This method also check that to_match match the schema at path, it
        prevents errors like checking for a non existing value in an option
        list or non-existing type in a Choice schema.

        Args:
            path: the path to test.
            to_match: the value to test agains.

        Raises:
            LookupError: if path does not exist.
            TypeError: if to_match does not pass check for the targeted schema.
        '''
        path = self._handle_path(path)
        schema, value = self._schema.getPathValue(path, self.value)
        if not schema.check(to_match):
            raise TypeError('%s cannot match a value for schema %s' % (value, schema))
        return value == to_match

    def root(self):
        '''Returns a Configuration objet for the root of the configuration
        tree'''
        if self.prefix:
            return self.__class__(self._root[0], self._root[1], check = False)
        else:
            return self

    def parent(self):
        '''Returns a Configuration object for the parent tree of the current view.

        Raises:
            TypeError: if the current view is already at the root of the tree
        '''
        if self.prefix:
            return self.root().get(self.prefix[:-1])
        else:
            raise TypeError('this configuration does not have a parent')

    def cd(self, path):
        '''Returns a new Configuration object moved to a sub-tree of the current configuration tree

           Raises:
               LookupError: if path does not exist or is not a sub-tree, i.e. it targets a Leaf schema.
        '''
        path = self._handle_path(path)
        schema, value = self._schema.getPathValue(path, self.value)
        if not isinstance(schema, LeafBase):
            return self.__class__(schema, value, check = False, prefix = path, _root = self._root)
        else:
            raise LookupError()

# Unit tests
if __name__ == '__main__':
    import pprint
    schema = Group('configuration', 'Ma Configuration',
                Boolean('do_it', 'Le faire ou non', default = True),
                String('nom', 'Le nom', default = ""),
                Integer('taille', 'La taille', default = 100),
                Range('nr_of_threads', 'Number of threads', start = 1, end = 20, default = 1),
                EMail('debug_email', 'EMail for debug', default = 'root@localhost.localnet'),
                Options('debug', 'Trace format', default = 'no_trace',
                    content = { 'no_trace': { 'description': 'Show no traces'},
                        'text_trace': { 'description': 'Show traces as plain text' },
                        'html_trace': { 'description': 'Show traces as an HTML page' }}),
                Group('subconfig', 'La sous configuration',
                    Boolean('a', '', default = False),
                    String('b', '', default = 'xxx'),
                    Dict('service_providers', 'Service Providers',
                        Group('sp','Service Provider Configuration',
                            String('metadata_url','Metadata URL',default=''),
                            Boolean('sign_assertions','Should we sign assertions?', default=True))),
                    Choice('data_source', 'Data Source',
                        Group('ldap', 'LDAP Data Source',
                            String('ldap_url','URL Ldap', default = 'ldap://localhost/'),
                            String('binddn', 'Bind DN', default = ''),
                            String('bindpassword', 'Bind Password', default = '')),
                        Group('sql', 'SQL Data Source',
                            String('sql_url', 'SQL Data Source URL', default = 'sql://localhost'),
                            String('sql_db', 'SQL Database', default = 'default_db'),
                            String('sql_password', 'SQL password', default = '')),
                        default='ldap')))
    value = { 'do_it': False, 'nom': "zobi" }
    config = Configuration(schema, value)
    assert config.match('/do_it', False)
    assert config.match('/nom', 'zobi')
    assert not config.match('/nom', 'coin')
    assert config.match('/taille', 100)
    config.set('/taille', 200)
    assert config.match('/taille', 200)
    config.set('/nom', 'coin')
    assert config.match('/nom', 'coin')
    assert config.match('/subconfig/a', False)
    assert config.match('/subconfig/b', 'xxx')
    config.set('/subconfig/b', 'yyy')
    assert config.match('/subconfig/b', 'yyy')
    subconfig = config.get('/subconfig')
    assert subconfig.match('/b', 'yyy')
    subconfig.set('/service_providers/http_coin.org_saml_metadata/metadata_url', 'http://coin.org/saml/metadata')
    subconfig = subconfig.get('/service_providers/http_coin.org_saml_metadata')
    assert subconfig.match('/sign_assertions', True)
    subconfig.set('/sign_assertions', False)
    assert config.match('/subconfig/service_providers/http_coin.org_saml_metadata/sign_assertions', False)
    subconfig = config.get('subconfig/service_providers')
    assert subconfig.keys() == [ 'http_coin.org_saml_metadata' ]
    assert config.keys() == subconfig.parent().parent().keys() == subconfig.root().keys()
    assert config.match('/subconfig/data_source/ldap_url', 'ldap://localhost/')
    assert config.match('/subconfig/data_source/type', 'ldap')
    config.set('/subconfig/data_source/sql:sql_url', 'coin')
    assert config.match('/subconfig/data_source/sql_url', 'coin')
    assert config.match('/subconfig/data_source/sql_db', 'default_db')
    assert config.match('debug', 'no_trace')
    config.set('debug', 'html_trace')
    assert config.match('debug', 'html_trace')
    assert config.match('nr_of_threads', 1)
    config.set('nr_of_threads', 10)
    assert config.match('nr_of_threads', 10)
    config.set('debug_email', 'coin@entrouvert.org')
    pprint.pprint(config.value)
