don't launch apply_requires more than needed
[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)
28 from tiramisu.setting import groups, multitypes
29
30 name_regexp = re.compile(r'^\d+')
31
32
33 def valid_name(name):
34     try:
35         name = str(name)
36     except:
37         raise ValueError("not a valid string name")
38     if re.match(name_regexp, name) is None:
39         return True
40     else:
41         return False
42 #____________________________________________________________
43 #
44
45
46 class BaseInformation(object):
47     __slots__ = ('informations')
48
49     def set_information(self, key, value):
50         """updates the information's attribute
51         (wich is a dictionnary)
52
53         :param key: information's key (ex: "help", "doc"
54         :param value: information's value (ex: "the help string")
55         """
56         self.informations[key] = value
57
58     def get_information(self, key, default=None):
59         """retrieves one information's item
60
61         :param key: the item string (ex: "help")
62         """
63         if key in self.informations:
64             return self.informations[key]
65         elif default is not None:
66             return default
67         else:
68             raise ValueError("Information's item not found: {0}".format(key))
69
70
71 class Option(BaseInformation):
72     """
73     Abstract base class for configuration option's.
74
75     Reminder: an Option object is **not** a container for the value
76     """
77     __slots__ = ('_name', '_requires', 'multi', '_validator', 'default_multi',
78                  'default', '_properties', 'callback', 'multitype',
79                  'master_slaves')
80
81     def __init__(self, name, doc, default=None, default_multi=None,
82                  requires=None, multi=False, callback=None,
83                  callback_params=None, validator=None, validator_args=None,
84                  properties=None):
85         #FIXME : validation de callback et callback_params !!!
86         """
87         :param name: the option's name
88         :param doc: the option's description
89         :param default: specifies the default value of the option,
90                         for a multi : ['bla', 'bla', 'bla']
91         :param default_multi: 'bla' (used in case of a reset to default only at
92                         a given index)
93         :param requires: is a list of names of options located anywhere
94                          in the configuration.
95         :param multi: if true, the option's value is a list
96         :param callback: the name of a function. If set, the function's output
97                          is responsible of the option's value
98         :param callback_params: the callback's parameter
99         :param validator: the name of a function wich stands for a custom
100                           validation of the value
101         :param validator_args: the validator's parameters
102         """
103         if not valid_name(name):
104             raise NameError("invalid name: {0} for option".format(name))
105         self._name = name
106         self.informations = {}
107         self.set_information('doc', doc)
108         validate_requires_arg(requires, self._name)
109         self._requires = requires
110         self.multi = multi
111         if validator is not None:
112             if type(validator) != FunctionType:
113                 raise TypeError("validator must be a function")
114             if validator_args is None:
115                 validator_args = {}
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):
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):
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 reset(self, config):
220         """resets the default value and owner
221         """
222         config._cfgimpl_context._cfgimpl_values.reset(self)
223
224     def setoption(self, config, value):
225         """changes the option's value with the value_owner's who
226         :param config: the parent config is necessary here to store the value
227         """
228         name = self._name
229         setting = config.cfgimpl_get_settings()
230         if not self.validate(value, setting.has_property('validator')):
231             raise ConfigError('invalid value %s for option %s' % (value, name))
232         if self not in config._cfgimpl_descr._children[1]:
233             raise AttributeError('unknown option %s' % (name))
234
235         if setting.has_property('everything_frozen'):
236             raise TypeError("cannot set a value to the option {} if the whole "
237                             "config has been frozen".format(name))
238
239         if setting.has_property('frozen') and setting.has_property('frozen',
240                                                                    self, False):
241             raise TypeError('cannot change the value to %s for '
242                             'option %s this option is frozen' % (str(value), name))
243         #apply_requires(self, config)
244         config.cfgimpl_get_values()[self] = value
245
246     def getkey(self, value):
247         return value
248
249     def is_multi(self):
250         return self.multi
251
252
253 class ChoiceOption(Option):
254     __slots__ = ('values', 'open_values', 'opt_type')
255     opt_type = 'string'
256
257     def __init__(self, name, doc, values, default=None, default_multi=None,
258                  requires=None, multi=False, callback=None,
259                  callback_params=None, open_values=False, validator=None,
260                  validator_args=None, properties=()):
261         if not isinstance(values, tuple):
262             raise ConfigError('values must be a tuple for {0}'.format(name))
263         self.values = values
264         if open_values not in (True, False):
265             raise ConfigError('Open_values must be a boolean for '
266                               '{0}'.format(name))
267         self.open_values = open_values
268         super(ChoiceOption, self).__init__(name, doc, default=default,
269                                            default_multi=default_multi,
270                                            callback=callback,
271                                            callback_params=callback_params,
272                                            requires=requires,
273                                            multi=multi,
274                                            validator=validator,
275                                            validator_args=validator_args,
276                                            properties=properties)
277
278     def _validate(self, value):
279         if not self.open_values:
280             return value is None or value in self.values
281         else:
282             return True
283
284
285 class BoolOption(Option):
286     __slots__ = ('opt_type')
287     opt_type = 'bool'
288
289     def _validate(self, value):
290         return isinstance(value, bool)
291
292
293 class IntOption(Option):
294     __slots__ = ('opt_type')
295     opt_type = 'int'
296
297     def _validate(self, value):
298         return isinstance(value, int)
299
300
301 class FloatOption(Option):
302     __slots__ = ('opt_type')
303     opt_type = 'float'
304
305     def _validate(self, value):
306         return isinstance(value, float)
307
308
309 class StrOption(Option):
310     __slots__ = ('opt_type')
311     opt_type = 'string'
312
313     def _validate(self, value):
314         return isinstance(value, str)
315
316
317 class UnicodeOption(Option):
318     __slots__ = ('opt_type')
319     opt_type = 'unicode'
320
321     def _validate(self, value):
322         return isinstance(value, unicode)
323
324
325 class SymLinkOption(object):
326     __slots__ = ('_name', 'opt')
327     opt_type = 'symlink'
328
329     def __init__(self, name, path, opt):
330         self._name = name
331         self.opt = opt
332
333     def setoption(self, config, value):
334         context = config.cfgimpl_get_context()
335         path = context.cfgimpl_get_description().get_path_by_opt(self.opt)
336         setattr(context, path, value)
337
338     def __getattr__(self, name):
339         if name in ('_name', 'opt', 'setoption'):
340             return object.__gettattr__(self, name)
341         else:
342             return getattr(self.opt, name)
343
344
345 class IPOption(Option):
346     __slots__ = ('opt_type')
347     opt_type = 'ip'
348
349     def _validate(self, value):
350         # by now the validation is nothing but a string, use IPy instead
351         return isinstance(value, str)
352
353
354 class NetmaskOption(Option):
355     __slots__ = ('opt_type')
356     opt_type = 'netmask'
357
358     def _validate(self, value):
359         # by now the validation is nothing but a string, use IPy instead
360         return isinstance(value, str)
361
362
363 class OptionDescription(BaseInformation):
364     """Config's schema (organisation, group) and container of Options"""
365     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
366                  '_properties', '_children')
367
368     def __init__(self, name, doc, children, requires=None, properties=()):
369         """
370         :param children: is a list of option descriptions (including
371         ``OptionDescription`` instances for nested namespaces).
372         """
373         if not valid_name(name):
374             raise NameError("invalid name: {0} for option descr".format(name))
375         self._name = name
376         self.informations = {}
377         self.set_information('doc', doc)
378         child_names = [child._name for child in children]
379         #better performance like this
380         valid_child = copy(child_names)
381         valid_child.sort()
382         old = None
383         for child in valid_child:
384             if child == old:
385                 raise ConflictConfigError('duplicate option name: '
386                                           '{0}'.format(child))
387             old = child
388         self._children = (tuple(child_names), tuple(children))
389         validate_requires_arg(requires, self._name)
390         self._requires = requires
391         self._cache_paths = None
392         if not isinstance(properties, tuple):
393             raise ConfigError('invalid properties type {0} for {1},'
394                               ' must be a tuple'.format(type(properties), self._name))
395         self._properties = properties  # 'hidden', 'disabled'...
396         # the group_type is useful for filtering OptionDescriptions in a config
397         self._group_type = groups.default
398
399     def getdoc(self):
400         return self.get_information('doc')
401
402     def __getattr__(self, name):
403         try:
404             return self._children[1][self._children[0].index(name)]
405         except ValueError:
406             raise AttributeError('unknown Option {} in OptionDescription {}'
407                                  ''.format(name, self._name))
408
409     def getkey(self, config):
410         return tuple([child.getkey(getattr(config, child._name))
411                       for child in self._children[1]])
412
413     def getpaths(self, include_groups=False, _currpath=None):
414         """returns a list of all paths in self, recursively
415            _currpath should not be provided (helps with recursion)
416         """
417         #FIXME : cache
418         if _currpath is None:
419             _currpath = []
420         paths = []
421         for option in self._children[1]:
422             attr = option._name
423             if isinstance(option, OptionDescription):
424                 if include_groups:
425                     paths.append('.'.join(_currpath + [attr]))
426                 paths += option.getpaths(include_groups=include_groups,
427                                          _currpath=_currpath + [attr])
428             else:
429                 paths.append('.'.join(_currpath + [attr]))
430         return paths
431
432     def getchildren(self):
433         return self._children[1]
434
435     def build_cache(self, cache_path=None, cache_option=None, _currpath=None):
436         if _currpath is None and self._cache_paths is not None:
437             return
438         if _currpath is None:
439             save = True
440             _currpath = []
441         else:
442             save = False
443         if cache_path is None:
444             cache_path = [self._name]
445             cache_option = [self]
446         for option in self._children[1]:
447             attr = option._name
448             if attr.startswith('_cfgimpl'):
449                 continue
450             cache_option.append(option)
451             cache_path.append(str('.'.join(_currpath + [attr])))
452             if isinstance(option, OptionDescription):
453                 _currpath.append(attr)
454                 option.build_cache(cache_path, cache_option, _currpath)
455                 _currpath.pop()
456         if save:
457             #valid no duplicated option
458             valid_child = copy(cache_option)
459             valid_child.sort()
460             old = None
461             for child in valid_child:
462                 if child == old:
463                     raise ConflictConfigError('duplicate option: '
464                                               '{0}'.format(child))
465                 old = child
466             self._cache_paths = (tuple(cache_option), tuple(cache_path))
467
468     def get_opt_by_path(self, path):
469         try:
470             return self._cache_paths[0][self._cache_paths[1].index(path)]
471         except ValueError:
472             raise NotFoundError('no option for path {}'.format(path))
473
474     def get_path_by_opt(self, opt):
475         try:
476             return self._cache_paths[1][self._cache_paths[0].index(opt)]
477         except ValueError:
478             raise NotFoundError('no option {} found'.format(opt))
479
480     # ____________________________________________________________
481     def set_group_type(self, group_type):
482         """sets a given group object to an OptionDescription
483
484         :param group_type: an instance of `GroupType` or `MasterGroupType`
485                               that lives in `setting.groups`
486         """
487         if self._group_type != groups.default:
488             ConfigError('cannot change group_type if already set '
489                         '(old {}, new {})'.format(self._group_type, group_type))
490         if isinstance(group_type, groups.GroupType):
491             self._group_type = group_type
492             if isinstance(group_type, groups.MasterGroupType):
493                 #if master (same name has group) is set
494                 identical_master_child_name = False
495                 #for collect all slaves
496                 slaves = []
497                 master = None
498                 for child in self._children[1]:
499                     if isinstance(child, OptionDescription):
500                         raise ConfigError("master group {} shall not have "
501                                           "a subgroup".format(self._name))
502                     if not child.multi:
503                         raise ConfigError("not allowed option {0} in group {1}"
504                                           ": this option is not a multi"
505                                           "".format(child._name, self._name))
506                     if child._name == self._name:
507                         identical_master_child_name = True
508                         child.multitype = multitypes.master
509                         master = child
510                     else:
511                         slaves.append(child)
512                 if master is None:
513                     raise ConfigError('master group with wrong master name for {}'
514                                       ''.format(self._name))
515                 master.master_slaves = tuple(slaves)
516                 for child in self._children[1]:
517                     if child != master:
518                         child.master_slaves = master
519                         child.multitype = multitypes.slave
520                 if not identical_master_child_name:
521                     raise ConfigError("the master group: {} has not any "
522                                       "master child".format(self._name))
523         else:
524             raise ConfigError('not allowed group_type : {0}'.format(group_type))
525
526     def get_group_type(self):
527         return self._group_type
528
529
530 def validate_requires_arg(requires, name):
531     "check malformed requirements"
532     if requires is not None:
533         config_action = {}
534         for req in requires:
535             if not type(req) == tuple:
536                 raise RequiresError("malformed requirements type for option:"
537                                     " {0}, must be a tuple".format(name))
538             if len(req) == 3:
539                 action = req[2]
540                 inverse = False
541             elif len(req) == 4:
542                 action = req[2]
543                 inverse = req[3]
544             else:
545                 raise RequiresError("malformed requirements for option: {0}"
546                                     " invalid len".format(name))
547             if action in config_action:
548                 if inverse != config_action[action]:
549                     raise RequiresError("inconsistency in action types for option: {0}"
550                                         " action: {1}".format(name, action))
551             else:
552                 config_action[action] = inverse