diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f1a4ea1..26baa63 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.16.0 +current_version = 1.17.0 commit = true tag = false diff --git a/configmanager/__init__.py b/configmanager/__init__.py index 93a19e1..b4ae1d2 100644 --- a/configmanager/__init__.py +++ b/configmanager/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.16.0' +__version__ = '1.17.0' from .managers import Config from .items import Item diff --git a/configmanager/config_declaration_parser.py b/configmanager/config_declaration_parser.py index 593ff38..ea8b3a6 100644 --- a/configmanager/config_declaration_parser.py +++ b/configmanager/config_declaration_parser.py @@ -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: diff --git a/configmanager/hooks.py b/configmanager/hooks.py index b9e99ab..e4a5512 100644 --- a/configmanager/hooks.py +++ b/configmanager/hooks.py @@ -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' @@ -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) diff --git a/configmanager/managers.py b/configmanager/managers.py index 76d1317..acf16ae 100644 --- a/configmanager/managers.py +++ b/configmanager/managers.py @@ -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 diff --git a/configmanager/meta.py b/configmanager/meta.py index 34ad2c6..13ae7e6 100644 --- a/configmanager/meta.py +++ b/configmanager/meta.py @@ -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(): @@ -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 diff --git a/configmanager/sections.py b/configmanager/sections.py index cdd211d..8d38fb6 100644 --- a/configmanager/sections.py +++ b/configmanager/sections.py @@ -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 @@ -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) @@ -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) @@ -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 diff --git a/tests/test_config.py b/tests/test_config.py index 3ce041d..cd49362 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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, @@ -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 diff --git a/tests/test_extending_library.py b/tests/test_extending_library.py index e918481..4eaa896 100644 --- a/tests/test_extending_library.py +++ b/tests/test_extending_library.py @@ -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) diff --git a/tests/test_section.py b/tests/test_section.py new file mode 100644 index 0000000..489cd9c --- /dev/null +++ b/tests/test_section.py @@ -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