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