rewrite make_dict
[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 tiramisu.error import (ConfigError, NotFoundError, ConflictConfigError,
27                             RequiresError, RequirementRecursionError,
28                             PropertiesOptionError)
29 from tiramisu.autolib import carry_out_calculation
30 from tiramisu.setting import groups, multitypes
31
32 name_regexp = re.compile(r'^\d+')
33
34
35 def valid_name(name):
36     try:
37         name = str(name)
38     except:
39         raise ValueError("not a valid string name")
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')
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         """
88         :param name: the option's name
89         :param doc: the option's description
90         :param default: specifies the default value of the option,
91                         for a multi : ['bla', 'bla', 'bla']
92         :param default_multi: 'bla' (used in case of a reset to default only at
93                         a given index)
94         :param requires: is a list of names of options located anywhere
95                          in the configuration.
96         :param multi: if true, the option's value is a list
97         :param callback: the name of a function. If set, the function's output
98                          is responsible of the option's value
99         :param callback_params: the callback's parameter
100         :param validator: the name of a function wich stands for a custom
101                           validation of the value
102         :param validator_args: the validator's parameters
103         """
104         if not valid_name(name):
105             raise NameError("invalid name: {0} for option".format(name))
106         self._name = name
107         self.informations = {}
108         self.set_information('doc', doc)
109         validate_requires_arg(requires, self._name)
110         self._requires = requires
111         self.multi = multi
112         #self._validator_args = None
113         if validator is not None:
114             if type(validator) != FunctionType:
115                 raise TypeError("validator must be a function")
116             self._validator = (validator, validator_args)
117         else:
118             self._validator = None
119         if not self.multi and default_multi is not None:
120             raise ConfigError("a default_multi is set whereas multi is False"
121                               " in option: {0}".format(name))
122         if default_multi is not None and not self._validate(default_multi):
123             raise ConfigError("invalid default_multi value {0} "
124                               "for option {1}".format(str(default_multi), name))
125         if callback is not None and (default is not None or default_multi is not None):
126             raise ConfigError("defaut values not allowed if option: {0} "
127                               "is calculated".format(name))
128         if callback is None and callback_params is not None:
129             raise ConfigError("params defined for a callback function but "
130                               "no callback defined yet for option {0}".format(name))
131         if callback is not None:
132             self.callback = (callback, callback_params)
133         else:
134             self.callback = None
135         if self.multi:
136             if default is None:
137                 default = []
138             if not isinstance(default, list):
139                 raise ConfigError("invalid default value {0} "
140                                   "for option {1} : not list type"
141                                   "".format(str(default), name))
142             if not self.validate(default, False):
143                 raise ConfigError("invalid default value {0} "
144                                   "for option {1}"
145                                   "".format(str(default), name))
146             self.multitype = multitypes.default
147             self.default_multi = default_multi
148         else:
149             if default is not None and not self.validate(default, False):
150                 raise ConfigError("invalid default value {0} "
151                                   "for option {1}".format(str(default), name))
152         self.default = default
153         if properties is None:
154             properties = ()
155         if not isinstance(properties, tuple):
156             raise ConfigError('invalid properties type {0} for {1},'
157                               ' must be a tuple'.format(type(properties), self._name))
158         self._properties = properties  # 'hidden', 'disabled'...
159
160     def validate(self, value, validate=True):
161         """
162         :param value: the option's value
163         :param validate: if true enables ``self._validator`` validation
164         """
165         # generic calculation
166         if not self.multi:
167             # None allows the reset of the value
168             if value is not None:
169                 # customizing the validator
170                 if validate and self._validator is not None and \
171                         not self._validator[0](value, **self._validator[1]):
172                     return False
173                 return self._validate(value)
174         else:
175             if not isinstance(value, list):
176                 raise ConfigError("invalid value {0} "
177                                   "for option {1} which must be a list"
178                                   "".format(value, self._name))
179             for val in value:
180                 # None allows the reset of the value
181                 if val is not None:
182                     # customizing the validator
183                     if validate and self._validator is not None and \
184                             not self._validator[0](val, **self._validator[1]):
185                         return False
186                     if not self._validate(val):
187                         return False
188         return True
189
190     def getdefault(self, default_multi=False):
191         "accessing the default value"
192         if not default_multi or not self.is_multi():
193             return self.default
194         else:
195             return self.getdefault_multi()
196
197     def getdefault_multi(self):
198         "accessing the default value for a multi"
199         return self.default_multi
200
201     def is_empty_by_default(self):
202         "no default value has been set yet"
203         if ((not self.is_multi() and self.default is None) or
204                 (self.is_multi() and (self.default == [] or None in self.default))):
205             return True
206         return False
207
208     def getdoc(self):
209         "accesses the Option's doc"
210         return self.get_information('doc')
211
212     def has_callback(self):
213         "to know if a callback has been defined or not"
214         if self.callback is None:
215             return False
216         else:
217             return True
218
219     def getcallback_value(self, config):
220         callback, callback_params = self.callback
221         if callback_params is None:
222             callback_params = {}
223         return carry_out_calculation(self._name, config=config,
224                                      callback=callback,
225                                      callback_params=callback_params)
226
227     def reset(self, config):
228         """resets the default value and owner
229         """
230         config._cfgimpl_context._cfgimpl_values.reset(self)
231
232     def setoption(self, config, value):
233         """changes the option's value with the value_owner's who
234         :param config: the parent config is necessary here to store the value
235         """
236         name = self._name
237         setting = config.cfgimpl_get_settings()
238         if not self.validate(value, setting.has_property('validator')):
239             raise ConfigError('invalid value %s for option %s' % (value, name))
240         if self not in config._cfgimpl_descr._children[1]:
241             raise AttributeError('unknown option %s' % (name))
242
243         if setting.has_property('everything_frozen'):
244             raise TypeError("cannot set a value to the option {} if the whole "
245                             "config has been frozen".format(name))
246
247         if setting.has_property('frozen') and setting.has_property('frozen',
248                                                                    self):
249             raise TypeError('cannot change the value to %s for '
250                             'option %s this option is frozen' % (str(value), name))
251         apply_requires(self, config)
252         config.cfgimpl_get_values()[self] = value
253
254     def getkey(self, value):
255         return value
256
257     def is_multi(self):
258         return self.multi
259
260
261 class ChoiceOption(Option):
262     __slots__ = ('values', 'open_values', 'opt_type')
263     opt_type = 'string'
264
265     def __init__(self, name, doc, values, default=None, default_multi=None,
266                  requires=None, multi=False, callback=None,
267                  callback_params=None, open_values=False, validator=None,
268                  validator_args=None, properties=()):
269         if not isinstance(values, tuple):
270             raise ConfigError('values must be a tuple for {0}'.format(name))
271         self.values = values
272         if open_values not in (True, False):
273             raise ConfigError('Open_values must be a boolean for '
274                               '{0}'.format(name))
275         self.open_values = open_values
276         super(ChoiceOption, self).__init__(name, doc, default=default,
277                                            default_multi=default_multi,
278                                            callback=callback,
279                                            callback_params=callback_params,
280                                            requires=requires,
281                                            multi=multi,
282                                            validator=validator,
283                                            validator_args=validator_args,
284                                            properties=properties)
285
286     def _validate(self, value):
287         if not self.open_values:
288             return value is None or value in self.values
289         else:
290             return True
291
292
293 class BoolOption(Option):
294     __slots__ = ('opt_type')
295     opt_type = 'bool'
296
297     def _validate(self, value):
298         return isinstance(value, bool)
299
300
301 class IntOption(Option):
302     __slots__ = ('opt_type')
303     opt_type = 'int'
304
305     def _validate(self, value):
306         return isinstance(value, int)
307
308
309 class FloatOption(Option):
310     __slots__ = ('opt_type')
311     opt_type = 'float'
312
313     def _validate(self, value):
314         return isinstance(value, float)
315
316
317 class StrOption(Option):
318     __slots__ = ('opt_type')
319     opt_type = 'string'
320
321     def _validate(self, value):
322         return isinstance(value, str)
323
324
325 class UnicodeOption(Option):
326     __slots__ = ('opt_type')
327     opt_type = 'unicode'
328
329     def _validate(self, value):
330         return isinstance(value, unicode)
331
332
333 class SymLinkOption(object):
334     __slots__ = ('_name', 'opt')
335     opt_type = 'symlink'
336
337     def __init__(self, name, path, opt):
338         self._name = name
339         self.opt = opt
340
341     def setoption(self, config, value):
342         context = config.cfgimpl_get_context()
343         path = context.cfgimpl_get_description().get_path_by_opt(self.opt)
344         setattr(context, path, value)
345
346     def __getattr__(self, name):
347         if name in ('_name', 'opt', 'setoption'):
348             return object.__gettattr__(self, name)
349         else:
350             return getattr(self.opt, name)
351
352
353 class IPOption(Option):
354     __slots__ = ('opt_type')
355     opt_type = 'ip'
356
357     def _validate(self, value):
358         # by now the validation is nothing but a string, use IPy instead
359         return isinstance(value, str)
360
361
362 class NetmaskOption(Option):
363     __slots__ = ('opt_type')
364     opt_type = 'netmask'
365
366     def _validate(self, value):
367         # by now the validation is nothing but a string, use IPy instead
368         return isinstance(value, str)
369
370
371 class OptionDescription(BaseInformation):
372     """Config's schema (organisation, group) and container of Options"""
373     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
374                  '_properties', '_children')
375
376     def __init__(self, name, doc, children, requires=None, properties=()):
377         """
378         :param children: is a list of option descriptions (including
379         ``OptionDescription`` instances for nested namespaces).
380         """
381         if not valid_name(name):
382             raise NameError("invalid name: {0} for option descr".format(name))
383         self._name = name
384         self.informations = {}
385         self.set_information('doc', doc)
386         child_names = [child._name for child in children]
387         #better performance like this
388         valid_child = copy(child_names)
389         valid_child.sort()
390         old = None
391         for child in valid_child:
392             if child == old:
393                 raise ConflictConfigError('duplicate option name: '
394                                           '{0}'.format(child))
395             old = child
396         self._children = (tuple(child_names), tuple(children))
397         validate_requires_arg(requires, self._name)
398         self._requires = requires
399         self._cache_paths = None
400         if not isinstance(properties, tuple):
401             raise ConfigError('invalid properties type {0} for {1},'
402                               ' must be a tuple'.format(type(properties), self._name))
403         self._properties = properties  # 'hidden', 'disabled'...
404         # the group_type is useful for filtering OptionDescriptions in a config
405         self._group_type = groups.default
406
407     def getdoc(self):
408         return self.get_information('doc')
409
410     def __getattr__(self, name):
411         try:
412             return self._children[1][self._children[0].index(name)]
413         except ValueError:
414             raise AttributeError('unknown Option {} in OptionDescription {}'
415                                  ''.format(name, self._name))
416
417     def getkey(self, config):
418         return tuple([child.getkey(getattr(config, child._name))
419                       for child in self._children[1]])
420
421     def getpaths(self, include_groups=False, _currpath=None):
422         """returns a list of all paths in self, recursively
423            _currpath should not be provided (helps with recursion)
424         """
425         #FIXME : cache
426         if _currpath is None:
427             _currpath = []
428         paths = []
429         for option in self._children[1]:
430             attr = option._name
431             if isinstance(option, OptionDescription):
432                 if include_groups:
433                     paths.append('.'.join(_currpath + [attr]))
434                 paths += option.getpaths(include_groups=include_groups,
435                                          _currpath=_currpath + [attr])
436             else:
437                 paths.append('.'.join(_currpath + [attr]))
438         return paths
439
440     def getchildren(self):
441         return self._children[1]
442
443     def build_cache(self, cache_path=None, cache_option=None, _currpath=None):
444         if _currpath is None and self._cache_paths is not None:
445             return
446         if _currpath is None:
447             save = True
448             _currpath = []
449         else:
450             save = False
451         if cache_path is None:
452             cache_path = [self._name]
453             cache_option = [self]
454         for option in self._children[1]:
455             attr = option._name
456             if attr.startswith('_cfgimpl'):
457                 continue
458             cache_option.append(option)
459             cache_path.append(str('.'.join(_currpath + [attr])))
460             if isinstance(option, OptionDescription):
461                 _currpath.append(attr)
462                 option.build_cache(cache_path, cache_option, _currpath)
463                 _currpath.pop()
464         if save:
465             #valid no duplicated option
466             valid_child = copy(cache_option)
467             valid_child.sort()
468             old = None
469             for child in valid_child:
470                 if child == old:
471                     raise ConflictConfigError('duplicate option: '
472                                               '{0}'.format(child))
473                 old = child
474             self._cache_paths = (tuple(cache_option), tuple(cache_path))
475
476     def get_opt_by_path(self, path):
477         try:
478             return self._cache_paths[0][self._cache_paths[1].index(path)]
479         except ValueError:
480             raise NotFoundError('no option for path {}'.format(path))
481
482     def get_path_by_opt(self, opt):
483         try:
484             return self._cache_paths[1][self._cache_paths[0].index(opt)]
485         except ValueError:
486             raise NotFoundError('no option {} found'.format(opt))
487
488     # ____________________________________________________________
489     def set_group_type(self, group_type):
490         """sets a given group object to an OptionDescription
491
492         :param group_type: an instance of `GroupType` or `MasterGroupType`
493                               that lives in `setting.groups`
494         """
495         if self._group_type != groups.default:
496             ConfigError('cannot change group_type if already set '
497                         '(old {}, new {})'.format(self._group_type, group_type))
498         if isinstance(group_type, groups.GroupType):
499             self._group_type = group_type
500             if isinstance(group_type, groups.MasterGroupType):
501                 #if master (same name has group) is set
502                 identical_master_child_name = False
503                 #for collect all slaves
504                 slaves = []
505                 master = None
506                 for child in self._children[1]:
507                     if isinstance(child, OptionDescription):
508                         raise ConfigError("master group {} shall not have "
509                                           "a subgroup".format(self._name))
510                     if not child.multi:
511                         raise ConfigError("not allowed option {0} in group {1}"
512                                           ": this option is not a multi"
513                                           "".format(child._name, self._name))
514                     if child._name == self._name:
515                         identical_master_child_name = True
516                         child.multitype = multitypes.master
517                         master = child
518                     else:
519                         slaves.append(child)
520                 if master is None:
521                     raise ConfigError('master group with wrong master name for {}'
522                                       ''.format(self._name))
523                 master.master_slaves = tuple(slaves)
524                 for child in self._children[1]:
525                     if child != master:
526                         child.master_slaves = master
527                         child.multitype = multitypes.slave
528                 if not identical_master_child_name:
529                     raise ConfigError("the master group: {} has not any "
530                                       "master child".format(self._name))
531         else:
532             raise ConfigError('not allowed group_type : {0}'.format(group_type))
533
534     def get_group_type(self):
535         return self._group_type
536
537
538 def validate_requires_arg(requires, name):
539     "check malformed requirements"
540     if requires is not None:
541         config_action = {}
542         for req in requires:
543             if not type(req) == tuple:
544                 raise RequiresError("malformed requirements type for option:"
545                                     " {0}, must be a tuple".format(name))
546             if len(req) == 3:
547                 action = req[2]
548                 inverse = False
549             elif len(req) == 4:
550                 action = req[2]
551                 inverse = req[3]
552             else:
553                 raise RequiresError("malformed requirements for option: {0}"
554                                     " invalid len".format(name))
555             if action in config_action:
556                 if inverse != config_action[action]:
557                     raise RequiresError("inconsistency in action types for option: {0}"
558                                         " action: {1}".format(name, action))
559             else:
560                 config_action[action] = inverse
561
562
563 def apply_requires(opt, config):
564     "carries out the jit (just in time requirements between options"
565     def build_actions(requires):
566         "action are hide, show, enable, disable..."
567         trigger_actions = {}
568         for require in requires:
569             action = require[2]
570             trigger_actions.setdefault(action, []).append(require)
571         return trigger_actions
572     #for symlink
573     if hasattr(opt, '_requires') and opt._requires is not None:
574         # filters the callbacks
575         setting = config.cfgimpl_get_settings()
576         trigger_actions = build_actions(opt._requires)
577         if isinstance(opt, OptionDescription):
578             optpath = config._cfgimpl_get_path() + '.' + opt._name
579         else:
580             optpath = config.cfgimpl_get_context().cfgimpl_get_description().get_path_by_opt(opt)
581         for requires in trigger_actions.values():
582             matches = False
583             for require in requires:
584                 if len(require) == 3:
585                     path, expected, action = require
586                     inverse = False
587                 elif len(require) == 4:
588                     path, expected, action, inverse = require
589                 if path.startswith(optpath):
590                     raise RequirementRecursionError("malformed requirements "
591                                                     "imbrication detected for option: '{0}' "
592                                                     "with requirement on: '{1}'".format(optpath, path))
593                 try:
594                     value = config.cfgimpl_get_context()._getattr(path, force_permissive=True)
595                 except PropertiesOptionError, err:
596                     properties = err.proptype
597                     raise NotFoundError("option '{0}' has requirement's property error: "
598                                         "{1} {2}".format(opt._name, path, properties))
599                 except Exception, err:
600                     raise NotFoundError("required option not found: "
601                                         "{0}".format(path))
602                 if value == expected:
603                     if inverse:
604                         setting.del_property(action, opt)
605                     else:
606                         setting.add_property(action, opt)
607                     matches = True
608                     #FIXME optimisation : fait un double break non ? voire un return
609             # no requirement has been triggered, then just reverse the action
610             if not matches:
611                 if inverse:
612                     setting.add_property(action, opt)
613                 else:
614                     setting.del_property(action, opt)