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