review exception
[tiramisu.git] / tiramisu / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description for the configuration management"
3 # Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 # The original `Config` design model is unproudly borrowed from
20 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
23 import re
24 from copy import copy
25 from types import FunctionType
26 from IPy import IP
27
28 from tiramisu.error import ConflictConfigError
29 from tiramisu.setting import groups, multitypes
30 from tiramisu.i18n import _
31
32 name_regexp = re.compile(r'^\d+')
33
34
35 def valid_name(name):
36     try:
37         name = str(name)
38     except:
39         return False
40     if re.match(name_regexp, name) is None:
41         return True
42     else:
43         return False
44 #____________________________________________________________
45 #
46
47
48 class BaseInformation(object):
49     __slots__ = ('_informations')
50
51     def set_information(self, key, value):
52         """updates the information's attribute
53         (wich is a dictionnary)
54
55         :param key: information's key (ex: "help", "doc"
56         :param value: information's value (ex: "the help string")
57         """
58         self._informations[key] = value
59
60     def get_information(self, key, default=None):
61         """retrieves one information's item
62
63         :param key: the item string (ex: "help")
64         """
65         if key in self._informations:
66             return self._informations[key]
67         elif default is not None:
68             return default
69         else:
70             raise ValueError(_("Information's item not found: {0}").format(key))
71
72
73 class Option(BaseInformation):
74     """
75     Abstract base class for configuration option's.
76
77     Reminder: an Option object is **not** a container for the value
78     """
79     __slots__ = ('_name', '_requires', '_multi', '_validator', '_default_multi',
80                  '_default', '_properties', '_callback', '_multitype',
81                  '_master_slaves', '_consistencies')
82
83     def __init__(self, name, doc, default=None, default_multi=None,
84                  requires=None, multi=False, callback=None,
85                  callback_params=None, validator=None, validator_args=None,
86                  properties=None):
87         #FIXME : validation de callback et callback_params !!!
88         """
89         :param name: the option's name
90         :param doc: the option's description
91         :param default: specifies the default value of the option,
92                         for a multi : ['bla', 'bla', 'bla']
93         :param default_multi: 'bla' (used in case of a reset to default only at
94                         a given index)
95         :param requires: is a list of names of options located anywhere
96                          in the configuration.
97         :param multi: if true, the option's value is a list
98         :param callback: the name of a function. If set, the function's output
99                          is responsible of the option's value
100         :param callback_params: the callback's parameter
101         :param validator: the name of a function wich stands for a custom
102                           validation of the value
103         :param validator_args: the validator's parameters
104         """
105         if not valid_name(name):
106             raise ValueError(_("invalid name: {0} for option").format(name))
107         self._name = name
108         self._informations = {}
109         self.set_information('doc', doc)
110         validate_requires_arg(requires, self._name)
111         self._requires = requires
112         self._multi = multi
113         self._consistencies = None
114         if validator is not None:
115             if type(validator) != FunctionType:
116                 raise TypeError(_("validator must be a function"))
117             if validator_args is None:
118                 validator_args = {}
119             self._validator = (validator, validator_args)
120         else:
121             self._validator = None
122         if not self._multi and default_multi is not None:
123             raise ValueError(_("a default_multi is set whereas multi is False"
124                              " in option: {0}").format(name))
125         if default_multi is not None and not self._validate(default_multi):
126             raise ValueError(_("invalid default_multi value {0} "
127                              "for option {1}").format(str(default_multi), name))
128         if callback is not None and (default is not None or default_multi is not None):
129             raise ValueError(_("defaut values not allowed if option: {0} "
130                              "is calculated").format(name))
131         if callback is None and callback_params is not None:
132             raise ValueError(_("params defined for a callback function but "
133                              "no callback defined yet for option {0}").format(name))
134         if callback is not None:
135             self._callback = (callback, callback_params)
136         else:
137             self._callback = None
138         if self._multi:
139             if default is None:
140                 default = []
141             #if not isinstance(default, list):
142             #    raise ValidateError("invalid default value {0} "
143             #                        "for option {1} : not list type"
144             #                        "".format(str(default), name))
145             if not self.validate(default):
146                 raise ValueError(_("invalid default value {0} "
147                                  "for option {1}"
148                                  "").format(str(default), name))
149             self._multitype = multitypes.default
150             self._default_multi = default_multi
151         else:
152             if default is not None and not self.validate(default):
153                 raise ValueError(_("invalid default value {0} "
154                                  "for option {1}").format(str(default), name))
155         self._default = default
156         if properties is None:
157             properties = ()
158         if not isinstance(properties, tuple):
159             raise TypeError(_('invalid properties type {0} for {1},'
160                             ' must be a tuple').format(type(properties), self._name))
161         self._properties = properties  # 'hidden', 'disabled'...
162
163     def validate(self, value, context=None, validate=True):
164         """
165         :param value: the option's value
166         :param validate: if true enables ``self._validator`` validation
167         """
168         # generic calculation
169         if context is not None:
170             cons = context.cfgimpl_get_description()
171         else:
172             cons = None
173         if not self._multi:
174             # None allows the reset of the value
175             if value is not None:
176                 # customizing the validator
177                 if validate and self._validator is not None and \
178                         not self._validator[0](value, **self._validator[1]):
179                     return False
180                 if not self._validate(value):
181                     return False
182                 if cons is not None:
183                     return cons.valid_consistency(self, value, context, None)
184         else:
185             if not isinstance(value, list):
186                 raise ValueError(_("invalid value {0} "
187                                     "for option {1} which must be a list"
188                                     "").format(value, self._name))
189             for index in range(0, len(value)):
190                 val = value[index]
191                 # None allows the reset of the value
192                 if val is not None:
193                     # customizing the validator
194                     if validate and self._validator is not None and \
195                             not self._validator[0](val, **self._validator[1]):
196                         return False
197                     if not self._validate(val):
198                         return False
199                     if cons is not None and not cons.valid_consistency(self, val, context, index):
200                         return False
201         return True
202
203     def getdefault(self, default_multi=False):
204         "accessing the default value"
205         if not default_multi or not self.is_multi():
206             return self._default
207         else:
208             return self.getdefault_multi()
209
210     def getdefault_multi(self):
211         "accessing the default value for a multi"
212         return self._default_multi
213
214     def get_multitype(self):
215         return self._multitype
216
217     def get_master_slaves(self):
218         return self._master_slaves
219
220     def is_empty_by_default(self):
221         "no default value has been set yet"
222         if ((not self.is_multi() and self._default is None) or
223                 (self.is_multi() and (self._default == [] or None in self._default))):
224             return True
225         return False
226
227     def getdoc(self):
228         "accesses the Option's doc"
229         return self.get_information('doc')
230
231     def has_callback(self):
232         "to know if a callback has been defined or not"
233         if self._callback is None:
234             return False
235         else:
236             return True
237
238     def reset(self, config):
239         """resets the default value and owner
240         """
241         config._cfgimpl_context._cfgimpl_values.reset(self)
242
243     def getkey(self, value):
244         return value
245
246     def is_multi(self):
247         return self._multi
248
249     def cons_not_equal(self, opt, value, context, index, opts):
250         values = [value]
251         descr = context.cfgimpl_get_description()
252         for opt_ in opts:
253             if opt_ is not opt:
254                 path = descr.get_path_by_opt(opt_)
255                 val = context._getattr(path, validate=False)
256                 if val is not None:
257                     if val in values:
258                         return False
259                     values.append(val)
260         return True
261
262     def add_consistency(self, func, opts):
263         pass
264         if self._consistencies is None:
265             self._consistencies = []
266         if self not in opts:
267             opts = list(opts)
268             opts.append(self)
269             opts = tuple(opts)
270         self._consistencies.append(('cons_{}'.format(func), opts))
271
272
273 class ChoiceOption(Option):
274     __slots__ = ('_values', '_open_values', 'opt_type')
275     opt_type = 'string'
276
277     def __init__(self, name, doc, values, default=None, default_multi=None,
278                  requires=None, multi=False, callback=None,
279                  callback_params=None, open_values=False, validator=None,
280                  validator_args=None, properties=()):
281         if not isinstance(values, tuple):
282             raise TypeError(_('values must be a tuple for {0}').format(name))
283         self._values = values
284         if open_values not in (True, False):
285             raise TypeError(_('open_values must be a boolean for '
286                             '{0}').format(name))
287         self._open_values = open_values
288         super(ChoiceOption, self).__init__(name, doc, default=default,
289                                            default_multi=default_multi,
290                                            callback=callback,
291                                            callback_params=callback_params,
292                                            requires=requires,
293                                            multi=multi,
294                                            validator=validator,
295                                            validator_args=validator_args,
296                                            properties=properties)
297
298     def _validate(self, value):
299         if not self._open_values:
300             return value is None or value in self._values
301         else:
302             return True
303
304
305 class BoolOption(Option):
306     __slots__ = ('opt_type')
307     opt_type = 'bool'
308
309     def _validate(self, value):
310         return isinstance(value, bool)
311
312
313 class IntOption(Option):
314     __slots__ = ('opt_type')
315     opt_type = 'int'
316
317     def _validate(self, value):
318         return isinstance(value, int)
319
320
321 class FloatOption(Option):
322     __slots__ = ('opt_type')
323     opt_type = 'float'
324
325     def _validate(self, value):
326         return isinstance(value, float)
327
328
329 class StrOption(Option):
330     __slots__ = ('opt_type')
331     opt_type = 'string'
332
333     def _validate(self, value):
334         return isinstance(value, str)
335
336
337 class UnicodeOption(Option):
338     __slots__ = ('opt_type')
339     opt_type = 'unicode'
340
341     def _validate(self, value):
342         return isinstance(value, unicode)
343
344
345 class SymLinkOption(object):
346     __slots__ = ('_name', 'opt', '_consistencies')
347     opt_type = 'symlink'
348     _consistencies = None
349
350     def __init__(self, name, path, opt):
351         self._name = name
352         self.opt = opt
353
354     def setoption(self, context, value):
355         path = context.cfgimpl_get_description().get_path_by_opt(self.opt)
356         setattr(context, path, value)
357
358     def __getattr__(self, name):
359         if name in ('_name', 'opt', 'setoption'):
360             return object.__gettattr__(self, name)
361         else:
362             return getattr(self.opt, name)
363
364
365 class IPOption(Option):
366     __slots__ = ('opt_type', '_only_private')
367     opt_type = 'ip'
368
369     def set_private(self):
370         self._only_private = True
371
372     def _validate(self, value):
373         try:
374             only_private = self._only_private
375         except AttributeError:
376             only_private = False
377         try:
378             ip = IP('{0}/32'.format(value))
379             if only_private:
380                 return ip.iptype() == 'PRIVATE'
381             return True
382         except ValueError:
383             return False
384
385
386 class NetworkOption(Option):
387     __slots__ = ('opt_type')
388     opt_type = 'network'
389
390     def _validate(self, value):
391         try:
392             IP(value)
393             return True
394         except ValueError:
395             return False
396
397
398 class NetmaskOption(Option):
399     __slots__ = ('opt_type')
400     opt_type = 'netmask'
401
402     def __init__(self, name, doc, default=None, default_multi=None,
403                  requires=None, multi=False, callback=None,
404                  callback_params=None, validator=None, validator_args=None,
405                  properties=None, opt_ip=None):
406         if opt_ip is not None and not isinstance(opt_ip, IPOption) and \
407                 not isinstance(opt_ip, NetworkOption):
408             raise TypeError(_('opt_ip must be a IPOption not {}').format(type(opt_ip)))
409         super(NetmaskOption, self).__init__(name, doc, default=default,
410                                             default_multi=default_multi,
411                                             callback=callback,
412                                             callback_params=callback_params,
413                                             requires=requires,
414                                             multi=multi,
415                                             validator=validator,
416                                             validator_args=validator_args,
417                                             properties=properties)
418         if opt_ip is None:
419             pass
420         elif isinstance(opt_ip, IPOption):
421             self._consistencies = [('cons_ip_netmask', (self, opt_ip))]
422         elif isinstance(opt_ip, NetworkOption):
423             self._consistencies = [('cons_network_netmask', (self, opt_ip))]
424         else:
425             raise TypeError(_('unknown type for opt_ip'))
426
427     def _validate(self, value):
428         try:
429             IP('0.0.0.0/{}'.format(value))
430             return True
431         except ValueError:
432             return False
433
434     def cons_network_netmask(self, opt, value, context, index, opts):
435         #opts must be (netmask, network) options
436         return self._cons_netmask(opt, value, context, index, opts, False)
437
438     def cons_ip_netmask(self, opt, value, context, index, opts):
439         #opts must be (netmask, ip) options
440         return self._cons_netmask(opt, value, context, index, opts, True)
441
442     def _cons_netmask(self, opt, value, context, index, opts, make_net):
443         opt_netmask, opt_ipnetwork = opts
444         descr = context.cfgimpl_get_description()
445         if opt is opt_ipnetwork:
446             val_ipnetwork = value
447             path = descr.get_path_by_opt(opt_netmask)
448             val_netmask = context._getattr(path, validate=False)
449             if opt_netmask.is_multi():
450                 val_netmask = val_netmask[index]
451             if val_netmask is None:
452                 return True
453         else:
454             val_netmask = value
455             path = descr.get_path_by_opt(opt_ipnetwork)
456             val_ipnetwork = getattr(context, path)
457             if opt_ipnetwork.is_multi():
458                 val_ipnetwork = val_ipnetwork[index]
459             if val_ipnetwork is None:
460                 return True
461         try:
462             IP('{}/{}'.format(val_ipnetwork, val_netmask, make_net=make_net))
463             return True
464         except ValueError:
465             return False
466
467
468 class OptionDescription(BaseInformation):
469     """Config's schema (organisation, group) and container of Options"""
470     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
471                  '_properties', '_children', '_consistencies')
472
473     def __init__(self, name, doc, children, requires=None, properties=()):
474         """
475         :param children: is a list of option descriptions (including
476         ``OptionDescription`` instances for nested namespaces).
477         """
478         if not valid_name(name):
479             raise ValueError(_("invalid name: {0} for option descr").format(name))
480         self._name = name
481         self._informations = {}
482         self.set_information('doc', doc)
483         child_names = [child._name for child in children]
484         #better performance like this
485         valid_child = copy(child_names)
486         valid_child.sort()
487         old = None
488         for child in valid_child:
489             if child == old:
490                 raise ConflictConfigError(_('duplicate option name: '
491                                           '{0}').format(child))
492             old = child
493         self._children = (tuple(child_names), tuple(children))
494         validate_requires_arg(requires, self._name)
495         self._requires = requires
496         self._cache_paths = None
497         self._consistencies = None
498         if not isinstance(properties, tuple):
499             raise TypeError(_('invalid properties type {0} for {1},'
500                               ' must be a tuple').format(type(properties), self._name))
501         self._properties = properties  # 'hidden', 'disabled'...
502         # the group_type is useful for filtering OptionDescriptions in a config
503         self._group_type = groups.default
504
505     def getdoc(self):
506         return self.get_information('doc')
507
508     def __getattr__(self, name):
509         try:
510             return self._children[1][self._children[0].index(name)]
511         except ValueError:
512             raise AttributeError(_('unknown Option {} in OptionDescription {}'
513                                  '').format(name, self._name))
514
515     def getkey(self, config):
516         return tuple([child.getkey(getattr(config, child._name))
517                       for child in self._children[1]])
518
519     def getpaths(self, include_groups=False, _currpath=None):
520         """returns a list of all paths in self, recursively
521            _currpath should not be provided (helps with recursion)
522         """
523         #FIXME : cache
524         if _currpath is None:
525             _currpath = []
526         paths = []
527         for option in self._children[1]:
528             attr = option._name
529             if isinstance(option, OptionDescription):
530                 if include_groups:
531                     paths.append('.'.join(_currpath + [attr]))
532                 paths += option.getpaths(include_groups=include_groups,
533                                          _currpath=_currpath + [attr])
534             else:
535                 paths.append('.'.join(_currpath + [attr]))
536         return paths
537
538     def getchildren(self):
539         return self._children[1]
540
541     def build_cache(self, cache_path=None, cache_option=None, _currpath=None, _consistencies=None):
542         if _currpath is None and self._cache_paths is not None:
543             return
544         if _currpath is None:
545             save = True
546             _currpath = []
547             _consistencies = {}
548         else:
549             save = False
550         if cache_path is None:
551             cache_path = [self._name]
552             cache_option = [self]
553         for option in self._children[1]:
554             attr = option._name
555             if attr.startswith('_cfgimpl'):
556                 continue
557             cache_option.append(option)
558             cache_path.append(str('.'.join(_currpath + [attr])))
559             if not isinstance(option, OptionDescription):
560                 if option._consistencies is not None:
561                     for consistency in option._consistencies:
562                         func, opts = consistency
563                         for opt in opts:
564                             _consistencies.setdefault(opt, []).append((func, opts))
565             else:
566                 _currpath.append(attr)
567                 option.build_cache(cache_path, cache_option, _currpath, _consistencies)
568                 _currpath.pop()
569         if save:
570             #valid no duplicated option
571             valid_child = copy(cache_option)
572             valid_child.sort()
573             old = None
574             for child in valid_child:
575                 if child == old:
576                     raise ConflictConfigError(_('duplicate option: '
577                                               '{0}').format(child))
578                 old = child
579             self._cache_paths = (tuple(cache_option), tuple(cache_path))
580             self._consistencies = _consistencies
581
582     def get_opt_by_path(self, path):
583         try:
584             return self._cache_paths[0][self._cache_paths[1].index(path)]
585         except ValueError:
586             raise AttributeError(_('no option for path {}').format(path))
587
588     def get_path_by_opt(self, opt):
589         try:
590             return self._cache_paths[1][self._cache_paths[0].index(opt)]
591         except ValueError:
592             raise AttributeError(_('no option {} found').format(opt))
593
594     # ____________________________________________________________
595     def set_group_type(self, group_type):
596         """sets a given group object to an OptionDescription
597
598         :param group_type: an instance of `GroupType` or `MasterGroupType`
599                               that lives in `setting.groups`
600         """
601         if self._group_type != groups.default:
602             raise TypeError(_('cannot change group_type if already set '
603                             '(old {}, new {})').format(self._group_type, group_type))
604         if isinstance(group_type, groups.GroupType):
605             self._group_type = group_type
606             if isinstance(group_type, groups.MasterGroupType):
607                 #if master (same name has group) is set
608                 identical_master_child_name = False
609                 #for collect all slaves
610                 slaves = []
611                 master = None
612                 for child in self._children[1]:
613                     if isinstance(child, OptionDescription):
614                         raise ValueError(_("master group {} shall not have "
615                                          "a subgroup").format(self._name))
616                     if not child.is_multi():
617                         raise ValueError(_("not allowed option {0} in group {1}"
618                                          ": this option is not a multi"
619                                          "").format(child._name, self._name))
620                     if child._name == self._name:
621                         identical_master_child_name = True
622                         child._multitype = multitypes.master
623                         master = child
624                     else:
625                         slaves.append(child)
626                 if master is None:
627                     raise ValueError(_('master group with wrong master name for {}'
628                                      '').format(self._name))
629                 master._master_slaves = tuple(slaves)
630                 for child in self._children[1]:
631                     if child != master:
632                         child._master_slaves = master
633                         child._multitype = multitypes.slave
634                 if not identical_master_child_name:
635                     raise ValueError(_("the master group: {} has not any "
636                                      "master child").format(self._name))
637         else:
638             raise ValueError(_('not allowed group_type : {0}').format(group_type))
639
640     def get_group_type(self):
641         return self._group_type
642
643     def valid_consistency(self, opt, value, context, index):
644         consistencies = self._consistencies.get(opt)
645         if consistencies is not None:
646             for consistency in consistencies:
647                 func, opts = consistency
648                 ret = getattr(opts[0], func)(opt, value, context, index, opts)
649                 if ret is False:
650                     return False
651         return True
652
653
654 def validate_requires_arg(requires, name):
655     "check malformed requirements"
656     if requires is not None:
657         config_action = {}
658         for req in requires:
659             if not type(req) == tuple:
660                 raise ValueError(_("malformed requirements type for option:"
661                                  " {0}, must be a tuple").format(name))
662             if len(req) == 3:
663                 action = req[2]
664                 inverse = False
665             elif len(req) == 4:
666                 action = req[2]
667                 inverse = req[3]
668             else:
669                 raise ValueError(_("malformed requirements for option: {0}"
670                                  " invalid len").format(name))
671             if action in config_action:
672                 if inverse != config_action[action]:
673                     raise ValueError(_("inconsistency in action types for option: {0}"
674                                      " action: {1}").format(name, action))
675             else:
676                 config_action[action] = inverse