Yaml configuration system refactoring.

* Refactoring YamlInterpolator -> YamlRefLoader
  * Add new exception types with formating errros, specified for cfg
  * Add documentation in class docstrings
  * Add unit tests for cfg.YamlRefLoader and exceptions.YamlException
  * Extend reference links possibilities (concatenating, etc.)
  * Add refence loop control
  * Reverting to simpler reflinks using I. Melnikov's draft (thanks to him).

Change-Id: I565085ae9a3015d005dcc532a74aaca49f41277a
This commit is contained in:
Vladimir Novikov 2013-10-18 20:31:34 +03:00
parent 552e072aa5
commit 2051e05d05
5 changed files with 770 additions and 126 deletions

View File

@ -56,9 +56,9 @@ class Action(object):
self.root_dir = root_dir self.root_dir = root_dir
# Action phases are tracked in this directory # Action phases are tracked in this directory
self.phase_dir = sh.joinpths(root_dir, 'phases') self.phase_dir = sh.joinpths(root_dir, 'phases')
# Yamls are 'interpolated' using this instance at the given # Yamls are loaded (with its reference links) using this instance at the
# component directory where component configuration will be found... # given component directory where component configuration will be found.
self.interpolator = cfg.YamlInterpolator(settings.COMPONENT_CONF_DIR) self.config_loader = cfg.YamlRefLoader(settings.COMPONENT_CONF_DIR)
# Keyring/pw settings + cache # Keyring/pw settings + cache
self.passwords = {} self.passwords = {}
self.keyring_path = cli_opts.pop('keyring_path') self.keyring_path = cli_opts.pop('keyring_path')
@ -173,7 +173,7 @@ class Action(object):
def _get_interpolated_options(self, name): def _get_interpolated_options(self, name):
opts = {} opts = {}
for c in self._get_interpolated_names(name): for c in self._get_interpolated_names(name):
opts.update(self.interpolator.extract(c)) opts.update(self.config_loader.load(c))
return opts return opts
def _construct_instances(self, persona): def _construct_instances(self, persona):

View File

@ -18,7 +18,6 @@
import ConfigParser import ConfigParser
from ConfigParser import NoOptionError from ConfigParser import NoOptionError
from ConfigParser import NoSectionError from ConfigParser import NoSectionError
import re import re
from StringIO import StringIO from StringIO import StringIO
@ -26,11 +25,11 @@ from StringIO import StringIO
# This one keeps comments but has some weirdness with it # This one keeps comments but has some weirdness with it
import iniparse import iniparse
from anvil import exceptions
from anvil import log as logging from anvil import log as logging
from anvil import shell as sh from anvil import shell as sh
from anvil import utils from anvil import utils
INTERP_PAT = r"\s*\$\(([\w\d-]+):([\w\d-]+)\)\s*"
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -164,141 +163,200 @@ class DefaultConf(object):
self.backing.remove_option(section, key) self.backing.remove_option(section, key)
class YamlInterpolator(object): # TODO(vnovikov): inject all config merges into class below
def __init__(self, base): #class YamlMergeLoader(object):
self.included = {} #
self.interpolated = {} # def __init__(self, path):
self.base = base # self._merge_order = ('general',)
self.auto_specials = { # self._base_loader = YamlRefLoader(path)
'ip': utils.get_host_ip, #
'home': sh.gethomedir, # def load(self, distro, component, persona, cli):
'hostname': sh.hostname, #
# distro_opts = distro.options
# general_component_opts = self._base_loader.load('general')
# component_specific_opts = self._base_loader.load(component)
# persona_component_opts = persona.component_options.get(component, {})
# persona_global_opts = persona.component_options.get('global', {})
# cli_opts = cli
#
# merged_opts = utils.merge_dicts(
# distro_opts,
# general_component_opts,
# component_specific_opts,
# persona_component_opts,
# persona_global_opts,
# cli_opts,
# )
#
# return merged_opts
class YamlRefLoader(object):
"""Reference loader for *.yaml configs.
Holds usual safe loading of the *.yaml files, caching, resolving and getting
all reference links and transforming all data to python built-in types.
Let's describe some basics.
In this context reference means value which formatted just like:
opt: "$(source:option)" , or
opt: "some-additional-data-$(source:option)-some-postfix-data", where:
opt - base option name
source - other source config (i.e. other *.yaml file) from which we
should get 'option'
option - option name in 'source'
In other words it means that loader will try to find and read 'option' from
'source'.
Any source config also allows:
References to itself via it's name (opt: "$(source:opt)",
in file - source.yaml)
References to auto parameters (opt: $(auto:ip), will insert current ip).
'auto' allows next options: 'ip', 'hostname' and 'home'
Implicit and multi references just like
s.yaml => opt: "here 3 opts: $(source:opt), $(source2:opt) and $(auto:ip)".
Exception cases:
* if reference 'option' does not exist than YamlOptionException is raised
* if config 'source' does not exist than YamlConfException is raised
* if reference loop found than YamlLoopException is raised
Config file example:
(file sample.yaml)
reference: "$(source:option)"
ip: "$(auto:ip)"
self_ref: "$(sample:ip)" # this will equal ip option.
opt: "http://$(auto:ip)/"
"""
def __init__(self, path):
self._conf_ext = '.yaml'
self._ref_pattern = re.compile(r"\$\(([\w\d-]+)\:([\w\d-]+)\)")
self._predefined_refs = {
'auto': {
'ip': utils.get_host_ip,
'home': sh.gethomedir,
'hostname': sh.hostname,
}
} }
self._path = path # path to root directory with configs
self._cached = {} # buffer to save already loaded configs
self._processed = {} # buffer to save already processed configs
self._ref_stack = [] # stack for controlling reference loop
def _interpolate_iterable(self, what): def _process_string(self, value):
if isinstance(what, (set)): """Processing string (and reference links) values via regexp."""
n_what = set() processed = value
for v in what:
n_what.add(self._interpolate(v))
return n_what
else:
n_what = []
for v in what:
n_what.append(self._interpolate(v))
if isinstance(what, (tuple)):
n_what = tuple(n_what)
return n_what
def _interpolate_dictionary(self, what): # Process each reference in value (one by one)
n_what = {} for match in self._ref_pattern.finditer(value):
for (k, v) in what.iteritems(): ref_conf, ref_opt = match.groups()
n_what[k] = self._interpolate(v) val = self._load_option(ref_conf, ref_opt)
return n_what
def _include_dictionary(self, what): if match.group(0) == value:
n_what = {} return val
for (k, value) in what.iteritems(): else:
n_what[k] = self._do_include(value) processed = re.sub(self._ref_pattern, str(val), processed, count=1)
return n_what return processed
def _include_iterable(self, what): def _process_dict(self, value):
if isinstance(what, (set)): """Process dictionary values."""
n_what = set() processed = utils.OrderedDict()
for v in what: for opt, val in sorted(value.items()):
n_what.add(self._do_include(v)) res = self._process(val)
return n_what processed[opt] = res
else:
n_what = []
for v in what:
n_what.append(self._do_include(v))
if isinstance(what, (tuple)):
n_what = tuple(n_what)
return n_what
def _interpolate(self, value): return processed
new_value = value
if value and isinstance(value, (basestring, str)): def _process_iterable(self, value):
new_value = self._interpolate_string(value) """Process list, set or tuple values."""
elif isinstance(value, (dict)): processed = []
new_value = self._interpolate_dictionary(value) for item in value:
processed.append(self._process(item))
return processed
def _process_asis(self, value):
"""Process built-in values."""
return value
def _process(self, value):
"""Base recursive method for processing references."""
if isinstance(value, basestring):
processed = self._process_string(value)
elif isinstance(value, dict):
processed = self._process_dict(value)
elif isinstance(value, (list, set, tuple)): elif isinstance(value, (list, set, tuple)):
new_value = self._interpolate_iterable(value) processed = self._process_iterable(value)
return new_value else:
processed = self._process_asis(value)
def _interpolate_string(self, what): return processed
if not re.search(INTERP_PAT, what):
# Leave it alone if the sub won't do
# anything to begin with
return what
def replacer(match): def _cache(self, conf):
who = match.group(1).strip() """Cache config file into memory to avoid re-reading it from disk."""
key = match.group(2).strip() if conf not in self._cached:
(is_special, special_value) = self._process_special(who, key) path = sh.joinpths(self._path, conf + self._conf_ext)
if is_special: if not sh.isfile(path):
return special_value raise exceptions.YamlConfigNotFoundException(path)
if who not in self.interpolated:
self.interpolated[who] = self.included[who]
self.interpolated[who] = self._interpolate(self.included[who])
return str(self.interpolated[who][key])
return re.sub(INTERP_PAT, replacer, what) # TODO(vnovikov): may be it makes sense to reintroduce load_yaml
# for returning OrderedDict with the same order as options placement
# in source yaml file...
self._cached[conf] = utils.load_yaml(path) or {}
def _process_special(self, who, key): def _precache(self):
if who and who.lower() in ['auto']: """Cache and process predefined auto-references"""
if key not in self.auto_specials: for conf, options in self._predefined_refs.items():
raise KeyError("Unknown auto key %r" % (key)) if conf not in self._processed:
functor = self.auto_specials[key] processed = dict((option, functor())
return (True, functor()) for option, functor in options.items())
return (False, None) self._cached[conf] = processed
self._processed[conf] = processed
def _include_string(self, what): def _load_option(self, conf, opt):
if not re.search(INTERP_PAT, what): try:
# Leave it alone if the sub won't do return self._processed[conf][opt]
# anything to begin with except KeyError:
return what if (conf, opt) in self._ref_stack:
raise exceptions.YamlLoopException(conf, opt, self._ref_stack)
self._ref_stack.append((conf, opt))
def replacer(match): self._cache(conf)
who = match.group(1).strip() try:
key = match.group(2).strip() raw_value = self._cached[conf][opt]
(is_special, special_value) = self._process_special(who, key) except KeyError:
if is_special: try:
return special_value cur_conf, cur_opt = self._ref_stack[-1]
# Process there includes and then except IndexError:
# fetch the value that should have been cur_conf, cur_opt = None, None
# populated raise exceptions.YamlOptionNotFoundException(
self._process_includes(who) cur_conf, cur_opt, conf, opt
return str(self.included[who][key]) )
result = self._process(raw_value)
self._processed.setdefault(conf, {})[opt] = result
return re.sub(INTERP_PAT, replacer, what) self._ref_stack.pop()
return result
def _do_include(self, value): def load(self, conf):
new_value = value """Load config `conf` from same yaml file with and resolve all
if value and isinstance(value, (basestring, str)): references.
new_value = self._include_string(value) """
elif isinstance(value, (dict)): self._precache()
new_value = self._include_dictionary(value) self._cache(conf)
elif isinstance(value, (list, set, tuple)): # NOTE(imelnikov): some confs may be partially processed, so
new_value = self._include_iterable(value) # we have to ensure all the options got loaded.
return new_value for opt in self._cached[conf].iterkeys():
self._load_option(conf, opt)
def _process_includes(self, root): # TODO(imelnikov: can we really restore original order here?
if root in self.included: self._processed[conf] = utils.OrderedDict(
return sorted(self._processed.get(conf, {}).iteritems())
pth = sh.joinpths(self.base, "%s.yaml" % (root)) )
if not sh.isfile(pth): return self._processed[conf]
self.included[root] = {}
return
self.included[root] = utils.load_yaml(pth)
self.included[root] = self._do_include(self.included[root])
def extract(self, root):
if root in self.interpolated:
return self.interpolated[root]
self._process_includes(root)
self.interpolated[root] = self.included[root]
self.interpolated[root] = self._interpolate(self.interpolated[root])
return self.interpolated[root]
def create_parser(cfg_cls, component, fns=None): def create_parser(cfg_cls, component, fns=None):

View File

@ -108,3 +108,37 @@ class ProcessExecutionError(IOError):
self.exit_code, self.stdout, self.exit_code, self.stdout,
self.stderr)) self.stderr))
IOError.__init__(self, message) IOError.__init__(self, message)
class YamlException(ConfigException):
pass
class YamlOptionNotFoundException(YamlException):
"""Raised by YamlRefLoader if reference option not found."""
def __init__(self, conf, opt, ref_conf, ref_opt):
msg = "In `{0}`=>`{1}: '$({2}:{3})'` " \
"reference option `{3}` not found." \
.format(conf, opt, ref_conf, ref_opt)
super(YamlOptionNotFoundException, self).__init__(msg)
class YamlConfigNotFoundException(YamlException):
"""Raised by YamlRefLoader if config source not found."""
def __init__(self, path):
msg = "Could not find (open) yaml source {0}.".format(path)
super(YamlConfigNotFoundException, self).__init__(msg)
class YamlLoopException(YamlException):
"""Raised by YamlRefLoader if reference loop found."""
def __init__(self, conf, opt, ref_stack):
prettified_stack = "".join(map(
lambda (i, (c, o)): "\n%s`%s`=>`%s`" % (" " * i, str(c), str(o)),
enumerate(ref_stack))
)
msg = "In `{0}`=>`{1}` reference loop found.\n" \
"Reference stack is:{2}." \
.format(conf, opt, prettified_stack)
super(YamlLoopException, self).__init__(msg)

508
anvil/tests/test_cfg.py Normal file
View File

@ -0,0 +1,508 @@
import os
import shutil
import tempfile
import unittest
from anvil import cfg
from anvil import exceptions
from anvil import shell
from anvil import utils
class TestYamlRefLoader(unittest.TestCase):
def setUp(self):
super(TestYamlRefLoader, self).setUp()
self.sample = ""
self.sample2 = ""
self.sample3 = ""
self.temp_dir = tempfile.mkdtemp()
self.loader = cfg.YamlRefLoader(self.temp_dir)
def tearDown(self):
super(TestYamlRefLoader, self).tearDown()
shutil.rmtree(self.temp_dir, ignore_errors=True)
del self.loader
def _write_samples(self):
with open(os.path.join(self.temp_dir, 'sample.yaml'), 'w') as f:
f.write(self.sample)
with open(os.path.join(self.temp_dir, 'sample2.yaml'), 'w') as f:
f.write(self.sample2)
with open(os.path.join(self.temp_dir, 'sample3.yaml'), 'w') as f:
f.write(self.sample3)
def test_load__default(self):
self.sample = "default: default_value"
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict({'default': 'default_value'})
self.assertEqual(processed, should_be)
def test_load__empty(self):
self.sample = ""
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict()
self.assertEqual(processed, should_be)
def test_load__empty2(self):
self.sample = "empty: "
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict({'empty': None})
self.assertEqual(processed, should_be)
def test_load__integer(self):
self.sample = "integer: 11"
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict({'integer': 11})
self.assertEqual(processed, should_be)
def test_load__string(self):
self.sample = 'string: "string sample"'
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict({'string': "string sample"})
self.assertEqual(processed, should_be)
def test_load__float(self):
self.sample = "float: 1.1234"
self._write_samples()
processed = self.loader.load('sample')
self.assertAlmostEqual(processed['float'], 1.1234)
def test_load__bool(self):
self.sample = "bool: true"
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict({'bool': True})
self.assertEqual(processed, should_be)
def test_load__list(self):
self.sample = """
list:
- first
- second
- 100
"""
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict({'list': ['first', 'second', 100]})
self.assertEqual(processed, should_be)
def test_load__dict(self):
self.sample = """
dict:
integer: 11
default: default_value
string: "string sample"
"""
self._write_samples()
# Note: dictionaries are always sorted by options names.
processed = self.loader.load('sample')
should_be = utils.OrderedDict([
('dict',
utils.OrderedDict([
('default', 'default_value'),
('integer', 11),
('string', 'string sample')
]))
])
self.assertEqual(processed, should_be)
def test_load__nested_dict(self):
self.sample = """
dict:
dict1:
default: default_value
integer: 11
dict2:
default: default_value
string: "string sample"
"""
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict({
'dict': {
'dict1': {'default': 'default_value',
'integer': 11},
'dict2': {'default': 'default_value',
'string': 'string sample'}
}
})
self.assertEqual(processed, should_be)
def test_load__complex(self):
self.sample = """
# some comments...
integer: 15
bool-opt: false
bool-opt2: 0
bool-opt3: 1
float: 0.15
list:
- 1st
- 2nd
- 0.1
- 100
- true
dict:
dict1:
default: default_value 1
integer: 11
bool: true
dict2:
default: default_value 2
"""
self._write_samples()
processed = self.loader.load('sample')
self.assertEqual(len(processed), 7)
self.assertEqual(processed['integer'], 15)
self.assertEqual(processed['bool-opt'], False)
self.assertEqual(processed['bool-opt2'], False)
self.assertEqual(processed['bool-opt3'], True)
self.assertAlmostEqual(processed['float'], 0.15)
self.assertEqual(processed['list'], ['1st', '2nd', 0.1, 100, True])
self.assertEqual(processed['dict']['dict1']['integer'], 11)
self.assertEqual(processed['dict']['dict1']['bool'], True)
self.assertEqual(processed['dict']['dict1']['default'],
'default_value 1')
self.assertEqual(processed['dict']['dict2']['default'],
'default_value 2')
def test_load__simple_reference(self):
self.sample = 'opt: $(sample2:opt)'
self.sample2 = 'opt: 10'
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict({'opt': 10})
self.assertEqual(processed, should_be)
def test_load__self_reference(self):
self.sample = """
opt1: "$(sample:opt2)"
opt2: "$(sample:opt3)"
opt3: 10
"""
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict([('opt1', 10), ('opt2', 10), ('opt3', 10)])
self.assertEqual(processed, should_be)
def test_load__auto_reference(self):
self.sample = """
ip: "$(auto:ip)"
host: "$(auto:hostname)"
home: "$(auto:home)"
"""
self._write_samples()
processed = self.loader.load('sample')
self.assertTrue(isinstance(processed, utils.OrderedDict))
self.assertEqual(len(processed), 3)
self.assertEqual(processed['ip'], utils.get_host_ip())
self.assertEqual(processed['host'], shell.hostname())
self.assertEqual(processed['home'], shell.gethomedir())
def test_load__multi_reference(self):
self.sample = """
multi_ref: "9 + $(sample2:opt) + $(sample3:opt) + $(auto:home) + 12"
"""
self.sample2 = """opt: 10"""
self.sample3 = """opt: 11"""
self._write_samples()
processed = self.loader.load('sample')
self.assertTrue(isinstance(processed, utils.OrderedDict))
self.assertEqual(len(processed), 1)
self.assertEqual(processed['multi_ref'],
"9 + 10 + 11 + " + shell.gethomedir() + " + 12")
def test_load__dict_reference(self):
self.sample = """
sample2:
opt: "$(sample2:opt)"
"""
self.sample2 = """opt: 10"""
self._write_samples()
processed = self.loader.load('sample')
should_be = utils.OrderedDict([
('sample2', utils.OrderedDict([
('opt', 10)
]))
])
self.assertEqual(processed, should_be)
def test_load__wrapped_ref(self):
self.sample = """
stable: 23
prefixed: "1$(sample:stable)"
suffixed: "$(sample:stable)4"
wrapped: "1$(sample:stable)4"
"""
self._write_samples()
processed = self.loader.load('sample')
self.assertEqual(processed['prefixed'], "123")
self.assertEqual(processed['suffixed'], "234")
self.assertEqual(processed['wrapped'], "1234")
def test_load__complex_reference(self):
self.sample = """
stable: 9
ref0: "$(sample:stable)"
ref1: "$(sample2:stable)"
ref2: "$(sample2:ref1)"
ref3: "$(sample2:ref2)"
ref4: "$(sample2:ref3)"
ref5: "$(sample3:ref1)"
sample:
stable: "$(sample:stable)"
ref0: "$(sample:ref0)"
ref1: "$(sample:ref1)"
sample2:
stable: "$(sample2:stable)"
ref3: "$(sample2:ref3)"
sample3:
stable: "$(sample3:stable)"
ref1: "$(sample3:ref1)"
list:
- "$(sample:sample2)"
- "$(sample:sample3)"
dict:
sample3: "$(sample:sample3)"
sample2: "$(sample:sample2)"
"""
self.sample2 = """
stable: 10
ref1: "$(sample:stable)"
ref2: "$(sample3:stable)"
ref3: "$(sample3:ref1)"
ref4: "$(sample2:stable)"
"""
self.sample3 = """
stable: 11
ref1: "$(sample:stable)"
"""
self._write_samples()
processed = self.loader.load('sample')
self.assertTrue(isinstance(processed, utils.OrderedDict))
#self.assertEqual(len(processed), 11)
self.assertEqual(processed['stable'], 9)
self.assertEqual(processed['ref0'], 9)
self.assertEqual(processed['ref1'], 10)
self.assertEqual(processed['ref2'], 9)
self.assertEqual(processed['ref3'], 11)
self.assertEqual(processed['ref4'], 9)
self.assertEqual(processed['ref5'], 9)
sample = utils.OrderedDict([
('ref0', 9),
('ref1', 10),
('stable', 9),
])
self.assertEqual(processed['sample'], sample)
sample2 = utils.OrderedDict([
('ref3', 9),
('stable', 10),
])
self.assertEqual(processed['sample2'], sample2)
sample3 = utils.OrderedDict([
('ref1', 9),
('stable', 11),
])
self.assertEqual(processed['sample3'], sample3)
self.assertEqual(processed['list'], [sample2, sample3])
self.assertEqual(
processed['dict'],
utils.OrderedDict([
('sample2', sample2),
('sample3', sample3),
])
)
processed = self.loader.load('sample2')
self.assertEquals(processed, {
'stable': 10,
'ref1': 9,
'ref2': 11,
'ref3': 9,
'ref4': 10,
})
processed = self.loader.load('sample3')
self.assertEqual(len(processed), 2)
self.assertEqual(processed['stable'], 11)
self.assertEqual(processed['ref1'], 9)
def test_load__magic_reference(self):
self.sample = """
magic:
reference: $(sample:reference)
reference: "$(sample:stable)"
stable: 1
"""
self._write_samples()
processed = self.loader.load('sample')
self.assertEqual(processed['stable'], 1)
self.assertEqual(processed['reference'], 1)
self.assertEqual(processed['magic']['reference'], 1)
def test_load__more_complex_ref(self):
"""Test loading references links via dictionaries and lists."""
self.sample = """
stable: 9
ref_to_s1: "$(sample:stable)"
ref_to_s2: "$(sample2:stable)"
ref_to_s3: "$(sample3:stable)"
sample:
stable: "$(sample:stable)"
ref_to_s1: "$(sample:ref_to_s1)"
ref_to_s2: "$(sample:ref_to_s2)"
list:
- "$(sample:stable)"
- "$(sample2:stable)"
- "$(sample3:stable)"
- "$(sample:ref_to_s1)"
- "$(sample:ref_to_s2)"
- "$(sample:ref_to_s3)"
- "$(sample:sample)"
dict:
stable: "$(sample:stable)"
sample: "$(sample:sample)"
list: "$(sample:list)"
"""
self.sample2 = """stable: 10"""
self.sample3 = """stable: 11"""
self._write_samples()
processed = self.loader.load('sample')
self.assertEqual(processed['stable'], 9)
self.assertEqual(processed['ref_to_s1'], 9)
self.assertEqual(processed['ref_to_s2'], 10)
self.assertEqual(processed['ref_to_s3'], 11)
self.assertEqual(
processed['sample'],
utils.OrderedDict([('ref_to_s1', 9),
('ref_to_s2', 10),
('stable', 9)])
)
self.assertEqual(processed['list'], [9, 10, 11, 9, 10, 11,
processed['sample']])
self.assertEqual(
processed['dict'],
utils.OrderedDict([
('list', processed['list']),
('sample', processed['sample']),
('stable', 9),
])
)
def test_load__raises_no_option(self):
self.sample = "ref: $(sample2:no-such-opt)"
self.sample2 = ""
self._write_samples()
self.assertRaises(exceptions.YamlOptionNotFoundException,
self.loader.load, 'sample')
def test_load__raises_no_config(self):
self.sample = "ref: $(no-sush-conf:opt)"
self.sample2 = ""
self._write_samples()
self.assertRaises(exceptions.YamlConfigNotFoundException,
self.loader.load, 'sample')
def test_load__raises_loop(self):
self.sample = "opt: $(sample2:opt)"
self.sample2 = "opt: $(sample:opt)"
self._write_samples()
self.assertRaises(exceptions.YamlLoopException,
self.loader.load, 'sample')
def test_load__raises_self_loop(self):
self.sample = "opt: $(sample:opt)"
self._write_samples()
self.assertRaises(exceptions.YamlLoopException,
self.loader.load, 'sample')
self.sample = """
opt:
- $(sample:opt)
"""
self._write_samples()
self.assertRaises(exceptions.YamlLoopException,
self.loader.load, 'sample')
self.sample = """
opt:
opt: $(sample:opt)
"""
self._write_samples()
self.assertRaises(exceptions.YamlLoopException,
self.loader.load, 'sample')

View File

@ -0,0 +1,44 @@
import unittest
from anvil import exceptions
class TestYamlException(unittest.TestCase):
def test_YamlException(self):
self.assertTrue(issubclass(exceptions.YamlException,
exceptions.ConfigException))
def test_YamlOptionNotFoundException(self):
self.assertTrue(issubclass(exceptions.YamlOptionNotFoundException,
exceptions.YamlException))
exc = str(exceptions.YamlOptionNotFoundException(
'conf-sample', 'opt-sample', 'ref-conf', 'ref-opt'
))
self.assertTrue("`conf-sample`" in exc)
self.assertTrue("`ref-opt`" in exc)
self.assertTrue("opt-sample" in exc)
self.assertTrue("ref-conf:ref-opt" in exc)
def test_YamlConfigNotFoundException(self):
self.assertTrue(issubclass(exceptions.YamlConfigNotFoundException,
exceptions.YamlException))
exc = str(exceptions.YamlConfigNotFoundException(
"no/such//path/to/yaml"
))
self.assertTrue("no/such//path/to/yaml" in exc)
def test_YamlLoopException(self):
self.assertTrue(issubclass(exceptions.YamlLoopException,
exceptions.YamlException))
exc = str(exceptions.YamlLoopException('conf-sample', 'opt-sample',
[('s1', 'r1'), ('s2', 'r2')]))
self.assertTrue("`conf-sample`" in exc)
self.assertTrue("`opt-sample`" in exc)
self.assertTrue("loop found" in exc)
self.assertTrue("`s1`=>`r1`" in exc)
self.assertTrue("`s2`=>`r2`" in exc)