Merge pull request #296 from sputnik13/refactor_config

diff --git a/lib/muchos/config/base.py b/lib/muchos/config/base.py
index d487a7f..b7a2e2a 100644
--- a/lib/muchos/config/base.py
+++ b/lib/muchos/config/base.py
@@ -164,7 +164,7 @@
                         # the cluster specific config
                         filter(lambda t: t.class_name.lower() in f,
                         # get all ansible vars of var_type
-                        get_ansible_vars(var_type))}
+                        get_ansible_vars(var_type, type(self)))}
 
     @abstractmethod
     def verify_config(self, action):
diff --git a/lib/muchos/config/decorators.py b/lib/muchos/config/decorators.py
index e3a709f..3fcd6fb 100644
--- a/lib/muchos/config/decorators.py
+++ b/lib/muchos/config/decorators.py
@@ -17,6 +17,7 @@
 
 from collections.abc import Iterable
 from functools import wraps
+from pydoc import locate
 
 
 # struct to hold information about ansible vars defined via decorators.
@@ -25,10 +26,16 @@
 # property_name indicates the class property/function where the variable was
 #               defined
 class _ansible_var(object):
-    def __init__(self, var_name, class_name, property_name):
+    def __init__(self, var_name, class_name, property_name, module_name):
         self.var_name = var_name
         self.class_name = class_name
         self.property_name = property_name
+        self.module_name = module_name
+
+    def __str__(self):
+        return 'var_name={}, class_name={}, property_name={}, module_name={}'.format(
+            self.var_name, self.class_name, self.property_name, self.module_name
+        )
 
 # each entry of _ansible_vars will contain a list of _ansible_var instances
 _ansible_vars = dict(
@@ -37,8 +44,11 @@
     extra=[]
 )
 
-def get_ansible_vars(var_type):
-    return _ansible_vars.get(var_type)
+def get_ansible_vars(var_type, class_in_scope):
+    # return variables for the complete class hierarchy
+    return list(filter(lambda v:
+        issubclass(class_in_scope, locate(v.module_name + "." + v.class_name)),
+        _ansible_vars.get(var_type)))
 
 # ansible hosts inventory variables
 def ansible_host_var(name=None):
@@ -57,7 +67,8 @@
         ansible_var = _ansible_var(
             var_name=name if isinstance(name, str) else func.__name__,
             class_name=func.__qualname__.split('.')[0],
-            property_name=func.__name__)
+            property_name=func.__name__,
+            module_name=func.__module__)
         _ansible_vars[var_type].append(ansible_var)
         return func
 
@@ -108,4 +119,3 @@
 class ConfigMissingError(Exception):
     def __init__(self, name):
         super(ConfigMissingError, self).__init__("{} is missing from the configuration".format(name))
-
diff --git a/lib/tests/test_decorators.py b/lib/tests/test_decorators.py
new file mode 100644
index 0000000..95a17cb
--- /dev/null
+++ b/lib/tests/test_decorators.py
@@ -0,0 +1,132 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from unittest import TestCase
+
+from muchos.config import decorators
+
+
+class DecoratedThing(object):
+    @property
+    @decorators.ansible_host_var
+    def host_var1(self):
+        return 'host_var1'
+
+    @property
+    @decorators.ansible_host_var(name='named_host_var2')
+    def host_var2(self):
+        return 'named_host_var2'
+
+    @property
+    @decorators.ansible_play_var
+    def play_var1(self):
+        return 'play_var1'
+
+    @property
+    @decorators.ansible_play_var(name='named_play_var2')
+    def play_var2(self):
+        return 'named_play_var2'
+
+    @property
+    @decorators.ansible_extra_var
+    def extra_var1(self):
+        return 'extra_var1'
+
+    @property
+    @decorators.ansible_extra_var(name='named_extra_var2')
+    def extra_var2(self):
+        return 'named_extra_var2'
+
+    @property
+    @decorators.default('default_val')
+    def default_val(self):
+        return None
+
+    @property
+    @decorators.default(True)
+    def default_boolean_val_True(self):
+        return True
+
+    @property
+    @decorators.default(True)
+    def default_boolean_val_False(self):
+        return False
+
+    @property
+    @decorators.default(True)
+    def default_missing_boolean_val(self):
+        return None
+
+    @property
+    @decorators.required
+    def required_val(self):
+        return 'required_val'
+
+    @property
+    @decorators.required
+    def missing_required_val(self):
+        return None
+
+
+class DecoratorTests(TestCase):
+    def _flatten_dict(d):
+        return {(k, v) for k, v in d.items()}
+
+    def test_decorators(self):
+        thing = DecoratedThing()
+
+        actual_host_vars = decorators.get_ansible_vars('host', type(thing))
+        actual_play_vars = decorators.get_ansible_vars('play', type(thing))
+        actual_extra_vars = decorators.get_ansible_vars('extra', type(thing))
+
+        expected_host_vars = [
+            decorators._ansible_var('host_var1', 'DecoratedThing', 'host_var1', 'tests.test_decorators'),
+            decorators._ansible_var('named_host_var2', 'DecoratedThing', 'host_var2', 'tests.test_decorators')
+        ]
+
+        expected_play_vars = [
+            decorators._ansible_var('play_var1', 'DecoratedThing', 'play_var1', 'tests.test_decorators'),
+            decorators._ansible_var('named_play_var2', 'DecoratedThing', 'play_var2', 'tests.test_decorators')
+        ]
+
+        expected_extra_vars = [
+            decorators._ansible_var('extra_var1', 'DecoratedThing', 'extra_var1', 'tests.test_decorators'),
+            decorators._ansible_var('named_extra_var2', 'DecoratedThing', 'extra_var2', 'tests.test_decorators')
+        ]
+
+        self.assertEquals(
+            set([str(v) for v in expected_host_vars]),
+            set([str(v) for v in actual_host_vars])
+        )
+
+        self.assertEquals(
+            set([str(v) for v in expected_play_vars]),
+            set([str(v) for v in actual_play_vars])
+        )
+
+        self.assertEquals(
+            set([str(v) for v in expected_extra_vars]),
+            set([str(v) for v in actual_extra_vars])
+        )
+
+        self.assertEquals(thing.default_val, 'default_val')
+        self.assertEquals(thing.default_boolean_val_True, True)
+        self.assertEquals(thing.default_boolean_val_False, False)
+        self.assertEquals(thing.default_missing_boolean_val, True)
+        self.assertEquals(thing.required_val, 'required_val')
+        with self.assertRaises(decorators.ConfigMissingError):
+            thing.missing_required_val
\ No newline at end of file
diff --git a/lib/tests/test_validators.py b/lib/tests/test_validators.py
new file mode 100644
index 0000000..0785717
--- /dev/null
+++ b/lib/tests/test_validators.py
@@ -0,0 +1,150 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from unittest import TestCase
+
+from muchos.config import validators
+from muchos.config.decorators import is_valid
+
+
+class ValidateThis(object):
+    @property
+    @is_valid(validators.greater_than(5))
+    def fourNotGreaterThanFive(self):
+        return 4
+
+    @property
+    @is_valid(validators.greater_than(5))
+    def fiveNotGreaterThanFive(self):
+        return 5
+
+    @property
+    @is_valid(validators.greater_than(5))
+    def sixGreaterThanFive(self):
+        return 6
+
+    @property
+    @is_valid(validators.less_than(5))
+    def fourLessThanFive(self):
+        return 4
+
+    @property
+    @is_valid(validators.less_than(5))
+    def fiveNotLessThanFive(self):
+        return 5
+
+    @property
+    @is_valid(validators.less_than(5))
+    def sixNotLessThanFive(self):
+        return 6
+
+    @property
+    @is_valid(validators.equals(5))
+    def fourNotEqualFive(self):
+        return 4
+
+    @property
+    @is_valid(validators.equals(5))
+    def fiveEqualFive(self):
+        return 5
+
+    @property
+    @is_valid(validators.equals(5))
+    def sixeNotEqualFive(self):
+        return 6
+
+    @property
+    @is_valid(validators.contains(5))
+    def containsFive(self):
+        return [4,5,6]
+
+    @property
+    @is_valid(validators.contains(5))
+    def notContainsFive(self):
+        return []
+
+    @property
+    @is_valid(validators.is_in([5]))
+    def fourNotInListOfFive(self):
+        return 4
+
+    @property
+    @is_valid(validators.is_in([5]))
+    def fiveInListOfFive(self):
+        return 5
+
+    @property
+    @is_valid(validators.is_in([5]))
+    def sixNotInListOfFive(self):
+        return 6
+
+    @property
+    @is_valid(validators.is_type(str))
+    def intIsNotString(self):
+        return 5
+
+    @property
+    @is_valid(validators.is_type(str))
+    def stringIsString(self):
+        return 'some string'
+
+
+class ValidationTests(TestCase):
+    def test_validators(self):
+        thing = ValidateThis()
+
+        with self.assertRaises(Exception):
+            thing.fourNotGreaterThanFive
+
+        with self.assertRaises(Exception):
+            thing.fiveNotGreaterThanFive
+
+        self.assertEqual(thing.sixGreaterThanFive, 6)
+
+        self.assertEqual(thing.fourLessThanFive, 4)
+
+        with self.assertRaises(Exception):
+            thing.fiveNotLessThanFive
+
+        with self.assertRaises(Exception):
+            thing.sixNotLessThanFive
+
+        with self.assertRaises(Exception):
+            thing.fourNotEqualFive
+
+        self.assertEqual(thing.fiveEqualFive, 5)
+
+        with self.assertRaises(Exception):
+            thing.sixeNotEqualFive
+
+        self.assertEqual(thing.containsFive, [4,5,6])
+
+        with self.assertRaises(Exception):
+            thing.notContainsFive
+
+        with self.assertRaises(Exception):
+            thing.fourNotInListOfFive
+
+        self.assertEqual(thing.fiveInListOfFive, 5)
+
+        with self.assertRaises(Exception):
+            thing.sixNotInListOfFive
+
+        with self.assertRaises(Exception):
+            thing.intIsNotString
+
+        self.assertEqual(thing.stringIsString, 'some string')
\ No newline at end of file