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