Skip to content

Commit

Permalink
Merge pull request #4 from jedipi/feature/subclassed-config-support
Browse files Browse the repository at this point in the history
Feature/subclassed config support
  • Loading branch information
staffanm committed Jul 27, 2016
2 parents 05116ae + aec6af1 commit 8072fd5
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 27 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ output/*.html
output/*/index.html

# Sphinx
docs/_build
docs/_build

# Pycharm
.idea/
24 changes: 12 additions & 12 deletions layeredconfig/layeredconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, *sources, **kwargs):
:type cascade: bool
:param writable: Whether configuration values should be mutable.
``True`` by default. This does not affect
:py:meth:`~Layeredconfig.set`.
:py:meth:`~Layeredconfig.set`.
:type writable: bool
"""
Expand All @@ -67,7 +67,7 @@ def __init__(self, *sources, **kwargs):
# we couldn't get any subsections for source, perhaps
# because it's an "empty" source. Well, that's ok.
pass

for k in sectionkeys:
# 2. find all subsections in all of our sources
s = []
Expand All @@ -86,9 +86,9 @@ def __init__(self, *sources, **kwargs):
empty=True,
cascade=self._cascade))
# 3. create a LayeredConfig object for the subsection
c = LayeredConfig(*s,
cascade=self._cascade,
writable=self._writable)
c = self.__class__(*s,
cascade=self._cascade,
writable=self._writable)
c._sectionkey = k
c._parent = self
self._subsections[k] = c
Expand All @@ -104,12 +104,12 @@ def write(config):
assignment. The modifications are written to the first
writable source in this config object.
.. note::
.. note::
This is a static method, ie not a method on any object
instance. This is because all attribute access on a
LayeredConfig object is meant to retrieve configuration
settings.
settings.
:param config: The configuration object to save
:type config: layeredconfig.LayeredConfig
Expand All @@ -133,7 +133,7 @@ def set(config, key, value, sourceid="defaults"):
:param config: The configuration object to set values on
:param key: The parameter name
:param value: The new value
:param sourceid: The identifier for the underlying source that the
:param sourceid: The identifier for the underlying source that the
value should be set on.
"""
for source in config._sources:
Expand All @@ -153,7 +153,7 @@ def get(config, key, default=None):
return default

# These are methods i'd like to implement next
#
#
# @staticmethod
# def where(config, key):
# """returns the identifier of a source where a given key is found, or None."""
Expand Down Expand Up @@ -195,7 +195,7 @@ def boolconvert(value):
"""Convert the string *value* to a boolean. ``"True"`` is converted to
``True`` and ``"False"`` is converted to ``False``.
.. note::
.. note::
If value is neither "True" nor "False", it's returned unchanged.
Expand Down Expand Up @@ -251,7 +251,7 @@ def __getattr__(self, name):
if source.typed(name):
return source.get(name)
else:
# we need to find a typesource for this value.
# we need to find a typesource for this value.
done = False
this = self
while not done:
Expand All @@ -273,7 +273,7 @@ def __getattr__(self, name):
else:
if self._cascade and self._parent:
return self._parent.__getattr__(name)

raise AttributeError("Configuration key %s doesn't exist" % name)

def __setattr__(self, name, value):
Expand Down
41 changes: 27 additions & 14 deletions tests/test_layeredconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

if sys.version_info < (2, 7, 0): # pragma: no cover
import unittest2 as unittest
else:
else:
import unittest
import requests
# The system under test
Expand Down Expand Up @@ -57,7 +57,7 @@ def _test_config_singlesection(self, cfg):
bool_transform = self.transforms.get(bool, bool_type)
self.assertIs(type(cfg.force), bool_type)
self.assertEqual(cfg.force, bool_transform(True))

if list in self.supported_types:
list_type = list
list_want = ['foo', 'bar']
Expand Down Expand Up @@ -170,7 +170,7 @@ def test_has(self):
def test_typed(self):
for key in self.simple.keys():
self.assertTrue(self.simple.typed(key))

def test_get(self):
# FIXME: This test should be able to look at supported_types
# like test_singlesection and test_subsections do, so derived
Expand Down Expand Up @@ -201,7 +201,7 @@ def test_layered_subsections(self):
# file-based sources) -- if the highest-priority source has
# subsections, and a lower-priority file-based source lacks
# those subsections, bad things would happen.
#
#
# see https://github.com/staffanm/layeredconfig/issues/2
cfg = LayeredConfig(self.complex, self.extra)
self._test_layered_configs(cfg)
Expand Down Expand Up @@ -283,7 +283,7 @@ class TestINIFile(TestINIFileHelper, unittest.TestCase,

supported_types = (str,)
supports_nesting = False

def setUp(self):
super(TestINIFile, self).setUp()
self.simple = INIFile("simple.ini")
Expand Down Expand Up @@ -318,7 +318,7 @@ def test_inifile_default_as_root(self):
# load a modified version of complex.ini
with open("complex.ini") as fp:
ini = fp.read()

with open("complex-otherroot.ini", "w") as fp:
fp.write(ini.replace("[__root__]", "[DEFAULT]"))
cfg = LayeredConfig(INIFile("complex-otherroot.ini",
Expand Down Expand Up @@ -398,7 +398,7 @@ def test_write(self):
class TestJSONFile(unittest.TestCase, TestConfigSourceHelper):

supported_types = (str, int, bool, list)

def setUp(self):
with open("simple.json", "w") as fp:
fp.write("""
Expand Down Expand Up @@ -502,7 +502,7 @@ def setUp(self):
home: mydata
processes: 4
force: true
extra:
extra:
- foo
- bar
expires: 2014-10-15
Expand Down Expand Up @@ -666,7 +666,7 @@ def setUp(self):
<string>otherdata</string>
</dict>
</plist>
""")
""")
self.simple = PListFile("simple.plist")
self.complex = PListFile("complex.plist")
self.extra = PListFile("extra.plist")
Expand Down Expand Up @@ -868,7 +868,7 @@ def test_set(self):
class TestCommandlineConfigured(TestCommandline):

supported_types = (str, int, bool, date, datetime, list)

def setUp(self):
super(TestCommandlineConfigured, self).setUp()
simp = argparse.ArgumentParser(description="This is a simple program")
Expand Down Expand Up @@ -915,7 +915,7 @@ def test_typed(self):
class TestEnvironment(unittest.TestCase, TestConfigSourceHelper):

supported_types = (str,)

simple = Environment({'MYAPP_HOME': 'mydata',
'MYAPP_PROCESSES': '4',
'MYAPP_FORCE': 'True',
Expand Down Expand Up @@ -982,7 +982,7 @@ def simple(self):
requests.put(ETCD_BASE + "/extra", data={'value': "foo, bar"})
requests.put(ETCD_BASE + "/expires", data={'value': "2014-10-15"})
requests.put(ETCD_BASE + "/lastrun", data={'value': "2014-10-15 14:32:07"})
return EtcdStore()
return EtcdStore()

@property
def complex(self):
Expand Down Expand Up @@ -1042,7 +1042,7 @@ def indexfilter(node):
cfg.mymodule.extra = ['foo', 'baz', 'quux']
# note that this will write the entire config incl cfg.extra,
# not just the values in the 'mymodule' subsection.
LayeredConfig.write(cfg.mymodule)
LayeredConfig.write(cfg.mymodule)
want = """
{
"dir": true,
Expand Down Expand Up @@ -1328,6 +1328,19 @@ def test_list(self):
self.assertEqual(set(['home', 'processes']),
set(cfg.subsection))

def test_subsection_respects_subclass(self):
defaults = {
'subsection': {
'processes': 4
}
}

class SubclassedLayeredConfig(LayeredConfig):
pass

cfg = SubclassedLayeredConfig(Defaults(defaults))
self.assertIsInstance(cfg.subsection, SubclassedLayeredConfig)


class TestLayeredSubsections(unittest.TestCase):

Expand All @@ -1344,7 +1357,7 @@ def _test_subsection(self, primary, secondary, cls):
finally:
os.unlink("primary.txt")
os.unlink("secondary.txt")

def test_layered_yaml(self):
self._test_subsection("""a:
b: b
Expand Down

0 comments on commit 8072fd5

Please sign in to comment.