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