adds information attribute to an option
[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 types import FunctionType
25 from tiramisu.basetype import BaseType
26 from tiramisu.error import (ConfigError, ConflictConfigError, NotFoundError,
27     RequiresError, RequirementRecursionError, MandatoryError,
28     PropertiesOptionError)
29 from tiramisu.autolib import carry_out_calculation
30 from tiramisu.setting import groups, owners
31
32 requires_actions = [('hide', 'show'), ('enable', 'disable'), 
33                     ('freeze', 'unfreeze')]
34
35 available_actions = []
36 reverse_actions = {}
37 for act1, act2 in requires_actions:
38     available_actions.extend([act1, act2])
39     reverse_actions[act1] = act2
40     reverse_actions[act2] = act1
41 # ____________________________________________________________
42 name_regexp = re.compile(r'^\d+')
43
44 def valid_name(name):
45     try:
46         name = str(name)
47     except:
48         raise ValueError("not a valid string name")
49     if re.match(name_regexp, name) is None:
50         return True
51     else:
52         return False
53 #____________________________________________________________
54 #
55
56 class Option(BaseType):
57     """
58     Abstract base class for configuration option's.
59
60     Reminder: an Option object is **not** a container for the value
61     """
62     #freeze means: cannot modify the value of an Option once set
63     _frozen = False
64     #if an Option has been frozen, shall return the default value
65     _force_default_on_freeze = False
66     def __init__(self, name, doc, default=None, default_multi=None,
67                  requires=None, mandatory=False, multi=False, callback=None,
68                  callback_params=None, validator=None, validator_args={}):
69         """
70         :param name: the option's name
71         :param doc: the option's description
72         :param default: specifies the default value of the option,
73                         for a multi : ['bla', 'bla', 'bla']
74         :param default_multi: 'bla' (used in case of a reset to default only at
75                         a given index)
76         :param requires: is a list of names of options located anywhere
77                          in the configuration.
78         :param multi: if true, the option's value is a list
79         :param callback: the name of a function. If set, the function's output
80                          is responsible of the option's value
81         :param callback_params: the callback's parameter
82         :param validator: the name of a function wich stands for a custom
83                           validation of the value
84         :param validator_args: the validator's parameters
85         """
86         if not valid_name(name):
87             raise NameError("invalid name: {0} for option".format(name))
88         self._name = name
89         self.doc = doc
90         self.informations = {}
91         self._requires = requires
92         self._mandatory = mandatory
93         self.multi = multi
94         self._validator = None
95         self._validator_args = None
96         if validator is not None:
97             if type(validator) != FunctionType:
98                 raise TypeError("validator must be a function")
99             self._validator = validator
100             if validator_args is not None:
101                 self._validator_args = validator_args
102         if not self.multi and default_multi is not None:
103             raise ConfigError("a default_multi is set whereas multi is False"
104                   " in option: {0}".format(name))
105         if default_multi is not None and not self._validate(default_multi):
106             raise ConfigError("invalid default_multi value {0} "
107                 "for option {1}".format(str(default_multi), name))
108         self.default_multi = default_multi
109         #if self.multi and default_multi is None:
110         #    _cfgimpl_warnings[name] = DefaultMultiWarning
111         if callback is not None and (default is not None or default_multi is not None):
112             raise ConfigError("defaut values not allowed if option: {0} "
113                 "is calculated".format(name))
114         self.callback = callback
115         if self.callback is None and callback_params is not None:
116             raise ConfigError("params defined for a callback function but"
117             " no callback defined yet for option {0}".format(name))
118         self.callback_params = callback_params
119         if self.multi == True:
120             if default == None:
121                 default = []
122             if not isinstance(default, list):
123                 raise ConfigError("invalid default value {0} "
124                 "for option {1} : not list type".format(str(default), name))
125             if not self.validate(default, False):
126                 raise ConfigError("invalid default value {0} "
127                 "for option {1}".format(str(default), name))
128         else:
129             if default != None and not self.validate(default, False):
130                 raise ConfigError("invalid default value {0} "
131                                          "for option {1}".format(str(default), name))
132         self.default = default
133         self.properties = [] # 'hidden', 'disabled'...
134
135     def validate(self, value, validate=True):
136         """
137         :param value: the option's value
138         :param validate: if true enables ``self._validator`` validation
139         """
140         # generic calculation
141         if self.multi == False:
142             # None allows the reset of the value
143             if value != None:
144                 # customizing the validator
145                 if validate and self._validator is not None and \
146                         not self._validator(value, **self._validator_args):
147                     return False
148                 return self._validate(value)
149         else:
150             if not isinstance(value, list):
151                 raise ConfigError("invalid value {0} "
152                         "for option {1} which must be a list".format(value,
153                         self._name))
154             for val in value:
155                 # None allows the reset of the value
156                 if val != None:
157                     # customizing the validator
158                     if validate and self._validator is not None and \
159                             not self._validator(val, **self._validator_args):
160                         return False
161                     if not self._validate(val):
162                         return False
163         return True
164
165     def getdefault(self, default_multi=False):
166         "accessing the default value"
167         if default_multi == False or not self.is_multi():
168             return self.default
169         else:
170             return self.getdefault_multi()
171
172     def getdefault_multi(self):
173         "accessing the default value for a multi"
174         return self.default_multi
175
176     def is_empty_by_default(self):
177         "no default value has been set yet"
178         if ((not self.is_multi() and self.default == None) or
179                 (self.is_multi() and (self.default == [] or None in self.default))):
180             return True
181         return False
182
183     def force_default(self):
184         "if an Option has been frozen, shall return the default value"
185         self._force_default_on_freeze = True
186
187     def hascallback_and_isfrozen():
188         return self._frozen and self.has_callback()
189
190     def is_forced_on_freeze(self):
191         "if an Option has been frozen, shall return the default value"
192         return self._frozen and self._force_default_on_freeze
193
194     def getdoc(self):
195         "accesses the Option's doc"
196         return self.doc
197
198     def set_information(self, key, value):
199         """updates the information's attribute
200         (wich is a dictionnary)
201
202         :param key: information's key (ex: "help", "doc"
203         :param value: information's value (ex: "the help string")
204         """
205         self.informations[key] = value
206
207     def get_information(self, key):
208         """retrieves one information's item
209
210         :param key: the item string (ex: "help")
211         """
212         if key in self.informations:
213             return self.informations[key]
214         else:
215             raise ValueError("Information's item not found: {0}".format(key))
216
217     def getcallback(self):
218         "a callback is only a link, the name of an external hook"
219         return self.callback
220
221     def has_callback(self):
222         "to know if a callback has been defined or not"
223         if self.callback == None:
224             return False
225         else:
226             return True
227
228     def getcallback_value(self, config):
229         return carry_out_calculation(self._name,
230                 option=self, config=config)
231
232     def getcallback_params(self):
233         "if a callback has been defined, returns his arity"
234         return self.callback_params
235
236     def setowner(self, config, owner):
237         """
238         :param config: *must* be only the **parent** config
239                        (not the toplevel config)
240         :param owner: is a **real** owner, that is an object
241                       that lives in setting.owners
242         """
243         name = self._name
244         if not isinstance(owner, owners.Owner):
245             raise ConfigError("invalid type owner for option: {0}".format(
246                     str(name)))
247         config._cfgimpl_context._cfgimpl_values.owners[self] = owner
248
249     def getowner(self, config):
250         "config *must* be only the **parent** config (not the toplevel config)"
251         return config._cfgimpl_context._cfgimpl_values.getowner(self)
252
253     def get_previous_value(self, config):
254         return config._cfgimpl_context._cfgimpl_values.get_previous_value(self)
255
256     def reset(self, config):
257         """resets the default value and owner
258         """
259         config._cfgimpl_context._cfgimpl_values.reset(self)
260
261     def is_default_owner(self, config):
262         """
263         :param config: *must* be only the **parent** config
264                        (not the toplevel config)
265         :return: boolean
266         """
267         return self.getowner(config) == owners.default
268
269     def setoption(self, config, value):
270         """changes the option's value with the value_owner's who
271         :param config: the parent config is necessary here to store the value
272         """
273         name = self._name
274         rootconfig = config._cfgimpl_get_toplevel()
275         if not self.validate(value,
276                         config._cfgimpl_context._cfgimpl_settings.validator):
277             raise ConfigError('invalid value %s for option %s' % (value, name))
278         if self.is_mandatory():
279             # value shall not be '' for a mandatory option
280             # so '' is considered as being None
281             if not self.is_multi() and value == '':
282                 value = None
283 #            if self.is_multi() and '' in value:
284 #                value = Multi([{'': None}.get(i, i) for i in value],
285 #                                config._cfgimpl_context, self)
286             if config._cfgimpl_context._cfgimpl_settings.is_mandatory() \
287                 and ((self.is_multi() and value == []) or \
288                 (not self.is_multi() and value is None)):
289                 raise MandatoryError('cannot change the value to %s for '
290               'option %s' % (value, name))
291         if self not in config._cfgimpl_descr._children:
292             raise AttributeError('unknown option %s' % (name))
293
294         if config._cfgimpl_context._cfgimpl_settings.is_frozen_for_everything():
295             raise TypeError("cannot set a value to the option {} if the whole "
296             "config has been frozen".format(name))
297
298         if config._cfgimpl_context._cfgimpl_settings.is_frozen() \
299                                                         and self.is_frozen():
300             raise TypeError('cannot change the value to %s for '
301                'option %s this option is frozen' % (str(value), name))
302         apply_requires(self, config)
303         config._cfgimpl_context._cfgimpl_values[self] = value
304
305     def getkey(self, value):
306         return value
307     # ____________________________________________________________
308     "freeze utility"
309     def freeze(self):
310         self._frozen = True
311         return True
312     def unfreeze(self):
313         self._frozen = False
314     def is_frozen(self):
315         return self._frozen
316     # ____________________________________________________________
317     def is_multi(self):
318         return self.multi
319     def is_mandatory(self):
320         return self._mandatory
321
322 class ChoiceOption(Option):
323     opt_type = 'string'
324
325     def __init__(self, name, doc, values, default=None, default_multi=None,
326                  requires=None, mandatory=False, multi=False, callback=None,
327                  callback_params=None, open_values=False, validator=None,
328                  validator_args={}):
329         self.values = values
330         if open_values not in [True, False]:
331             raise ConfigError('Open_values must be a boolean for '
332                               '{0}'.format(name))
333         self.open_values = open_values
334         super(ChoiceOption, self).__init__(name, doc, default=default,
335                         default_multi=default_multi, callback=callback,
336                         callback_params=callback_params, requires=requires,
337                         multi=multi, mandatory=mandatory, validator=validator,
338                         validator_args=validator_args)
339
340     def _validate(self, value):
341         if not self.open_values:
342             return value is None or value in self.values
343         else:
344             return True
345
346 class BoolOption(Option):
347     opt_type = 'bool'
348
349     def _validate(self, value):
350         return isinstance(value, bool)
351
352 class IntOption(Option):
353     opt_type = 'int'
354
355     def _validate(self, value):
356         return isinstance(value, int)
357
358 class FloatOption(Option):
359     opt_type = 'float'
360
361     def _validate(self, value):
362         return isinstance(value, float)
363
364 class StrOption(Option):
365     opt_type = 'string'
366
367     def _validate(self, value):
368         return isinstance(value, str)
369
370 class SymLinkOption(object):
371     opt_type = 'symlink'
372
373     def __init__(self, name, path, opt):
374         self._name = name
375         self.path = path
376         self.opt = opt
377
378     def setoption(self, config, value):
379         setattr(config._cfgimpl_get_toplevel(), self.path, value)
380
381     def __getattr__(self, name):
382         if name in ('_name', 'path', 'opt', 'setoption'):
383             return self.__dict__[name]
384         else:
385             return getattr(self.opt, name)
386
387 class IPOption(Option):
388     opt_type = 'ip'
389
390     def _validate(self, value):
391         # by now the validation is nothing but a string, use IPy instead
392         return isinstance(value, str)
393
394 class NetmaskOption(Option):
395     opt_type = 'netmask'
396
397     def _validate(self, value):
398         # by now the validation is nothing but a string, use IPy instead
399         return isinstance(value, str)
400
401 class OptionDescription(BaseType):
402     """Config's schema (organisation, group) and container of Options"""
403     # the group_type is useful for filtering OptionDescriptions in a config
404     group_type = groups.default
405     def __init__(self, name, doc, children, requires=None):
406         """
407         :param children: is a list of option descriptions (including
408         ``OptionDescription`` instances for nested namespaces).
409         """
410         if not valid_name(name):
411             raise NameError("invalid name: {0} for option descr".format(name))
412         self._name = name
413         self.doc = doc
414         self._children = children
415         self._requires = requires
416         self._build()
417         self.properties = [] # 'hidden', 'disabled'...
418
419     def getdoc(self):
420         return self.doc
421
422     def _build(self):
423         for child in self._children:
424             setattr(self, child._name, child)
425
426     def add_child(self, child):
427         "dynamically adds a configuration option"
428         #Nothing is static. Even the Mona Lisa is falling apart.
429         for ch in self._children:
430             if isinstance(ch, Option):
431                 if child._name == ch._name:
432                     raise ConflictConfigError("existing option : {0}".format(
433                                                                    child._name))
434         self._children.append(child)
435         setattr(self, child._name, child)
436
437     def update_child(self, child):
438         "modification of an existing option"
439         # XXX : corresponds to the `redefine`, is it usefull
440         pass
441
442     def getkey(self, config):
443         return tuple([child.getkey(getattr(config, child._name))
444                       for child in self._children])
445
446     def getpaths(self, include_groups=False, currpath=None):
447         """returns a list of all paths in self, recursively
448            currpath should not be provided (helps with recursion)
449         """
450         if currpath is None:
451             currpath = []
452         paths = []
453         for option in self._children:
454             attr = option._name
455             if attr.startswith('_cfgimpl'):
456                 continue
457             if isinstance(option, OptionDescription):
458                 if include_groups:
459                     paths.append('.'.join(currpath + [attr]))
460                 currpath.append(attr)
461                 paths += option.getpaths(include_groups=include_groups,
462                                         currpath=currpath)
463                 currpath.pop()
464             else:
465                 paths.append('.'.join(currpath + [attr]))
466         return paths
467
468     def build_cache(self, paths, currpath=None):
469         if currpath is None:
470             currpath = []
471         for option in self._children:
472             attr = option._name
473             if attr.startswith('_cfgimpl'):
474                 continue
475             if isinstance(option, OptionDescription):
476                 currpath.append(attr)
477                 option.build_cache(paths, currpath=currpath)
478                 currpath.pop()
479             else:
480                 paths[option] = '.'.join(currpath + [attr])
481
482     # ____________________________________________________________
483     def set_group_type(self, group_type):
484         """sets a given group object to an OptionDescription
485
486         :param group_type: an instance of `GroupType` or `MasterGroupType`
487                               that lives in `setting.groups`
488         """
489         if isinstance(group_type, groups.GroupType):
490             self.group_type = group_type
491             if isinstance(group_type, groups.MasterGroupType):
492                 identical_master_child_name = False
493                 for child in self._children:
494                     if isinstance(child, OptionDescription):
495                         raise ConfigError("master group {} shall not have "
496                             "a subgroup".format(self._name))
497                     if not child.multi:
498                         raise ConfigError("not allowed option {0} in group {1}"
499                             ": this option is not a multi".format(child._name,
500                             self._name))
501                     if child._name == self._name:
502                         identical_master_child_name = True
503                 if not identical_master_child_name:
504                     raise ConfigError("the master group: {} has not any "
505                      "master child".format(self._name))
506         else:
507             raise ConfigError('not allowed group_type : {0}'.format(group_type))
508
509     def get_group_type(self):
510         return self.group_type
511     # ____________________________________________________________
512     "actions API"
513     def hide(self):
514         super(OptionDescription, self).hide()
515         for child in self._children:
516             if isinstance(child, OptionDescription):
517                 child.hide()
518     def show(self):
519         super(OptionDescription, self).show()
520         for child in self._children:
521             if isinstance(child, OptionDescription):
522                 child.show()
523
524     def disable(self):
525         super(OptionDescription, self).disable()
526         for child in self._children:
527             if isinstance(child, OptionDescription):
528                 child.disable()
529     def enable(self):
530         super(OptionDescription, self).enable()
531         for child in self._children:
532             if isinstance(child, OptionDescription):
533                 child.enable()
534 # ____________________________________________________________
535
536 def validate_requires_arg(requires, name):
537     "malformed requirements"
538     config_action = []
539     for req in requires:
540         if not type(req) == tuple and len(req) != 3:
541             raise RequiresError("malformed requirements for option:"
542                                            " {0}".format(name))
543         action = req[2]
544         if action not in available_actions:
545             raise RequiresError("malformed requirements for option: {0}"
546                                 " unknown action: {1}".format(name, action))
547         if reverse_actions[action] in config_action:
548             raise RequiresError("inconsistency in action types for option: {0}"
549                                 " action: {1} in contradiction with {2}\n"
550                                 " ({3})".format(name, action,
551                                     reverse_actions[action], requires))
552         config_action.append(action)
553
554 def build_actions(requires):
555     "action are hide, show, enable, disable..."
556     trigger_actions = {}
557     for require in requires:
558         action = require[2]
559         trigger_actions.setdefault(action, []).append(require)
560     return trigger_actions
561
562 def apply_requires(opt, config, permissive=False):
563     "carries out the jit (just in time requirements between options"
564     if hasattr(opt, '_requires') and opt._requires is not None:
565         rootconfig = config._cfgimpl_get_toplevel()
566         validate_requires_arg(opt._requires, opt._name)
567         # filters the callbacks
568         trigger_actions = build_actions(opt._requires)
569         for requires in trigger_actions.values():
570             matches = False
571             for require in requires:
572                 name, expected, action = require
573                 path = config._cfgimpl_get_path() + '.' + opt._name
574                 if name.startswith(path):
575                     raise RequirementRecursionError("malformed requirements "
576                           "imbrication detected for option: '{0}' "
577                           "with requirement on: '{1}'".format(path, name))
578                 homeconfig, shortname = rootconfig._cfgimpl_get_home_by_path(name)
579                 try:
580                     value = homeconfig._getattr(shortname, permissive=True)
581                 except PropertiesOptionError, err:
582                     properties = err.proptype
583                     if permissive:
584                         for perm in \
585                                 config._cfgimpl_context._cfgimpl_settings.permissive:
586                             if perm in properties:
587                                 properties.remove(perm)
588                     if properties != []:
589                         raise NotFoundError("option '{0}' has requirement's property error: "
590                                      "{1} {2}".format(opt._name, name, properties))
591                 except Exception, err:
592                     raise NotFoundError("required option not found: "
593                                                              "{0}".format(name))
594                 if value == expected:
595                     getattr(opt, action)() #.hide() or show() or...
596                     # FIXME generic programming opt.property_launch(action, False)
597                     matches = True
598             # no callback has been triggered, then just reverse the action
599             if not matches:
600                 getattr(opt, reverse_actions[action])()