an OptionDescription can be serialized
[tiramisu.git] / tiramisu / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description"
3 # Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 # The original `Config` design model is unproudly borrowed from
20 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
23 import re
24 import sys
25 from copy import copy, deepcopy
26 from types import FunctionType
27 from IPy import IP
28
29 from tiramisu.error import ConflictError
30 from tiramisu.setting import groups, multitypes
31 from tiramisu.i18n import _
32 from tiramisu.autolib import carry_out_calculation
33
34 name_regexp = re.compile(r'^\d+')
35 forbidden_names = ('iter_all', 'iter_group', 'find', 'find_first',
36                    'make_dict', 'unwrap_from_path', 'read_only',
37                    'read_write', 'getowner', 'set_contexts')
38
39
40 def valid_name(name):
41     "an option's name is a str and does not start with 'impl' or 'cfgimpl'"
42     try:
43         name = str(name)
44     except:
45         return False
46     if re.match(name_regexp, name) is None and not name.startswith('_') \
47             and name not in forbidden_names \
48             and not name.startswith('impl_') \
49             and not name.startswith('cfgimpl_'):
50         return True
51     else:
52         return False
53 #____________________________________________________________
54 #
55
56
57 class BaseInformation(object):
58     "interface for an option's information attribute"
59     __slots__ = ('_impl_informations',)
60
61     def impl_set_information(self, key, value):
62         """updates the information's attribute
63         (which is a dictionary)
64
65         :param key: information's key (ex: "help", "doc"
66         :param value: information's value (ex: "the help string")
67         """
68         try:
69             self._impl_informations[key] = value
70         except AttributeError:
71             raise AttributeError(_('{0} has no attribute '
72                                    'impl_set_information').format(
73                                        self.__class__.__name__))
74
75     def impl_get_information(self, key, default=None):
76         """retrieves one information's item
77
78         :param key: the item string (ex: "help")
79         """
80         try:
81             if key in self._impl_informations:
82                 return self._impl_informations[key]
83             elif default is not None:
84                 return default
85             else:
86                 raise ValueError(_("information's item"
87                                    " not found: {0}").format(key))
88         except AttributeError:
89             raise AttributeError(_('{0} has no attribute '
90                                    'impl_get_information').format(
91                                        self.__class__.__name__))
92
93
94 class BaseOption(BaseInformation):
95     __slots__ = ('_readonly', '_state_consistencies', '_state_requires')
96
97     def __setattr__(self, name, value):
98         if not name.startswith('_state'):
99             is_readonly = False
100             # never change _name
101             if name == '_name':
102                 try:
103                     self._name
104                     #so _name is already set
105                     is_readonly = True
106                 except:
107                     pass
108             try:
109                 if self._readonly is True:
110                     if value is True:
111                         # already readonly and try to re set readonly
112                         # don't raise, just exit
113                         return
114                     is_readonly = True
115             except AttributeError:
116                 pass
117             if is_readonly:
118                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
119                                        " read-only").format(
120                                            self.__class__.__name__,
121                                            self._name,
122                                            name))
123         object.__setattr__(self, name, value)
124
125     def _impl_convert_consistencies(self, cache):
126         # cache is a dico in import/not a dico in export
127         if self._consistencies is None:
128             self._state_consistencies = None
129         else:
130             new_value = []
131             for consistency in self._consistencies:
132                 if isinstance(cache, dict):
133                     new_value.append((consistency[0], cache[consistency[1]]))
134                 else:
135                     new_value.append((consistency[0],
136                                       cache.impl_get_path_by_opt(
137                                           consistency[1])))
138             if isinstance(cache, dict):
139                 pass
140             else:
141                 self._state_consistencies = tuple(new_value)
142
143     def _impl_convert_requires(self, cache):
144         # cache is a dico in import/not a dico in export
145         if self._requires is None:
146             self._state_requires = None
147         else:
148             new_value = []
149             for requires in self._requires:
150                 new_requires = []
151                 for require in requires:
152                     if isinstance(cache, dict):
153                         new_require = [cache[require[0]]]
154                     else:
155                         new_require = [cache.impl_get_path_by_opt(require[0])]
156                     new_require.extend(require[1:])
157                     new_requires.append(tuple(new_require))
158                 new_value.append(tuple(new_requires))
159             if isinstance(cache, dict):
160                 pass
161             else:
162                 self._state_requires = new_value
163
164     def _impl_getstate(self, descr):
165         self._impl_convert_consistencies(descr)
166         self._impl_convert_requires(descr)
167
168     def __getstate__(self, export=False):
169         slots = set()
170         for subclass in self.__class__.__mro__:
171             if subclass is not object:
172                 slots.update(subclass.__slots__)
173         slots -= frozenset(['_children', '_cache_paths', '__weakref__'])
174         states = {}
175         for slot in slots:
176             # remove variable if save variable converted in _state_xxxx variable
177             if '_state' + slot not in slots:
178                 if slot.startswith('_state'):
179                     # should exists
180                     states[slot] = getattr(self, slot)
181                     # remove _state_xxx variable
182                     self.__delattr__(slot)
183                 else:
184                     try:
185                         states[slot] = getattr(self, slot)
186                     except AttributeError:
187                         pass
188         return states
189
190
191 class Option(BaseOption):
192     """
193     Abstract base class for configuration option's.
194
195     Reminder: an Option object is **not** a container for the value
196     """
197     __slots__ = ('_name', '_requires', '_multi', '_validator',
198                  '_default_multi', '_default', '_properties', '_callback',
199                  '_multitype', '_master_slaves', '_consistencies',
200                  '_calc_properties', '__weakref__')
201     _empty = ''
202
203     def __init__(self, name, doc, default=None, default_multi=None,
204                  requires=None, multi=False, callback=None,
205                  callback_params=None, validator=None, validator_args=None,
206                  properties=None):
207         """
208         :param name: the option's name
209         :param doc: the option's description
210         :param default: specifies the default value of the option,
211                         for a multi : ['bla', 'bla', 'bla']
212         :param default_multi: 'bla' (used in case of a reset to default only at
213                         a given index)
214         :param requires: is a list of names of options located anywhere
215                          in the configuration.
216         :param multi: if true, the option's value is a list
217         :param callback: the name of a function. If set, the function's output
218                          is responsible of the option's value
219         :param callback_params: the callback's parameter
220         :param validator: the name of a function wich stands for a custom
221                           validation of the value
222         :param validator_args: the validator's parameters
223
224         """
225         if not valid_name(name):
226             raise ValueError(_("invalid name: {0} for option").format(name))
227         self._name = name
228         self._impl_informations = {}
229         self.impl_set_information('doc', doc)
230         self._calc_properties, self._requires = validate_requires_arg(
231             requires, self._name)
232         self._multi = multi
233         self._consistencies = None
234         if validator is not None:
235             if type(validator) != FunctionType:
236                 raise TypeError(_("validator must be a function"))
237             if validator_args is None:
238                 validator_args = {}
239             self._validator = (validator, validator_args)
240         else:
241             self._validator = None
242         if not self._multi and default_multi is not None:
243             raise ValueError(_("a default_multi is set whereas multi is False"
244                              " in option: {0}").format(name))
245         if default_multi is not None:
246             try:
247                 self._validate(default_multi)
248             except ValueError as err:
249                 raise ValueError(_("invalid default_multi value {0} "
250                                    "for option {1}: {2}").format(
251                                        str(default_multi), name, err))
252         if callback is not None and (default is not None or
253                                      default_multi is not None):
254             raise ValueError(_("default value not allowed if option: {0} "
255                              "is calculated").format(name))
256         if callback is None and callback_params is not None:
257             raise ValueError(_("params defined for a callback function but "
258                              "no callback defined"
259                              " yet for option {0}").format(name))
260         if callback is not None:
261             if type(callback) != FunctionType:
262                 raise ValueError('callback must be a function')
263             if callback_params is not None and \
264                     not isinstance(callback_params, dict):
265                 raise ValueError('callback_params must be a dict')
266             self._callback = (callback, callback_params)
267         else:
268             self._callback = None
269         if self._multi:
270             if default is None:
271                 default = []
272             self._multitype = multitypes.default
273             self._default_multi = default_multi
274         self.impl_validate(default)
275         self._default = default
276         if properties is None:
277             properties = tuple()
278         if not isinstance(properties, tuple):
279             raise TypeError(_('invalid properties type {0} for {1},'
280                             ' must be a tuple').format(
281                                 type(properties),
282                                 self._name))
283         self._properties = properties  # 'hidden', 'disabled'...
284
285     def _launch_consistency(self, func, opt, vals, context, index, opt_):
286         if context is not None:
287             descr = context.cfgimpl_get_description()
288         if opt is self:
289             #values are for self, search opt_ values
290             values = vals
291             if context is not None:
292                 path = descr.impl_get_path_by_opt(opt_)
293                 values_ = context._getattr(path, validate=False)
294             else:
295                 values_ = opt_.impl_getdefault()
296             if index is not None:
297                 #value is not already set, could be higher
298                 try:
299                     values_ = values_[index]
300                 except IndexError:
301                     values_ = None
302         else:
303             #values are for opt_, search self values
304             values_ = vals
305             if context is not None:
306                 path = descr.impl_get_path_by_opt(self)
307                 values = context._getattr(path, validate=False)
308             else:
309                 values = self.impl_getdefault()
310             if index is not None:
311                 #value is not already set, could be higher
312                 try:
313                     values = values[index]
314                 except IndexError:
315                     values = None
316         if index is None and self.impl_is_multi():
317             for index in range(0, len(values)):
318                 try:
319                     value = values[index]
320                     value_ = values_[index]
321                 except IndexError:
322                     value = None
323                     value_ = None
324                 if None not in (value, value_):
325                     getattr(self, func)(opt_._name, value, value_)
326         else:
327             if None not in (values, values_):
328                 getattr(self, func)(opt_._name, values, values_)
329
330     def impl_validate(self, value, context=None, validate=True):
331         """
332         :param value: the option's value
333         :param validate: if true enables ``self._validator`` validation
334         """
335         if not validate:
336             return
337
338         def val_validator(val):
339             if self._validator is not None:
340                 callback_params = deepcopy(self._validator[1])
341                 callback_params.setdefault('', []).insert(0, val)
342                 return carry_out_calculation(self._name, config=context,
343                                              callback=self._validator[0],
344                                              callback_params=callback_params)
345             else:
346                 return True
347
348         def do_validation(_value, _index=None):
349             if _value is None:
350                 return True
351             if not val_validator(_value):
352                 raise ValueError(_("invalid value {0} "
353                                    "for option {1} for object {2}"
354                                    ).format(_value,
355                                             self._name,
356                                             self.__class__.__name__))
357             try:
358                 self._validate(_value)
359             except ValueError as err:
360                 raise ValueError(_("invalid value {0} for option {1}: {2}"
361                                    "").format(_value, self._name, err))
362             if context is not None:
363                 descr._valid_consistency(self, _value, context, _index)
364
365         # generic calculation
366         if context is not None:
367             descr = context.cfgimpl_get_description()
368         if not self._multi:
369             do_validation(value)
370         else:
371             if not isinstance(value, list):
372                 raise ValueError(_("invalid value {0} for option {1} "
373                                    "which must be a list").format(value,
374                                                                   self._name))
375             for index in range(0, len(value)):
376                 val = value[index]
377                 do_validation(val, index)
378
379     def impl_getdefault(self, default_multi=False):
380         "accessing the default value"
381         if not default_multi or not self.impl_is_multi():
382             return self._default
383         else:
384             return self.getdefault_multi()
385
386     def impl_getdefault_multi(self):
387         "accessing the default value for a multi"
388         return self._default_multi
389
390     def impl_get_multitype(self):
391         return self._multitype
392
393     def impl_get_master_slaves(self):
394         return self._master_slaves
395
396     def impl_is_empty_by_default(self):
397         "no default value has been set yet"
398         if ((not self.impl_is_multi() and self._default is None) or
399                 (self.impl_is_multi() and (self._default == []
400                                            or None in self._default))):
401             return True
402         return False
403
404     def impl_getdoc(self):
405         "accesses the Option's doc"
406         return self.impl_get_information('doc')
407
408     def impl_has_callback(self):
409         "to know if a callback has been defined or not"
410         if self._callback is None:
411             return False
412         else:
413             return True
414
415     def impl_getkey(self, value):
416         return value
417
418     def impl_is_multi(self):
419         return self._multi
420
421     def impl_add_consistency(self, func, opt):
422         if self._consistencies is None:
423             self._consistencies = []
424         if not isinstance(opt, Option):
425             raise ValueError('consistency must be set with an option')
426         if self is opt:
427             raise ValueError('cannot add consistency with itself')
428         if self.impl_is_multi() != opt.impl_is_multi():
429             raise ValueError('options in consistency'
430                              ' should be multi in two sides')
431         func = '_cons_{0}'.format(func)
432         self._launch_consistency(func,
433                                  self,
434                                  self.impl_getdefault(),
435                                  None, None, opt)
436         self._consistencies.append((func, opt))
437         self.impl_validate(self.impl_getdefault())
438
439     def _cons_not_equal(self, optname, value, value_):
440         if value == value_:
441             raise ValueError(_("invalid value {0} for option {1} "
442                                "must be different as {2} option"
443                                "").format(value, self._name, optname))
444
445
446 class ChoiceOption(Option):
447     """represents a choice out of several objects.
448
449     The option can also have the value ``None``
450     """
451
452     __slots__ = ('_values', '_open_values')
453     _opt_type = 'string'
454
455     def __init__(self, name, doc, values, default=None, default_multi=None,
456                  requires=None, multi=False, callback=None,
457                  callback_params=None, open_values=False, validator=None,
458                  validator_args=None, properties=()):
459         """
460         :param values: is a list of values the option can possibly take
461         """
462         if not isinstance(values, tuple):
463             raise TypeError(_('values must be a tuple for {0}').format(name))
464         self._values = values
465         if open_values not in (True, False):
466             raise TypeError(_('open_values must be a boolean for '
467                             '{0}').format(name))
468         self._open_values = open_values
469         super(ChoiceOption, self).__init__(name, doc, default=default,
470                                            default_multi=default_multi,
471                                            callback=callback,
472                                            callback_params=callback_params,
473                                            requires=requires,
474                                            multi=multi,
475                                            validator=validator,
476                                            validator_args=validator_args,
477                                            properties=properties)
478
479     def impl_get_values(self):
480         return self._values
481
482     def impl_is_openvalues(self):
483         return self._open_values
484
485     def _validate(self, value):
486         if not self._open_values and not value in self._values:
487             raise ValueError(_('value {0} is not permitted, '
488                                'only {1} is allowed'
489                                '').format(value, self._values))
490
491
492 class BoolOption(Option):
493     "represents a choice between ``True`` and ``False``"
494     __slots__ = tuple()
495     _opt_type = 'bool'
496
497     def _validate(self, value):
498         if not isinstance(value, bool):
499             raise ValueError(_('value must be a boolean'))
500
501
502 class IntOption(Option):
503     "represents a choice of an integer"
504     __slots__ = tuple()
505     _opt_type = 'int'
506
507     def _validate(self, value):
508         if not isinstance(value, int):
509             raise ValueError(_('value must be an integer'))
510
511
512 class FloatOption(Option):
513     "represents a choice of a floating point number"
514     __slots__ = tuple()
515     _opt_type = 'float'
516
517     def _validate(self, value):
518         if not isinstance(value, float):
519             raise ValueError(_('value must be a float'))
520
521
522 class StrOption(Option):
523     "represents the choice of a string"
524     __slots__ = tuple()
525     _opt_type = 'string'
526
527     def _validate(self, value):
528         if not isinstance(value, str):
529             raise ValueError(_('value must be a string, not '
530                                '{0}').format(type(value)))
531
532
533 if sys.version_info[0] >= 3:
534     #UnicodeOption is same has StrOption in python 3+
535     class UnicodeOption(StrOption):
536         __slots__ = tuple()
537         pass
538 else:
539     class UnicodeOption(Option):
540         "represents the choice of a unicode string"
541         __slots__ = tuple()
542         _opt_type = 'unicode'
543         _empty = u''
544
545         def _validate(self, value):
546             if not isinstance(value, unicode):
547                 raise ValueError(_('value must be an unicode'))
548
549
550 class SymLinkOption(BaseOption):
551     __slots__ = ('_name', '_opt', '_state_opt')
552     _opt_type = 'symlink'
553
554     def __init__(self, name, opt):
555         self._name = name
556         if not isinstance(opt, Option):
557             raise ValueError(_('malformed symlinkoption '
558                                'must be an option '
559                                'for symlink {0}').format(name))
560         self._opt = opt
561         self._readonly = True
562
563     def __getattr__(self, name):
564         if name in ('_name', '_opt', '_opt_type', '_readonly'):
565             return object.__getattr__(self, name)
566         else:
567             return getattr(self._opt, name)
568
569     def _impl_getstate(self, descr):
570         super(SymLinkOption, self)._impl_getstate(descr)
571         self._state_opt = descr.impl_get_path_by_opt(self._opt)
572
573
574 class IPOption(Option):
575     "represents the choice of an ip"
576     __slots__ = ('_only_private',)
577     _opt_type = 'ip'
578
579     def __init__(self, name, doc, default=None, default_multi=None,
580                  requires=None, multi=False, callback=None,
581                  callback_params=None, validator=None, validator_args=None,
582                  properties=None, only_private=False):
583         self._only_private = only_private
584         super(IPOption, self).__init__(name, doc, default=default,
585                                        default_multi=default_multi,
586                                        callback=callback,
587                                        callback_params=callback_params,
588                                        requires=requires,
589                                        multi=multi,
590                                        validator=validator,
591                                        validator_args=validator_args,
592                                        properties=properties)
593
594     def _validate(self, value):
595         ip = IP('{0}/32'.format(value))
596         if ip.iptype() == 'RESERVED':
597             raise ValueError(_("IP mustn't not be in reserved class"))
598         if self._only_private and not ip.iptype() == 'PRIVATE':
599             raise ValueError(_("IP must be in private class"))
600
601
602 class PortOption(Option):
603     """represents the choice of a port
604     The port numbers are divided into three ranges:
605     the well-known ports,
606     the registered ports,
607     and the dynamic or private ports.
608     You can actived this three range.
609     Port number 0 is reserved and can't be used.
610     see: http://en.wikipedia.org/wiki/Port_numbers
611     """
612     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
613     _opt_type = 'port'
614
615     def __init__(self, name, doc, default=None, default_multi=None,
616                  requires=None, multi=False, callback=None,
617                  callback_params=None, validator=None, validator_args=None,
618                  properties=None, allow_range=False, allow_zero=False,
619                  allow_wellknown=True, allow_registred=True,
620                  allow_private=False):
621         self._allow_range = allow_range
622         self._min_value = None
623         self._max_value = None
624         ports_min = [0, 1, 1024, 49152]
625         ports_max = [0, 1023, 49151, 65535]
626         is_finally = False
627         for index, allowed in enumerate([allow_zero,
628                                          allow_wellknown,
629                                          allow_registred,
630                                          allow_private]):
631             if self._min_value is None:
632                 if allowed:
633                     self._min_value = ports_min[index]
634             elif not allowed:
635                 is_finally = True
636             elif allowed and is_finally:
637                 raise ValueError(_('inconsistency in allowed range'))
638             if allowed:
639                 self._max_value = ports_max[index]
640
641         if self._max_value is None:
642             raise ValueError(_('max value is empty'))
643
644         super(PortOption, self).__init__(name, doc, default=default,
645                                          default_multi=default_multi,
646                                          callback=callback,
647                                          callback_params=callback_params,
648                                          requires=requires,
649                                          multi=multi,
650                                          validator=validator,
651                                          validator_args=validator_args,
652                                          properties=properties)
653
654     def _validate(self, value):
655         if self._allow_range and ":" in str(value):
656             value = str(value).split(':')
657             if len(value) != 2:
658                 raise ValueError('range must have two values only')
659             if not value[0] < value[1]:
660                 raise ValueError('first port in range must be'
661                                  ' smaller than the second one')
662         else:
663             value = [value]
664
665         for val in value:
666             if not self._min_value <= int(val) <= self._max_value:
667                 raise ValueError('port must be an between {0} and {1}'
668                                  ''.format(self._min_value, self._max_value))
669
670
671 class NetworkOption(Option):
672     "represents the choice of a network"
673     __slots__ = tuple()
674     _opt_type = 'network'
675
676     def _validate(self, value):
677         ip = IP(value)
678         if ip.iptype() == 'RESERVED':
679             raise ValueError(_("network mustn't not be in reserved class"))
680
681
682 class NetmaskOption(Option):
683     "represents the choice of a netmask"
684     __slots__ = tuple()
685     _opt_type = 'netmask'
686
687     def _validate(self, value):
688         IP('0.0.0.0/{0}'.format(value))
689
690     def _cons_network_netmask(self, optname, value, value_):
691         #opts must be (netmask, network) options
692         self.__cons_netmask(optname, value, value_, False)
693
694     def _cons_ip_netmask(self, optname, value, value_):
695         #opts must be (netmask, ip) options
696         self.__cons_netmask(optname, value, value_, True)
697
698     #def __cons_netmask(self, opt, value, context, index, opts, make_net):
699     def __cons_netmask(self, optname, val_netmask, val_ipnetwork, make_net):
700         msg = None
701         try:
702             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
703                     make_net=make_net)
704             #if cidr == 32, ip same has network
705             if ip.prefixlen() != 32:
706                 try:
707                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
708                         make_net=not make_net)
709                 except ValueError:
710                     if not make_net:
711                         msg = _("invalid network {0} ({1}) "
712                                 "with netmask {2} ({3}),"
713                                 " this network is an IP")
714                 else:
715                     if make_net:
716                         msg = _("invalid IP {0} ({1}) with netmask {2} ({3}),"
717                                 " this IP is a network")
718
719         except ValueError:
720             if make_net:
721                 msg = _("invalid IP {0} ({1}) with netmask {2} ({3})")
722             else:
723                 msg = _("invalid network {0} ({1}) with netmask {2} ({3})")
724         if msg is not None:
725             raise ValueError(msg.format(val_ipnetwork, optname,
726                                         val_netmask, self._name))
727
728
729 class DomainnameOption(Option):
730     "represents the choice of a domain name"
731     __slots__ = ('_type', '_allow_ip')
732     _opt_type = 'domainname'
733
734     def __init__(self, name, doc, default=None, default_multi=None,
735                  requires=None, multi=False, callback=None,
736                  callback_params=None, validator=None, validator_args=None,
737                  properties=None, allow_ip=False, type_='domainname'):
738         #netbios: for MS domain
739         #hostname: to identify the device
740         #domainname:
741         #fqdn: with tld, not supported yet
742         if type_ not in ['netbios', 'hostname', 'domainname']:
743             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
744         self._type = type_
745         if allow_ip not in [True, False]:
746             raise ValueError(_('allow_ip must be a boolean'))
747         self._allow_ip = allow_ip
748         super(DomainnameOption, self).__init__(name, doc, default=default,
749                                                default_multi=default_multi,
750                                                callback=callback,
751                                                callback_params=callback_params,
752                                                requires=requires,
753                                                multi=multi,
754                                                validator=validator,
755                                                validator_args=validator_args,
756                                                properties=properties)
757
758     def _validate(self, value):
759         if self._allow_ip is True:
760             try:
761                 IP('{0}/32'.format(value))
762                 return
763             except ValueError:
764                 pass
765         if self._type == 'netbios':
766             length = 15
767             extrachar = ''
768         elif self._type == 'hostname':
769             length = 63
770             extrachar = ''
771         elif self._type == 'domainname':
772             length = 255
773             extrachar = '\.'
774             if '.' not in value:
775                 raise ValueError(_("invalid value for {0}, must have dot"
776                                    "").format(self._name))
777         if len(value) > length:
778             raise ValueError(_("invalid domainname's length for"
779                                " {0} (max {1})").format(self._name, length))
780         if len(value) == 1:
781             raise ValueError(_("invalid domainname's length for {0} (min 2)"
782                                "").format(self._name))
783         regexp = r'^[a-z]([a-z\d{0}-])*[a-z\d]$'.format(extrachar)
784         if re.match(regexp, value) is None:
785             raise ValueError(_('invalid domainname'))
786
787
788 class OptionDescription(BaseOption):
789     """Config's schema (organisation, group) and container of Options
790     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
791     """
792     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
793                  '_state_group_type', '_properties', '_children',
794                  '_consistencies', '_calc_properties', '__weakref__',
795                  '_readonly', '_impl_informations')
796     _opt_type = 'optiondescription'
797
798     def __init__(self, name, doc, children, requires=None, properties=None):
799         """
800         :param children: a list of options (including optiondescriptions)
801
802         """
803         if not valid_name(name):
804             raise ValueError(_("invalid name:"
805                                " {0} for optiondescription").format(name))
806         self._name = name
807         self._impl_informations = {}
808         self.impl_set_information('doc', doc)
809         child_names = [child._name for child in children]
810         #better performance like this
811         valid_child = copy(child_names)
812         valid_child.sort()
813         old = None
814         for child in valid_child:
815             if child == old:
816                 raise ConflictError(_('duplicate option name: '
817                                       '{0}').format(child))
818             old = child
819         self._children = (tuple(child_names), tuple(children))
820         self._calc_properties, self._requires = validate_requires_arg(requires, self._name)
821         self._cache_paths = None
822         self._consistencies = None
823         if properties is None:
824             properties = tuple()
825         if not isinstance(properties, tuple):
826             raise TypeError(_('invalid properties type {0} for {1},'
827                               ' must be a tuple').format(type(properties),
828                                                          self._name))
829         self._properties = properties  # 'hidden', 'disabled'...
830         # the group_type is useful for filtering OptionDescriptions in a config
831         self._group_type = groups.default
832
833     def impl_getdoc(self):
834         return self.impl_get_information('doc')
835
836     def __getattr__(self, name):
837         if name in self.__slots__:
838             return object.__getattribute__(self, name)
839         try:
840             return self._children[1][self._children[0].index(name)]
841         except ValueError:
842             raise AttributeError(_('unknown Option {0} '
843                                    'in OptionDescription {1}'
844                                    '').format(name, self._name))
845
846     def impl_getkey(self, config):
847         return tuple([child.impl_getkey(getattr(config, child._name))
848                       for child in self.impl_getchildren()])
849
850     def impl_getpaths(self, include_groups=False, _currpath=None):
851         """returns a list of all paths in self, recursively
852            _currpath should not be provided (helps with recursion)
853         """
854         if _currpath is None:
855             _currpath = []
856         paths = []
857         for option in self.impl_getchildren():
858             attr = option._name
859             if isinstance(option, OptionDescription):
860                 if include_groups:
861                     paths.append('.'.join(_currpath + [attr]))
862                 paths += option.impl_getpaths(include_groups=include_groups,
863                                               _currpath=_currpath + [attr])
864             else:
865                 paths.append('.'.join(_currpath + [attr]))
866         return paths
867
868     def impl_getchildren(self):
869         return self._children[1]
870
871     def impl_build_cache(self,
872                          cache_path=None,
873                          cache_option=None,
874                          _currpath=None,
875                          _consistencies=None):
876         if _currpath is None and self._cache_paths is not None:
877             # cache already set
878             return
879         if _currpath is None:
880             save = True
881             _currpath = []
882             _consistencies = {}
883         else:
884             save = False
885         if cache_path is None:
886             cache_path = []
887             cache_option = []
888         for option in self.impl_getchildren():
889             attr = option._name
890             if option in cache_option:
891                 raise ConflictError(_('duplicate option: {0}').format(option))
892
893             cache_option.append(option)
894             option._readonly = True
895             cache_path.append(str('.'.join(_currpath + [attr])))
896             if not isinstance(option, OptionDescription):
897                 if option._consistencies is not None:
898                     for consistency in option._consistencies:
899                         func, opt = consistency
900                         opts = (option, opt)
901                         _consistencies.setdefault(opt,
902                                                   []).append((func, opts))
903                         _consistencies.setdefault(option,
904                                                   []).append((func, opts))
905             else:
906                 _currpath.append(attr)
907                 option.impl_build_cache(cache_path,
908                                         cache_option,
909                                         _currpath,
910                                         _consistencies)
911                 _currpath.pop()
912         if save:
913             self._cache_paths = (tuple(cache_option), tuple(cache_path))
914             self._consistencies = _consistencies
915             self._readonly = True
916
917     def impl_get_opt_by_path(self, path):
918         try:
919             return self._cache_paths[0][self._cache_paths[1].index(path)]
920         except ValueError:
921             raise AttributeError(_('no option for path {0}').format(path))
922
923     def impl_get_path_by_opt(self, opt):
924         try:
925             return self._cache_paths[1][self._cache_paths[0].index(opt)]
926         except ValueError:
927             raise AttributeError(_('no option {0} found').format(opt))
928
929     # ____________________________________________________________
930     def impl_set_group_type(self, group_type):
931         """sets a given group object to an OptionDescription
932
933         :param group_type: an instance of `GroupType` or `MasterGroupType`
934                               that lives in `setting.groups`
935         """
936         if self._group_type != groups.default:
937             raise TypeError(_('cannot change group_type if already set '
938                             '(old {0}, new {1})').format(self._group_type,
939                                                          group_type))
940         if isinstance(group_type, groups.GroupType):
941             self._group_type = group_type
942             if isinstance(group_type, groups.MasterGroupType):
943                 #if master (same name has group) is set
944                 identical_master_child_name = False
945                 #for collect all slaves
946                 slaves = []
947                 master = None
948                 for child in self.impl_getchildren():
949                     if isinstance(child, OptionDescription):
950                         raise ValueError(_("master group {0} shall not have "
951                                          "a subgroup").format(self._name))
952                     if isinstance(child, SymLinkOption):
953                         raise ValueError(_("master group {0} shall not have "
954                                          "a symlinkoption").format(self._name))
955                     if not child.impl_is_multi():
956                         raise ValueError(_("not allowed option {0} "
957                                          "in group {1}"
958                                          ": this option is not a multi"
959                                          "").format(child._name, self._name))
960                     if child._name == self._name:
961                         identical_master_child_name = True
962                         child._multitype = multitypes.master
963                         master = child
964                     else:
965                         slaves.append(child)
966                 if master is None:
967                     raise ValueError(_('master group with wrong'
968                                        ' master name for {0}'
969                                        ).format(self._name))
970                 master._master_slaves = tuple(slaves)
971                 for child in self.impl_getchildren():
972                     if child != master:
973                         child._master_slaves = master
974                         child._multitype = multitypes.slave
975                 if not identical_master_child_name:
976                     raise ValueError(_("no child has same nom has master group"
977                                        " for: {0}").format(self._name))
978         else:
979             raise ValueError(_('group_type: {0}'
980                                ' not allowed').format(group_type))
981
982     def impl_get_group_type(self):
983         return self._group_type
984
985     def _valid_consistency(self, opt, value, context=None, index=None):
986         consistencies = self._consistencies.get(opt)
987         if consistencies is not None:
988             for consistency in consistencies:
989                 opt_ = consistency[1]
990                 ret = opt_[0]._launch_consistency(consistency[0],
991                                                   opt,
992                                                   value,
993                                                   context,
994                                                   index,
995                                                   opt_[1])
996                 if ret is False:
997                     return False
998         return True
999
1000     def _impl_getstate(self, descr=None):
1001         if descr is None:
1002             self.impl_build_cache()
1003             descr = self
1004         super(OptionDescription, self)._impl_getstate(descr)
1005         self._state_group_type = str(self._group_type)
1006         for option in self.impl_getchildren():
1007             option._impl_getstate(descr)
1008
1009     def __getstate__(self, export=False):
1010         if not export:
1011             self._impl_getstate()
1012         states = super(OptionDescription, self).__getstate__(True)
1013         states['_state_children'] = []
1014         for option in self.impl_getchildren():
1015             states['_state_children'].append(option.__getstate__(True))
1016         return states
1017
1018
1019 def validate_requires_arg(requires, name):
1020     """check malformed requirements
1021     and tranform dict to internal tuple
1022
1023     :param requires: have a look at the
1024                      :meth:`tiramisu.setting.Settings.apply_requires` method to
1025                      know more about
1026                      the description of the requires dictionary
1027     """
1028     if requires is None:
1029         return None, None
1030     ret_requires = {}
1031     config_action = {}
1032
1033     for require in requires:
1034         if not type(require) == dict:
1035             raise ValueError(_("malformed requirements type for option:"
1036                                " {0}, must be a dict").format(name))
1037         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1038                       'same_action')
1039         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1040         if unknown_keys != frozenset():
1041             raise ValueError('malformed requirements for option: {0}'
1042                              ' unknown keys {1}, must only '
1043                              '{2}'.format(name,
1044                                           unknown_keys,
1045                                           valid_keys))
1046         try:
1047             option = require['option']
1048             expected = require['expected']
1049             action = require['action']
1050         except KeyError:
1051             raise ValueError(_("malformed requirements for option: {0}"
1052                                " require must have option, expected and"
1053                                " action keys").format(name))
1054         inverse = require.get('inverse', False)
1055         if inverse not in [True, False]:
1056             raise ValueError(_('malformed requirements for option: {0}'
1057                                ' inverse must be boolean'))
1058         transitive = require.get('transitive', True)
1059         if transitive not in [True, False]:
1060             raise ValueError(_('malformed requirements for option: {0}'
1061                                ' transitive must be boolean'))
1062         same_action = require.get('same_action', True)
1063         if same_action not in [True, False]:
1064             raise ValueError(_('malformed requirements for option: {0}'
1065                                ' same_action must be boolean'))
1066
1067         if not isinstance(option, Option):
1068             raise ValueError(_('malformed requirements '
1069                                'must be an option in option {0}').format(name))
1070         if option.impl_is_multi():
1071             raise ValueError(_('malformed requirements option {0} '
1072                                'should not be a multi').format(name))
1073         if expected is not None:
1074             try:
1075                 option._validate(expected)
1076             except ValueError as err:
1077                 raise ValueError(_('malformed requirements second argument '
1078                                    'must be valid for option {0}'
1079                                    ': {1}').format(name, err))
1080         if action in config_action:
1081             if inverse != config_action[action]:
1082                 raise ValueError(_("inconsistency in action types"
1083                                    " for option: {0}"
1084                                    " action: {1}").format(name, action))
1085         else:
1086             config_action[action] = inverse
1087         if action not in ret_requires:
1088             ret_requires[action] = {}
1089         if option not in ret_requires[action]:
1090             ret_requires[action][option] = (option, [expected], action,
1091                                             inverse, transitive, same_action)
1092         else:
1093             ret_requires[action][option][1].append(expected)
1094     ret = []
1095     for opt_requires in ret_requires.values():
1096         ret_action = []
1097         for require in opt_requires.values():
1098             req = (require[0], tuple(require[1]), require[2], require[3],
1099                    require[4], require[5])
1100             ret_action.append(req)
1101         ret.append(tuple(ret_action))
1102     return frozenset(config_action.keys()), tuple(ret)