Skip to content

Commit

Permalink
Added the last two configuration loaders
Browse files Browse the repository at this point in the history
  • Loading branch information
jvanstraten committed Jul 22, 2019
1 parent 856900a commit 5dfe55d
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 3 deletions.
2 changes: 2 additions & 0 deletions vhdmmio/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
from .choice import choice, flag
from .parsed import parsed
from .subconfig import subconfig, opt_subconfig, embedded, opt_embedded
from .listconfig import listconfig, protolistconfig
from .select import select
from .utils import ParseError
143 changes: 143 additions & 0 deletions vhdmmio/config/listconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Submodule for the `SubConfig` `Loader`, which can be used to
create/configure hierarchical object structures in various ways."""

from .loader import Loader
from .utils import ParseError, friendly_path

class ListConfig(Loader):
"""Loader for lists of `Configurable`s. This loader takes a single key from
its configuration dictionary, which must be a list of dictionaries. These
dictionaries are normally used to configure the target `Configurable`
classes. Alternatively, the dictionaries can again contain a list of
dictionaries with a specific name which are parsed into the `Configurable`s
instead, using the original dictionary as a prototype.
The class is constructed with a reference to its parent as its first and
only positional argument. Any keys that have been parsed before can be read
from this for contextual information."""

def __init__(self, key, doc, configurable, subkey=None):
super().__init__(key, doc)
self._configurable = configurable
self._subkey = subkey

@property
def subkey(self):
"""The prototype subkey for this loader, or `None` if this feature is
disabled."""
return self._subkey

@property
def friendly_subkey(self):
""""Friendly" version of the prototype subkey for this loader (using
dashes instead of underscores), or `None` if this feature is
disabled."""
if self._subkey is None:
return None
return self._subkey.replace('_', '-')

def markdown(self):
"""Yields markdown documentation for all the keys that this loader can
make sense of as `(key, markdown)` tuples."""
markdown = [self.doc]

if self.subkey is not None:
markdown.append(
'If the `%s` key is present in a configuration dictionary, it '
'is recursively interpreted as a list of dictionaries, similar '
'to this key, instead of being parsed directly. Any additional '
'keys in the dictionary are then used as default values for '
'the subdictionaries. For example,' % self.friendly_subkey)
markdown.append(
'```\n'
'%s:\n'
'- a: 1\n'
' b: 2\n'
' c: 3\n'
' %s:\n'
' - a: 5\n'
' - d: 4\n'
'```' % (self.friendly_key, self.friendly_subkey))
markdown.append('is equivalent to')
markdown.append(
'```\n'
'%s:\n'
'- a: 5\n'
' b: 2\n'
' c: 3\n'
'- a: 1\n'
' b: 2\n'
' c: 3\n'
' d: 4\n'
'```' % self.friendly_key)
markdown.append(
'This can be useful for specifying repetetive structures.')

markdown.append(
'This key is optional. Not specifying it is equivalent to '
'specifying an empty list.')

yield self.friendly_key, markdown

def markdown_more(self):
"""Yields or returns a list of `@configurable` classes that must be
documented in addition because the docs generated by `markdown()` refer
to them."""
yield self._configurable

def _handle_list(self, config_list, parent, path, prototype=None):
"""Handles a list of subconfigs, yielding the deserialized objects."""
if not isinstance(config_list, list):
raise ParseError('%s must be a list' % friendly_path(path))
for index, subdict in enumerate(config_list):
subpath = path + (index,)
if not isinstance(subdict, dict):
raise ParseError('%s must be a dictionary' % friendly_path(subpath))

# Merge the prototype and the dictionary.
if prototype:
updated_prototype = prototype.copy()
updated_prototype.update(subdict)
subdict = updated_prototype

# Handle the next level of prototypes.
if self.subkey is not None and self.subkey in subdict:
sublist = subdict.pop(self.subkey)
generator = self._handle_list(
sublist,
parent,
subpath + (self.friendly_subkey,),
subdict)
for item in generator:
yield item
continue

# Pass the final dict to the configurable for deserialization.
yield self._configurable.from_dict(subdict, parent)

def deserialize(self, dictionary, parent, path=()):
"""`ListConfig` deserializer. See `Loader.deserialize()` for more
info."""
return list(self._handle_list(
dictionary.pop(self.key, []),
parent,
path + (self.friendly_key,)))

def serialize(self, dictionary, value):
"""`ListConfig` serializer. See `Loader.serialize()` for more info."""
dictionary[self.friendly_key] = [item.to_dict() for item in value]


def listconfig(method):
"""Method decorator for configuring a list of `configurable`-annotated
objects. The annotated method is called with zero arguments (not even
`self`) to get the class that is to be constructed. The name of the key is
set to the name of the method, and the markdown documentation for the key
is set to the method's docstring."""
return ListConfig(method.__name__, method.__doc__, method())


def protolistconfig(method):
"""Like `@listconfig`, but supports prototypes. The prototype key is set
to the name of the method with `'sub'` prefixed to it."""
return ListConfig(method.__name__, method.__doc__, method(), 'sub' + method.__name__)
2 changes: 1 addition & 1 deletion vhdmmio/config/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def serialize(self, dictionary, value):
return
value = self.scalar_serialize(value)
if value != self.default:
dictionary[self.key] = value
dictionary[self.friendly_key] = value

@staticmethod
def scalar_serialize(value):
Expand Down
71 changes: 71 additions & 0 deletions vhdmmio/config/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Submodule for the `Select` `Loader`. This loader works like a `SubConfig`,
but uses an additional key in the dictionary to select which configurable to
instantiate."""

import textwrap
from collections import OrderedDict
from .loader import Loader
from .utils import ParseError

class Select(Loader):
"""Loader for embedding `Configurable`s, with multiple different kinds of
possible `Configurable` classes, based on an additional selection key.
The class is constructed with a reference to its parent as its first
and only positional argument. Any keys that have been parsed before can be
read from this for contextual information."""

def __init__(self, key, doc, config_options):
super().__init__(key, doc)
self._config_options = config_options

def markdown(self):
"""Yields markdown documentation for all the keys that this loader can
make sense of as `(key, markdown)` tuples."""
markdown = [self.doc]

markdown.append('This key can take the following values:')

for value, (_, doc) in self._config_options.items():
markdown.append(' - `%s`: %s' % (
value, textwrap.dedent(doc).replace('\n', '\n ')))

yield self.friendly_key, markdown

def markdown_more(self):
"""Yields or returns a list of `@configurable` classes that must be
documented in addition because the docs generated by `markdown()` refer
to them."""
for configurable, _ in self._config_options:
yield configurable

def deserialize(self, dictionary, parent, path=()):
"""`Select` deserializer. See `Loader.deserialize()` for more
info."""
selection = self.pop_dict(dictionary, self.key, path)
configurable, _ = self._config_options.get(selection, (None, None))
if configurable is None:
raise ParseError('%s has unknown value `%r`' % (
self.friendly_path(path), selection))
return selection, configurable.from_dict(dictionary, parent)

def serialize(self, dictionary, value):
"""`Select` serializer. See `Loader.serialize()` for more info."""
selection, item = value
dictionary[self.friendly_key] = selection
item.to_dict(dictionary)


def select(method):
"""Method decorator for configuring a `configurable`-annotated class
selected through another configuration value. The annotated method is
called with zero arguments (not even `self`) to generate/yield
`(value, configurable, doc)` three-tuples, where `value` is the unique
value that the user must specify to select `configurable`, and `doc` is the
documentation associated with this option. The method is transformed to a
property that allows the object type and the constructed configurable
instance to be accessed as a `(selection, instance)` two-tuple."""
return Select(
method.__name__, method.__doc__, OrderedDict((
(value, (configurable, doc))
for value, configurable, doc in method())))
4 changes: 2 additions & 2 deletions vhdmmio/config/subconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,14 @@ def deserialize(self, dictionary, parent, path=()):
if not subdict and self._optional:
return None

return self._configurable.from_dict(subdict, path)
return self._configurable.from_dict(subdict, parent)

def serialize(self, dictionary, value):
"""`SubConfig` serializer. See `Loader.serialize()` for more info."""
if value is None:
return
if self._style is True:
dictionary[self.key] = value.to_dict()
dictionary[self.friendly_key] = value.to_dict()
else:
prefix = '%s-' % self._style.replace('_', '-') if self._style else ''
subdict = value.to_dict()
Expand Down

0 comments on commit 5dfe55d

Please # to comment.