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