Repository: clueboard/milc

Issue:
Bug: Can't set config values via attribute
Test case:

```
import milc.configuration

config = milc.configuration.Configuration()
config.a.a = True
print(config['a']['a'])
```

Expected result: `True`

Actual result: `None`

Discussion:

Patch:
diff --git a/milc/attrdict.py b/milc/attrdict.py
index 0ac22fa..66b1f90 100644
--- a/milc/attrdict.py
+++ b/milc/attrdict.py
@@ -34,12 +34,10 @@ class AttrDict(object):
         return self._data[key]
 
     def __setitem__(self, key, value):
-        self.__dict__[key] = value
         self._data[key] = value
+        self.__setattr__(key, value)
 
     def __delitem__(self, key):
-        if key in self.__dict__ and key[0] != '_':
-            del self.__dict__[key]
         if key in self._data:
             del self._data[key]
 
@@ -54,6 +52,6 @@ class SparseAttrDict(AttrDict):
         """Returns an item, creating it if it doesn't already exist
         """
         if key not in self._data:
-            self.__dict__[key] = self._data[key] = None
+            self._data[key] = None
 
         return self._data[key]
diff --git a/milc/configuration.py b/milc/configuration.py
index c307b7e..add9832 100644
--- a/milc/configuration.py
+++ b/milc/configuration.py
@@ -11,7 +11,7 @@ class Configuration(AttrDict):
         """Returns a config section, creating it if it doesn't exist yet.
         """
         if key not in self._data:
-            self.__dict__[key] = self._data[key] = ConfigurationSection(self)
+            self._data[key] = ConfigurationSection(self)
 
         return self._data[key]
 
@@ -19,7 +19,7 @@ class Configuration(AttrDict):
 class ConfigurationSection(Configuration):
     def __init__(self, parent, *args, **kwargs):
         super(ConfigurationSection, self).__init__(*args, **kwargs)
-        self.parent = parent
+        self._parent = parent
 
     def __getitem__(self, key):
         """Returns a config value, pulling from the `user` section as a fallback.
@@ -28,8 +28,8 @@ class ConfigurationSection(Configuration):
         if key in self._data and self._data.get(key) is not None:
             return self._data[key]
 
-        elif key in self.parent.user:
-            return self.parent.user[key]
+        elif key in self._parent.user:
+            return self._parent.user[key]
 
         return None
 
@@ -37,11 +37,19 @@ class ConfigurationSection(Configuration):
         """Returns the config value from the `user` section.
         This is called when the attribute is accessed via dot notation but does not exist.
         """
-        if key in self.parent.user:
-            return self.parent.user[key]
+        if key[0] != '_' and key in self._parent['user']:
+            return self._parent['user'][key]
 
         return None
 
+    def __setattr__(self, key, value):
+        """Sets dictionary value when an attribute is set.
+        """
+        super().__setattr__(key, value)
+
+        if key[0] != '_':
+            self._data[key] = value
+
 
 class SubparserWrapper(object):
     """Wrap subparsers so we can track what options the user passed.
diff --git a/setup.cfg b/setup.cfg
index 26f4098..8002e66 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -17,9 +17,9 @@ universal = 1
 [flake8]
 format = pylint
 max_line_length = 200
-ignore = 
+ignore =
 	E501
-per_file_ignores = 
+per_file_ignores =
 	**/__init__.py:F401
 	tests/**:N802
 max_complexity = 14
@@ -51,13 +51,13 @@ continuation_indent_width = 4
 dedent_closing_brackets = True
 disable_ending_comma_heuristic = False
 each_dict_entry_on_separate_line = True
-i18n_comment = 
-i18n_function_call = 
+i18n_comment =
+i18n_function_call =
 indent_blank_lines = False
 indent_dictionary_value = True
 indent_width = 4
 join_multiple_lines = False
-no_spaces_around_selected_binary_operators = 
+no_spaces_around_selected_binary_operators =
 spaces_around_default_or_named_assign = False
 spaces_around_power_operator = False
 spaces_before_comment = 2


Test patch:
diff --git a/tests/test_configuration_Configuration.py b/tests/test_configuration_Configuration.py
index 44bbe76..a329b23 100644
--- a/tests/test_configuration_Configuration.py
+++ b/tests/test_configuration_Configuration.py
@@ -26,6 +26,14 @@ def test_Configuration_dictionary():
     assert config['a']['c'] is None
 
 
+def test_Configuration_dictionary_write():
+    """Test that Configuration dictionary writes can be read as attributes.
+    """
+    config = milc.configuration.Configuration()
+    config['a']['a'] = True
+    assert config.a.a is True
+
+
 def test_Configuration_attribute():
     """Test Configuration attribute access
     """
@@ -36,6 +44,14 @@ def test_Configuration_attribute():
     assert config.a.c is None
 
 
+def test_Configuration_attribute_write():
+    """Test that Configuration attribute writes can be read as a dictionary method.
+    """
+    config = milc.configuration.Configuration()
+    config.a.a = True
+    assert config['a']['a'] is True
+
+
 def test_Configuration_iteration():
     """Make sure we can iterate over our stored volues.
     """