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