-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added the last two configuration loaders
- Loading branch information
1 parent
856900a
commit 5dfe55d
Showing
5 changed files
with
219 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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__) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters