attributes in Option are now read-only if option set in Config (_name is everytime...
authorEmmanuel Garette <egarette@cadoles.com>
Fri, 30 Aug 2013 19:15:55 +0000 (21:15 +0200)
committerEmmanuel Garette <egarette@cadoles.com>
Fri, 30 Aug 2013 19:15:55 +0000 (21:15 +0200)
test/test_slots.py
tiramisu/option.py

index 524006f..f99484f 100644 (file)
@@ -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])
index 031fb50..50625fb 100644 (file)
@@ -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: