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

Commit

Permalink
Configuration declaration parser rewritten:
Browse files Browse the repository at this point in the history
* empty list is interpreted as a default value of list type item
* empty dictionary is interpreted as a default value of a dict type item
* list of anything but tuples is treated as a default value of list type item
* no more class-based configurations
* no more free-floating Item() instances in lists used for section declaration -- must be a tuple.
  • Loading branch information
jbasko committed Jun 10, 2017
1 parent 2e7ed20 commit 7dd091d
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 308 deletions.
2 changes: 2 additions & 0 deletions configmanager/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
class BaseItem(object):
is_item = True
is_section = False
is_config = False


def is_config_item(obj):
Expand All @@ -18,6 +19,7 @@ class BaseSection(object):

is_item = False
is_section = True
is_config = False

def add_item(self, alias, item):
raise NotImplementedError()
Expand Down
96 changes: 96 additions & 0 deletions configmanager/config_declaration_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import collections
import inspect

import six

from configmanager.base import BaseItem, BaseSection


def parse_config_declaration(declaration, parent_section=None, root=None):

# Pass _configmanager_settings through so we know how to create new items/sections
# Process declaration recursively (unlike the old parser)
# Don't allow much freedom into what is accepted
# Sequences (excluding strings) and mappings (excluding our base types) have special meaning and
# cannot be used as default values unless wrapped inside Item.

if root:
parent_section = root

is_valid_config_root_declaration = (
inspect.ismodule(declaration)
or
(
isinstance(declaration, collections.Sequence)
and len(declaration) > 0
and isinstance(declaration[0], tuple)
)
or
(
isinstance(declaration, collections.Mapping)
and len(declaration) > 0
)
)
if not is_valid_config_root_declaration:
raise ValueError(
'Config root declaration has to be a module, a non-empty sequence, or non-empty mapping, '
'got a {}'.format(type(declaration)),
)

if isinstance(declaration, (BaseItem, BaseSection)):
# Do not parse existing objects of our hierarchy
# TODO However, we should probably process raw sections!
# TODO Also, how would it be possible for user to legally create a new Section?
return declaration

elif inspect.ismodule(declaration):
return parse_config_declaration(declaration.__dict__, parent_section=parent_section, root=root)

elif isinstance(declaration, collections.Mapping):

if len(declaration) == 0:
# Empty dictionary means an empty item
return parent_section.create_item(default=declaration)

# Create a list of tuples so we can use the standard declaration parser below
return parse_config_declaration([x for x in declaration.items()], parent_section=parent_section, root=root)

if isinstance(declaration, collections.Sequence) and not isinstance(declaration, six.string_types):

if len(declaration) == 0 or not isinstance(declaration[0], tuple):
# Declaration of an item
return parent_section.create_item(default=declaration)

# Pre-process all keys and discard private parts and separate out meta parts
clean_declaration = []
meta = {}

for k, v in declaration:
if k.startswith('_'):
continue
elif k.startswith('@'):
meta[k[1:]] = v
continue
clean_declaration.append((k, v))

if not clean_declaration or meta.get('type'):
# Must be an item
if not clean_declaration:
return parent_section.create_item(**meta)
else:
meta['default'] = dict(clean_declaration)
return parent_section.create_item(**meta)

section = root or parent_section.create_section()

for k, v in clean_declaration:
obj = parse_config_declaration(v, parent_section=section)
if obj.is_section:
section.add_section(k, obj)
else:
section.add_item(k, obj)

return section

# Declaration of an item
return parent_section.create_item(default=declaration)
10 changes: 5 additions & 5 deletions configmanager/managers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .config_declaration_parser import parse_config_declaration
from .meta import ConfigManagerSettings
from .parsers import ConfigDeclarationParser
from .persistence import ConfigPersistenceAdapter, YamlReaderWriter, JsonReaderWriter, ConfigParserReaderWriter
from .sections import Section

Expand Down Expand Up @@ -91,6 +91,8 @@ class Config(Section):
"""

is_config = True

def __init__(self, config_declaration=None, **configmanager_settings):
if 'configmanager_settings' in configmanager_settings:
if len(configmanager_settings) > 1:
Expand All @@ -107,10 +109,8 @@ def __init__(self, config_declaration=None, **configmanager_settings):
self._configmanager_yaml_adapter = None
self._configmanager_click_extension = None

self._configmanager_process_config_declaration = ConfigDeclarationParser(section=self)

if config_declaration:
self._configmanager_process_config_declaration(config_declaration)
if config_declaration is not None:
parse_config_declaration(config_declaration, root=self)

self.load_user_app_config()

Expand Down
108 changes: 0 additions & 108 deletions configmanager/parsers.py

This file was deleted.

3 changes: 3 additions & 0 deletions configmanager/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ def _load_config_from_config_parser(self, config, cp, as_defaults=False):

# TODO Clean up the repetition here!

# TODO Shouldn't really use create_item and create_section methods here,
# TODO should use load_values(..., as_defaults=True) instead!

for option, value in cp.defaults().items():
if as_defaults:
if option not in config:
Expand Down
4 changes: 4 additions & 0 deletions configmanager/sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ def add_section(self, alias, section):
section._configmanager_section = self
section._configmanager_section_alias = alias

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

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

def _get_str_path_separator(self, override=not_set):
Expand Down
40 changes: 0 additions & 40 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,6 @@
from configmanager import Config, Section, Item, Types, NotFound


def test_items_are_created_using_create_item_method():
class CustomItem(Item):
pass

class CustomConfig(Config):
def create_item(self, *args, **kwargs):
return CustomItem(*args, **kwargs)

config = CustomConfig({
'a': {'b': {'c': {'d': 1, 'e': '2', 'f': True}}},
'g': False,
})

assert isinstance(config.g, CustomItem)
assert isinstance(config.a.b.c.d, CustomItem)


def test_config_create_section_creates_instance_of_section_class():
class ExtendedConfig(Config):
pass

config = ExtendedConfig({
'uploads': {
'db': {
'user': 'root'
},
'api': {
'port': 8000,
'extensions': {},
},
}
})

assert isinstance(config, ExtendedConfig)
assert isinstance(config.uploads, Section)
assert isinstance(config.uploads.db, Section)
assert isinstance(config.uploads.db.user, Item)
assert isinstance(config.uploads.api.extensions, Section)


def test_reset_resets_values_to_defaults():
config = Config({
'x': Item(type=int),
Expand Down
Loading

0 comments on commit 7dd091d

Please sign in to comment.