Le cache des paths est dans l'OptionDescription
[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         self._cache_paths = {}
428
429     def getdoc(self):
430         return self.doc
431
432     def _build(self):
433         for child in self._children:
434             setattr(self, child._name, child)
435
436     def add_child(self, child):
437         "dynamically adds a configuration option"
438         #Nothing is static. Even the Mona Lisa is falling apart.
439         for ch in self._children:
440             if isinstance(ch, Option):
441                 if child._name == ch._name:
442                     raise ConflictConfigError("existing option : {0}".format(
443                                                                    child._name))
444         self._children.append(child)
445         setattr(self, child._name, child)
446
447     def update_child(self, child):
448         "modification of an existing option"
449         # XXX : corresponds to the `redefine`, is it usefull
450         pass
451
452     def getkey(self, config):
453         return tuple([child.getkey(getattr(config, child._name))
454                       for child in self._children])
455
456     def getpaths(self, include_groups=False, currpath=None):
457         """returns a list of all paths in self, recursively
458            currpath should not be provided (helps with recursion)
459         """
460         if currpath is None:
461             currpath = []
462         paths = []
463         for option in self._children:
464             attr = option._name
465             if attr.startswith('_cfgimpl'):
466                 continue
467             if isinstance(option, OptionDescription):
468                 if include_groups:
469                     paths.append('.'.join(currpath + [attr]))
470                 currpath.append(attr)
471                 paths += option.getpaths(include_groups=include_groups,
472                                         currpath=currpath)
473                 currpath.pop()
474             else:
475                 paths.append('.'.join(currpath + [attr]))
476         return paths
477
478     def build_cache(self, currpath=None):
479         if currpath is None and self._cache_paths != {}:
480             return
481         if currpath is None:
482             currpath = []
483         for option in self._children:
484             attr = option._name
485             if attr.startswith('_cfgimpl'):
486                 continue
487             if isinstance(option, OptionDescription):
488                 currpath.append(attr)
489                 option.build_cache(currpath=currpath)
490                 currpath.pop()
491             else:
492                 self._cache_paths[option] = str('.'.join(currpath + [attr]))
493
494     # ____________________________________________________________
495     def set_group_type(self, group_type):
496         """sets a given group object to an OptionDescription
497
498         :param group_type: an instance of `GroupType` or `MasterGroupType`
499                               that lives in `setting.groups`
500         """
501         if isinstance(group_type, groups.GroupType):
502             self.group_type = group_type
503             if isinstance(group_type, groups.MasterGroupType):
504                 identical_master_child_name = False
505                 for child in self._children:
506                     if isinstance(child, OptionDescription):
507                         raise ConfigError("master group {} shall not have "
508                             "a subgroup".format(self._name))
509                     if not child.multi:
510                         raise ConfigError("not allowed option {0} in group {1}"
511                             ": this option is not a multi".format(child._name,
512                             self._name))
513                     if child._name == self._name:
514                         identical_master_child_name = True
515                 if not identical_master_child_name:
516                     raise ConfigError("the master group: {} has not any "
517                      "master child".format(self._name))
518         else:
519             raise ConfigError('not allowed group_type : {0}'.format(group_type))
520
521     def get_group_type(self):
522         return self.group_type
523     # ____________________________________________________________
524     "actions API"
525     def hide(self):
526         super(OptionDescription, self).hide()
527         for child in self._children:
528             if isinstance(child, OptionDescription):
529                 child.hide()
530     def show(self):
531         super(OptionDescription, self).show()
532         for child in self._children:
533             if isinstance(child, OptionDescription):
534                 child.show()
535
536     def disable(self):
537         super(OptionDescription, self).disable()
538         for child in self._children:
539             if isinstance(child, OptionDescription):
540                 child.disable()
541     def enable(self):
542         super(OptionDescription, self).enable()
543         for child in self._children:
544             if isinstance(child, OptionDescription):
545                 child.enable()
546 # ____________________________________________________________
547
548 def validate_requires_arg(requires, name):
549     "malformed requirements"
550     config_action = []
551     for req in requires:
552         if not type(req) == tuple and len(req) != 3:
553             raise RequiresError("malformed requirements for option:"
554                                            " {0}".format(name))
555         action = req[2]
556         if action not in available_actions:
557             raise RequiresError("malformed requirements for option: {0}"
558                                 " unknown action: {1}".format(name, action))
559         if reverse_actions[action] in config_action:
560             raise RequiresError("inconsistency in action types for option: {0}"
561                                 " action: {1} in contradiction with {2}\n"
562                                 " ({3})".format(name, action,
563                                     reverse_actions[action], requires))
564         config_action.append(action)
565
566 def build_actions(requires):
567     "action are hide, show, enable, disable..."
568     trigger_actions = {}
569     for require in requires:
570         action = require[2]
571         trigger_actions.setdefault(action, []).append(require)
572     return trigger_actions
573
574 def apply_requires(opt, config, permissive=False):
575     "carries out the jit (just in time requirements between options"
576     if hasattr(opt, '_requires') and opt._requires is not None:
577         rootconfig = config._cfgimpl_get_toplevel()
578         validate_requires_arg(opt._requires, opt._name)
579         # filters the callbacks
580         trigger_actions = build_actions(opt._requires)
581         for requires in trigger_actions.values():
582             matches = False
583             for require in requires:
584                 name, expected, action = require
585                 path = config._cfgimpl_get_path() + '.' + opt._name
586                 if name.startswith(path):
587                     raise RequirementRecursionError("malformed requirements "
588                           "imbrication detected for option: '{0}' "
589                           "with requirement on: '{1}'".format(path, name))
590                 homeconfig, shortname = rootconfig.cfgimpl_get_home_by_path(name)
591                 try:
592                     value = homeconfig._getattr(shortname, permissive=True)
593                 except PropertiesOptionError, err:
594                     properties = err.proptype
595                     if permissive:
596                         for perm in \
597                                 config._cfgimpl_context._cfgimpl_settings.permissive:
598                             if perm in properties:
599                                 properties.remove(perm)
600                     if properties != []:
601                         raise NotFoundError("option '{0}' has requirement's property error: "
602                                      "{1} {2}".format(opt._name, name, properties))
603                 except Exception, err:
604                     raise NotFoundError("required option not found: "
605                                                              "{0}".format(name))
606                 if value == expected:
607                     getattr(opt, action)() #.hide() or show() or...
608                     # FIXME generic programming opt.property_launch(action, False)
609                     matches = True
610             # no callback has been triggered, then just reverse the action
611             if not matches:
612                 getattr(opt, reverse_actions[action])()