Skip to content
This repository has been archived by the owner on Feb 11, 2023. It is now read-only.

Commit

Permalink
Merge pull request #147 from jbasko/config-of-configs
Browse files Browse the repository at this point in the history
Allow sections to be created on their own
  • Loading branch information
jbasko authored Jun 11, 2017
2 parents 351642c + 8b4e132 commit dc14390
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.16.0
current_version = 1.17.0
commit = true
tag = false

Expand Down
2 changes: 1 addition & 1 deletion configmanager/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '1.16.0'
__version__ = '1.17.0'

from .managers import Config
from .items import Item
Expand Down
2 changes: 2 additions & 0 deletions configmanager/config_declaration_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ def parse_config_declaration(declaration, parent_section=None, root=None):
meta['default'] = dict(clean_declaration)
return parent_section.create_item(**meta)

# If root is specified it means we are parsing declaration for the root,
# so no need to create a new section.
section = root or parent_section.create_section()

for k, v in clean_declaration:
Expand Down
6 changes: 5 additions & 1 deletion configmanager/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


class Hooks(object):

NOT_FOUND = 'not_found'
ITEM_ADDED_TO_SECTION = 'item_added_to_section'
SECTION_ADDED_TO_SECTION = 'section_added_to_section'
Expand Down Expand Up @@ -40,6 +39,11 @@ def __getattr__(self, name):
))

def handle(self, hook_name, *args, **kwargs):

# If hooks are disabled in a high-level Config, and enabled
# in a low-level Config, they will still be handled within
# the low-level Config's "jurisdiction".

if self._section._settings.hooks_enabled:
for handler in self._registry[hook_name]:
result = handler(*args, **kwargs)
Expand Down
2 changes: 0 additions & 2 deletions configmanager/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ def __init__(self, config_declaration=None, **configmanager_settings):

super(Config, self).__init__(configmanager_settings=configmanager_settings)

self._manager = self

self._configparser_adapter = None
self._json_adapter = None
self._yaml_adapter = None
Expand Down
6 changes: 3 additions & 3 deletions configmanager/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ def __init__(self, **settings_and_factories):
# Use _settings when you want to initialise defaults for all Config instances.
# Use _factories when you want to lazy-load defaults only when requested.
self._settings = {
'item_cls': Item,
'item_factory': Item,
'app_name': None,
'hooks_enabled': None, # None means that when a hook is registered, hooks will be enabled automatically
'str_path_separator': '.',
}
self._factories = {
'configparser_factory': self.create_configparser_factory,
'section_cls': self.create_section_cls,
'section_factory': self.create_section_factory,
}

for k, v in settings_and_factories.items():
Expand Down Expand Up @@ -49,7 +49,7 @@ def create_configparser_factory(self):
import configparser
return configparser.ConfigParser

def create_section_cls(self):
def create_section_factory(self):
from .sections import Section
return Section

Expand Down
65 changes: 35 additions & 30 deletions configmanager/sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import six

from configmanager.config_declaration_parser import parse_config_declaration
from .hooks import Hooks
from .meta import ConfigManagerSettings
from .exceptions import NotFound
Expand All @@ -28,32 +29,27 @@ class Section(BaseSection):
# Core section functionality.
# Keep as light as possible.

def __init__(self, configmanager_settings=None):

# It is Config's responsibility to initialise configmanager_settings.
if configmanager_settings is None:
configmanager_settings = ConfigManagerSettings()
elif isinstance(configmanager_settings, dict):
def __init__(self, declaration=None, section=None, configmanager_settings=None):
#: local settings which are used only until we have settings available from manager
self._local_settings = configmanager_settings or ConfigManagerSettings()
if not isinstance(self._local_settings, ConfigManagerSettings):
raise ValueError('configmanager_settings should be either None or an instance of ConfigManagerSettings')

#: configmanager settings
self._settings = configmanager_settings

#: Actual contents of the section
self._tree = collections.OrderedDict()

#: Section to which this section belongs (if any at all)
self._section = None
self._section = section

#: Alias of this section with which it was added to its parent section
self._section_alias = None

#: :class:`.Config` which manages this section
self._manager = None

#: Hooks registry
self._hooks = Hooks(self)

if declaration is not None:
parse_config_declaration(declaration, root=self)

def __len__(self):
return len(self._tree)

Expand Down Expand Up @@ -205,11 +201,6 @@ def add_section(self, alias, section):

section._section = self
section._section_alias = alias
section._manager = self._manager

# Must not mess around with settings of other Config instances.
if not section.is_config:
section._settings = self._settings

self.hooks.handle(Hooks.SECTION_ADDED_TO_SECTION, alias=alias, section=self, subject=section)

Expand Down Expand Up @@ -463,24 +454,38 @@ def load_values(self, dictionary, as_defaults=False, flat=False, separator=not_s
def create_item(self, *args, **kwargs):
"""
Internal factory method used to create an instance of configuration item.
Should only be used to extend configmanager's functionality.
Should only be used when extending or modifying configmanager's functionality.
Under normal circumstances you should let configmanager create sections
and items when parsing configuration declarations.
Do not override this method. To customise item creation,
write your own item factory and pass it to Config through
item_factory= keyword argument.
"""
return self._settings.item_cls(*args, **kwargs)
return self._settings.item_factory(*args, **kwargs)

def create_section(self, *args, **kwargs):
"""
Internal factory method used to create an instance of configuration section.
Should only be used to extend configmanager's functionality.
Should only be used when extending or modifying configmanager's functionality.
Under normal circumstances you should let configmanager create sections
and items when parsing configuration declarations.
Do not override this method. To customise section creation,
write your own section factory and pass it to Config through
section_factory= keyword argument.
"""
kwargs.setdefault('configmanager_settings', self._settings)
return self._settings.section_cls(*args, **kwargs)
kwargs.setdefault('section', self)
return self._settings.section_factory(*args, **kwargs)

@property
def _root_manager(self):
# TODO Maybe this isn't needed really?
if self._manager is not None:
if self._manager is self:
return self
else:
return self._manager._root_manager
return None
def _settings(self):
if self.is_config:
return self._local_settings
elif self._section:
return self._section._settings
else:
return self._local_settings
66 changes: 42 additions & 24 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,23 +535,6 @@ def test_config_accepts_and_respects_str_path_separator_setting(simple_config):
}


def test_section_holds_reference_to_the_manager_to_which_it_belongs():
config1 = Config()
assert config1._manager is config1
assert config1._root_manager is config1

config2 = Config({
'uploads': Section(),
})

assert config2.uploads._manager is config2
assert config2.uploads._root_manager is config2

config2.add_section('downloads', Section())
assert config2.downloads._manager is config2
assert config2.downloads._root_manager is config2


def test_config_of_configs():
uploads = Config({
'threads': 1,
Expand All @@ -576,12 +559,47 @@ def test_config_of_configs():
}
})

assert config.messages._manager is config
assert config.messages._root_manager is config
assert config.uploads.threads.is_item
assert config.uploads.api.port.is_item
assert config.downloads.db.user.is_item
assert config.messages.greeting.is_item


def test_nested_section_settings_always_point_to_the_settings_of_the_topmost_section_or_first_manager():
s01 = Section()
s02 = Section()

s01_settings = s01._settings
s02_settings = s02._settings

assert config.uploads._manager is config
assert config.uploads._root_manager is config
assert config.uploads.section is config
assert s01_settings is not s02_settings

s11 = Section({
's01': s01,
's02': s02,
})

s11_settings = s11._settings
assert s11_settings is s01._settings
assert s11_settings is s02._settings
assert s01._settings is s02._settings
assert s01._settings is not s01_settings

c20 = Config({
's11': s11
})

c20_settings = c20._settings
assert c20_settings is s01._settings
assert c20_settings is s02._settings
assert c20_settings is s11._settings
assert s01._settings is s02._settings
assert s01._settings is s11._settings
assert s11_settings is not s11._settings

c30 = Config({
'c20': c20
})

assert config.uploads.api._manager is config.uploads
assert config.uploads.api._root_manager is config
# Settings don't cross Config boundaries
assert c30._settings is not c20._settings
2 changes: 1 addition & 1 deletion tests/test_extending_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class CustomItem(Item):
},
'd': 'e',
'f': Item(default='this will not be converted'),
}, item_cls=CustomItem)
}, item_factory=CustomItem)

assert isinstance(config.a.b, CustomItem)
assert isinstance(config.d, CustomItem)
Expand Down
45 changes: 45 additions & 0 deletions tests/test_section.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Most section tests are in test_config.
Here are tests just to test Section usage outside of Configs.
"""
from configmanager import Section


def test_section_created_from_declaration():
uploads = Section({
'enabled': True,
'threads': 1,
})

assert uploads.is_section

assert uploads.enabled.is_item
assert uploads.enabled.value is True

assert uploads.threads.is_item
assert uploads.threads.value == 1


def test_nested_section_created_from_declaration():
config = Section({
'uploads': Section({
'db': Section({
'user': 'root'
})
})
})

assert config.uploads.db._settings is config._settings
assert config.uploads._settings is config._settings

calls = []

@config.hooks.item_value_changed
def value_changed(**kwargs):
calls.append(1)

assert len(calls) == 0

config.uploads.db.user.value = 'admin'

assert len(calls) == 1

0 comments on commit dc14390

Please sign in to comment.