From 5893f8ad721681798bbd108f6c4b730986aeae4c Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Fri, 30 Aug 2013 21:15:55 +0200 Subject: [PATCH] attributes in Option are now read-only if option set in Config (_name is everytime read-only) --- test/test_slots.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++ tiramisu/option.py | 47 ++++++++++++++++++++++++---- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/test/test_slots.py b/test/test_slots.py index 524006f..f99484f 100644 --- a/test/test_slots.py +++ b/test/test_slots.py @@ -37,6 +37,84 @@ def test_slots_option(): raises(AttributeError, "c.x = 1") +def test_slots_option_readonly(): + a = ChoiceOption('a', '', ('a',)) + b = BoolOption('b', '') + c = IntOption('c', '') + d = FloatOption('d', '') + e = StrOption('e', '') + g = UnicodeOption('g', '') + h = IPOption('h', '') + i = PortOption('i', '') + j = NetworkOption('j', '') + k = NetmaskOption('k', '') + l = DomainnameOption('l', '') + m = OptionDescription('m', '', [a, b, c, d, e, g, h, i, j, k, l]) + a._requires = 'a' + b._requires = 'b' + c._requires = 'c' + d._requires = 'd' + e._requires = 'e' + g._requires = 'g' + h._requires = 'h' + i._requires = 'i' + j._requires = 'j' + k._requires = 'k' + l._requires = 'l' + m._requires = 'm' + Config(m) + raises(AttributeError, "a._requires = 'a'") + raises(AttributeError, "b._requires = 'b'") + raises(AttributeError, "c._requires = 'c'") + raises(AttributeError, "d._requires = 'd'") + raises(AttributeError, "e._requires = 'e'") + raises(AttributeError, "g._requires = 'g'") + raises(AttributeError, "h._requires = 'h'") + raises(AttributeError, "i._requires = 'i'") + raises(AttributeError, "j._requires = 'j'") + raises(AttributeError, "k._requires = 'k'") + raises(AttributeError, "l._requires = 'l'") + raises(AttributeError, "m._requires = 'm'") + + +def test_slots_option_readonly_name(): + a = ChoiceOption('a', '', ('a',)) + b = BoolOption('b', '') + c = IntOption('c', '') + d = FloatOption('d', '') + e = StrOption('e', '') + f = SymLinkOption('f', c) + g = UnicodeOption('g', '') + h = IPOption('h', '') + i = PortOption('i', '') + j = NetworkOption('j', '') + k = NetmaskOption('k', '') + l = DomainnameOption('l', '') + m = OptionDescription('m', '', [a, b, c, d, e, f, g, h, i, j, k, l]) + raises(AttributeError, "a._name = 'a'") + raises(AttributeError, "b._name = 'b'") + raises(AttributeError, "c._name = 'c'") + raises(AttributeError, "d._name = 'd'") + raises(AttributeError, "e._name = 'e'") + raises(AttributeError, "f._name = 'f'") + raises(AttributeError, "g._name = 'g'") + raises(AttributeError, "h._name = 'h'") + raises(AttributeError, "i._name = 'i'") + raises(AttributeError, "j._name = 'j'") + raises(AttributeError, "k._name = 'k'") + raises(AttributeError, "l._name = 'l'") + raises(AttributeError, "m._name = 'm'") + + +def test_slots_description(): + # __slots__ for OptionDescription must be complete + slots = set() + for subclass in OptionDescription.__mro__: + if subclass is not object: + slots.update(subclass.__slots__) + assert slots == set(OptionDescription.__slots__) + + def test_slots_config(): od1 = OptionDescription('a', '', []) od2 = OptionDescription('a', '', [od1]) diff --git a/tiramisu/option.py b/tiramisu/option.py index 031fb50..50625fb 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -91,7 +91,37 @@ class BaseInformation(object): self.__class__.__name__)) -class Option(BaseInformation): +class _ReadOnlyOption(BaseInformation): + __slots__ = ('_readonly',) + + def __setattr__(self, name, value): + is_readonly = False + # never change _name + if name == '_name': + try: + self._name + #so _name is already set + is_readonly = True + except: + pass + try: + if self._readonly is True: + if value is True: + # already readonly and try to re set readonly + # don't raise, just exit + return + is_readonly = True + except AttributeError: + pass + if is_readonly: + raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is" + " read-only").format( + self.__class__.__name__, self._name, + name)) + object.__setattr__(self, name, value) + + +class Option(_ReadOnlyOption): """ Abstract base class for configuration option's. @@ -450,10 +480,9 @@ else: raise ValueError(_('value must be an unicode')) -class SymLinkOption(object): +class SymLinkOption(_ReadOnlyOption): __slots__ = ('_name', '_opt') _opt_type = 'symlink' - _consistencies = None def __init__(self, name, opt): self._name = name @@ -462,9 +491,10 @@ class SymLinkOption(object): 'must be an option ' 'for symlink {0}').format(name)) self._opt = opt + self._readonly = True def __getattr__(self, name): - if name in ('_name', '_opt', '_consistencies'): + if name in ('_name', '_opt', '_opt_type', '_readonly'): return object.__getattr__(self, name) else: return getattr(self._opt, name) @@ -684,13 +714,13 @@ class DomainnameOption(Option): raise ValueError(_('invalid domainname')) -class OptionDescription(BaseInformation): +class OptionDescription(_ReadOnlyOption): """Config's schema (organisation, group) and container of Options The `OptionsDescription` objects lives in the `tiramisu.config.Config`. """ __slots__ = ('_name', '_requires', '_cache_paths', '_group_type', '_properties', '_children', '_consistencies', - '_calc_properties', '__weakref__') + '_calc_properties', '__weakref__', '_readonly', '_impl_informations') def __init__(self, name, doc, children, requires=None, properties=None): """ @@ -731,6 +761,8 @@ class OptionDescription(BaseInformation): return self.impl_get_information('doc') def __getattr__(self, name): + if name in self.__slots__: + return object.__getattribute__(self, name) try: return self._children[1][self._children[0].index(name)] except ValueError: @@ -769,6 +801,7 @@ class OptionDescription(BaseInformation): _currpath=None, _consistencies=None): if _currpath is None and self._cache_paths is not None: + # cache already set return if _currpath is None: save = True @@ -787,6 +820,7 @@ class OptionDescription(BaseInformation): raise ConflictError(_('duplicate option: {0}').format(option)) cache_option.append(option) + option._readonly = True cache_path.append(str('.'.join(_currpath + [attr]))) if not isinstance(option, OptionDescription): if option._consistencies is not None: @@ -807,6 +841,7 @@ class OptionDescription(BaseInformation): if save: self._cache_paths = (tuple(cache_option), tuple(cache_path)) self._consistencies = _consistencies + self._readonly = True def impl_get_opt_by_path(self, path): try: