Repository: jbasko/configmanager

Issue:
len(Config)?
What would you expect -- number of sections or number of items? Probably items.

Discussion:

Patch:
diff --git a/configmanager/__init__.py b/configmanager/__init__.py
index 246eadc..f2d2fcc 100644
--- a/configmanager/__init__.py
+++ b/configmanager/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '1.2.0'
+__version__ = '1.3.0'
 
 from .managers import Config
 from .items import Item
diff --git a/configmanager/managers.py b/configmanager/managers.py
index 5a611d8..a0f6346 100644
--- a/configmanager/managers.py
+++ b/configmanager/managers.py
@@ -49,6 +49,7 @@ class Config(BaseSection):
         instance = super(Config, cls).__new__(cls)
 
         instance._cm__section = None
+        instance._cm__section_alias = None
         instance._cm__configs = collections.OrderedDict()
         instance._cm__configparser_adapter = None
         instance._cm__json_adapter = None
@@ -68,10 +69,41 @@ class Config(BaseSection):
     def __repr__(self):
         return '<{cls} at {id}>'.format(cls=self.__class__.__name__, id=id(self))
 
-    def __contains__(self, item):
-        return item in self._cm__configs
+    def _resolve_config_key(self, key):
+        if isinstance(key, six.string_types):
+            return self._cm__configs[key]
+        elif isinstance(key, (tuple, list)) and len(key) > 0:
+            if len(key) == 1:
+                return self[key[0]]
+            else:
+                return self[key[0]][key[1:]]
+        else:
+            raise TypeError('Expected either a string or a tuple as key, got {!r}'.format(key))
+
+    def __contains__(self, key):
+        try:
+            _ = self._resolve_config_key(key)
+            return True
+        except KeyError:
+            return False
+
+    def __setitem__(self, key, value):
+        if isinstance(key, six.string_types):
+            name = key
+            rest = None
+        elif isinstance(key, (tuple, list)) and len(key) > 0:
+            name = key[0]
+            if len(key) == 1:
+                rest = None
+            else:
+                rest = key[1:]
+        else:
+            raise TypeError('Expected either a string or a tuple as key, got {!r}'.format(key))
+
+        if rest:
+            self[name][rest] = value
+            return
 
-    def __setitem__(self, name, value):
         if is_config_item(value):
             self.cm__add_item(name, value)
         elif isinstance(value, self.__class__):
@@ -85,8 +117,8 @@ class Config(BaseSection):
                 )
             )
 
-    def __getitem__(self, name):
-        return self._cm__configs[name]
+    def __getitem__(self, key):
+        return self._resolve_config_key(key)
 
     def __getattr__(self, name):
         if name in self._cm__configs:
@@ -110,6 +142,15 @@ class Config(BaseSection):
                 )
             )
 
+    def __len__(self):
+        return sum(1 for _ in self.iter_items())
+
+    def __nonzero__(self):
+        return True
+
+    def __bool__(self):
+        return True
+
     def iter_items(self):
         """
         Iterate over all items contained (recursively).
@@ -224,6 +265,17 @@ class Config(BaseSection):
         """
         return self._cm__section
 
+    @property
+    def alias(self):
+        """
+        Returns alias with which this section was added to another or ``None`` if it hasn't been added
+        to any.
+        
+        Returns:
+            (str)
+        """
+        return self._cm__section_alias
+
     def added_to_section(self, alias, section):
         """
         A hook that is called when this section is added to another.
@@ -234,6 +286,7 @@ class Config(BaseSection):
             section (:class:`.Config`): section to which this section has been added
         """
         self._cm__section = section
+        self._cm__section_alias = alias
 
     @property
     def configparser(self):
diff --git a/configmanager/persistence.py b/configmanager/persistence.py
index 2a284f2..9e4e360 100644
--- a/configmanager/persistence.py
+++ b/configmanager/persistence.py
@@ -126,7 +126,7 @@ class JsonAdapter(object):
         else:
             self._config.read_dict(json.load(source), as_defaults=as_defaults)
 
-    def write(self, destination, with_defaults=True):
+    def write(self, destination, with_defaults=False):
         if isinstance(destination, six.string_types):
             with open(destination, 'w') as f:
                 json.dump(self._config.to_dict(with_defaults=with_defaults), f)
diff --git a/requirements.txt b/requirements.txt
index b2945bc..b7fcb9f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,7 @@ six
 configparser
 
 pytest
+pytest-random-order
 coverage
 tox
 


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 856120e..562ab1c 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -343,7 +343,6 @@ def test_can_inspect_config_contents(mixed_app_config):
 
     assert 'db' in config
     assert 'dbe' not in config
-    assert ('db',) not in config
 
     assert 'logging' in config
 
@@ -379,4 +378,67 @@ def test_can_have_a_dict_as_a_config_value_if_wrapped_inside_item():
     # value, not the real thing.
     config.aws.value['secret_key'] = 'NEW_SECRET'
 
-    assert config.to_dict()['aws'] == {'access_key': '123', 'secret_key': 'secret'}
\ No newline at end of file
+    assert config.to_dict()['aws'] == {'access_key': '123', 'secret_key': 'secret'}
+
+
+def test_len_of_config_returns_number_of_items_in_it():
+    assert len(Config()) == 0
+
+    assert len(Config({'enabled': True})) == 1
+
+    assert len(Config({'uploads': Config()})) == 0
+    assert len(Config({'uploads': {}})) == 0
+
+    assert len(Config({'uploads': {'enabled': False}})) == 1
+    assert len(Config({'uploads': {'enabled': False, 'threads': 1}})) == 2
+
+    assert len(Config({'uploads': {'enabled': False, 'threads': 0}, 'greeting': 'Hi'})) == 3
+
+
+def test__getitem__handles_paths_to_sections_and_items_and_so_does__contains__():
+    config = Config()
+    with pytest.raises(KeyError):
+        assert not config['uploads', 'enabled']
+    assert ('uploads',) not in config
+    assert ('uploads', 'enabled') not in config
+
+    config.uploads = Config({'enabled': True, 'db': {'user': 'root'}})
+    assert config['uploads', 'enabled'] is config.uploads.enabled
+    assert config['uploads', 'db'] is config.uploads.db
+
+    assert 'uploads' in config
+    assert ('uploads',) in config
+    assert ('uploads', 'enabled') in config
+    assert ('uploads', 'db') in config
+    assert ('uploads', 'db', 'user') in config
+
+    assert config.uploads.db.user.value == 'root'
+
+    config['uploads', 'db', 'user'].set('admin')
+    assert config.uploads.db.user.value == 'admin'
+
+
+def test_can_use__setitem__to_create_new_deep_paths():
+    config = Config()
+    config['uploads'] = Config({'enabled': True})
+
+    with pytest.raises(TypeError):
+        config['uploads', 'threads'] = 5
+
+    config['uploads', 'threads'] = Item(value=5)
+    assert config.uploads.threads.type is int
+
+    config['uploads', 'db'] = Config({'user': 'root'})
+    assert config.uploads.db
+
+
+def test_section_knows_its_alias():
+    config = Config()
+    config.uploads = Config({
+        'enabled': True
+    })
+    assert config.uploads.alias == 'uploads'
+
+    config.uploads.db = Config({'connection': {'user': 'root'}})
+    assert config.uploads.db.alias == 'db'
+    assert config.uploads.db.connection.alias == 'connection'
diff --git a/tests/test_json.py b/tests/test_json.py
index 68c9c6a..0e7530b 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -91,3 +91,24 @@ def test_json_read_and_write(defaults_json_path, user_json_path):
     c1.json.read(user_json_path)
 
     assert c1.to_dict() == c2.to_dict() == c3.to_dict()
+
+
+def test_json_writes_with_defaults_false_by_default(user_json_path):
+    c = Config({'greeting': 'Hello'})
+    c.json.write(user_json_path)
+
+    d = Config()
+    d.json.read(user_json_path, as_defaults=True)
+    assert len(d) == 0
+
+    c.json.write(user_json_path, with_defaults=True)
+
+    d.json.read(user_json_path, as_defaults=True)
+    assert len(d) == 1
+    assert d.greeting.value == 'Hello'
+
+    c.greeting.value = 'Hey!'
+    c.json.write(user_json_path)
+
+    d.json.read(user_json_path, as_defaults=True)
+    assert d.greeting.value == 'Hey!'