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