Skip to content

Commit

Permalink
Merge pull request #133 from arroyoj/sequence_fix_error_add_docs
Browse files Browse the repository at this point in the history
Sequence template updates: fix AttributeError and add examples
  • Loading branch information
sampsyo committed May 19, 2021
2 parents 200203f + 56a94d6 commit 8b69242
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 12 deletions.
41 changes: 30 additions & 11 deletions confuse/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,26 +106,25 @@ def __iter__(self):
"""Iterate over the keys of a dictionary view or the *subviews*
of a list view.
"""
# Try getting the keys, if this is a dictionary view.
# Try iterating over the keys, if this is a dictionary view.
try:
keys = self.keys()
for key in keys:
for key in self.keys():
yield key

except ConfigTypeError:
# Otherwise, try iterating over a list.
collection = self.get()
if not isinstance(collection, (list, tuple)):
# Otherwise, try iterating over a list view.
try:
for subview in self.sequence():
yield subview

except ConfigTypeError:
item, _ = self.first()
raise ConfigTypeError(
u'{0} must be a dictionary or a list, not {1}'.format(
self.name, type(collection).__name__
self.name, type(item).__name__
)
)

# Yield all the indices in the list.
for index in range(len(collection)):
yield self[index]

def __getitem__(self, key):
"""Get a subview of this view."""
return Subview(self, key)
Expand Down Expand Up @@ -295,6 +294,26 @@ def values(self):

# List/sequence emulation.

def sequence(self):
"""Iterates over the subviews contained in lists from the *first*
source at this view. If the object for this view in the first source
is not a list or tuple, then a `ConfigTypeError` is raised.
"""
try:
collection, _ = self.first()
except NotFoundError:
return
if not isinstance(collection, (list, tuple)):
raise ConfigTypeError(
u'{0} must be a list, not {1}'.format(
self.name, type(collection).__name__
)
)

# Yield all the indices in the sequence.
for index in range(len(collection)):
yield self[index]

def all_contents(self):
"""Iterates over all subviews from collections at this view from
*all* sources. If the object for this view in any source is not
Expand Down
2 changes: 1 addition & 1 deletion confuse/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def value(self, view, template=None):
"""Get a list of items validated against the template.
"""
out = []
for item in view:
for item in view.sequence():
out.append(self.subtemplate.value(item, self))
return out

Expand Down
85 changes: 85 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,91 @@ These examples demonstrate how the confuse templates work to validate
configuration values.


Sequence
--------

A ``Sequence`` template allows validation of a sequence of configuration items
that all must match a subtemplate. The items in the sequence can be simple
values or more complex objects, as defined by the subtemplate. When the view
is defined in multiple sources, the highest priority source will override the
entire list of items, rather than appending new items to the list from lower
sources. If the view is not defined in any source of the configuration, an
empty list will be returned.

As an example of using the ``Sequence`` template, consider a configuration that
includes a list of servers, where each server is required to have a host string
and an optional port number that defaults to 80. For this example, an initial
configuration file named ``servers_example.yaml`` has the following contents:

.. code-block:: yaml
servers:
- host: one.example.com
- host: two.example.com
port: 8000
- host: three.example.com
port: 8080
Validation of this configuration could be performed like this:

>>> import confuse
>>> import pprint
>>> source = confuse.YamlSource('servers_example.yaml')
>>> config = confuse.RootView([source])
>>> template = {
... 'servers': confuse.Sequence({
... 'host': str,
... 'port': 80,
... }),
... }
>>> valid_config = config.get(template)
>>> pprint.pprint(valid_config)
{'servers': [{'host': 'one.example.com', 'port': 80},
{'host': 'two.example.com', 'port': 8000},
{'host': 'three.example.com', 'port': 8080}]}

The list of items in the initial configuration can be overridden by setting a
higher priority source. Continuing the previous example:

>>> config.set({
... 'servers': [
... {'host': 'four.example.org'},
... {'host': 'five.example.org', 'port': 9000},
... ],
... })
>>> updated_config = config.get(template)
>>> pprint.pprint(updated_config)
{'servers': [{'host': 'four.example.org', 'port': 80},
{'host': 'five.example.org', 'port': 9000}]}

If the requested view is missing, ``Sequence`` returns an empty list:

>>> config.clear()
>>> config.get(template)
{'servers': []}

However, if an item within the sequence does not match the subtemplate
provided to ``Sequence``, then an error will be raised:

>>> config.set({
... 'servers': [
... {'host': 'bad_port.example.net', 'port': 'default'}
... ]
... })
>>> try:
... config.get(template)
... except confuse.ConfigError as err:
... print(err)
...
servers#0.port: must be a number

.. note::
A python list is not the shortcut for defining a ``Sequence`` template but
will instead produce a ``OneOf`` template. For example,
``config.get([str])`` is equivalent to ``config.get(confuse.OneOf([str]))``
and *not* ``config.get(confuse.Sequence(str))``.


MappingValues
-------------

Expand Down
10 changes: 10 additions & 0 deletions test/test_valid.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,16 @@ def test_invalid_item(self):
{'bar': int, 'baz': int}
))

def test_wrong_type(self):
config = _root({'foo': {'one': 1, 'two': 2, 'three': 3}})
with self.assertRaises(confuse.ConfigTypeError):
config['foo'].get(confuse.Sequence(int))

def test_missing(self):
config = _root({'foo': [1, 2, 3]})
valid = config['bar'].get(confuse.Sequence(int))
self.assertEqual(valid, [])


class MappingValuesTest(unittest.TestCase):
def test_int_dict(self):
Expand Down
35 changes: 35 additions & 0 deletions test/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ def test_missing_index(self):
with self.assertRaises(confuse.NotFoundError):
config['l'][5].get()

def test_dict_iter(self):
config = _root({'foo': 'bar', 'baz': 'qux'})
keys = [key for key in config]
self.assertEqual(set(keys), set(['foo', 'baz']))

def test_list_iter(self):
config = _root({'l': ['foo', 'bar']})
items = [subview.get() for subview in config['l']]
self.assertEqual(items, ['foo', 'bar'])

def test_int_iter(self):
config = _root({'n': 2})
with self.assertRaises(confuse.ConfigTypeError):
[item for item in config['n']]

def test_dict_keys(self):
config = _root({'foo': 'bar', 'baz': 'qux'})
keys = config.keys()
Expand All @@ -51,6 +66,16 @@ def test_list_keys_error(self):
with self.assertRaises(confuse.ConfigTypeError):
config['l'].keys()

def test_list_sequence(self):
config = _root({'l': ['foo', 'bar']})
items = [item.get() for item in config['l'].sequence()]
self.assertEqual(items, ['foo', 'bar'])

def test_dict_sequence_error(self):
config = _root({'foo': 'bar', 'baz': 'qux'})
with self.assertRaises(confuse.ConfigTypeError):
list(config.sequence())

def test_dict_contents(self):
config = _root({'foo': 'bar', 'baz': 'qux'})
contents = config.all_contents()
Expand Down Expand Up @@ -182,6 +207,16 @@ def test_dict_items_replaced(self):
items = [(key, value.get()) for (key, value) in config['foo'].items()]
self.assertEqual(list(items), [('bar', 'baz')])

def test_list_sequence_shadowed(self):
config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']})
items = [item.get() for item in config['l'].sequence()]
self.assertEqual(items, ['a', 'b'])

def test_list_sequence_shadowed_by_dict(self):
config = _root({'foo': {'bar': 'baz'}}, {'foo': ['qux', 'fred']})
with self.assertRaises(confuse.ConfigTypeError):
list(config['foo'].sequence())

def test_dict_contents_concatenated(self):
config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}})
contents = config['foo'].all_contents()
Expand Down

0 comments on commit 8b69242

Please sign in to comment.