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