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)
#
# def load(self, distro, component, persona, cli):
#
# 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, 'ip': utils.get_host_ip,
'home': sh.gethomedir, 'home': sh.gethomedir,
'hostname': sh.hostname, '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)) # Process each reference in value (one by one)
return n_what for match in self._ref_pattern.finditer(value):
ref_conf, ref_opt = match.groups()
val = self._load_option(ref_conf, ref_opt)
if match.group(0) == value:
return val
else: else:
n_what = [] processed = re.sub(self._ref_pattern, str(val), processed, count=1)
for v in what: return processed
n_what.append(self._interpolate(v))
if isinstance(what, (tuple)):
n_what = tuple(n_what)
return n_what
def _interpolate_dictionary(self, what): def _process_dict(self, value):
n_what = {} """Process dictionary values."""
for (k, v) in what.iteritems(): processed = utils.OrderedDict()
n_what[k] = self._interpolate(v) for opt, val in sorted(value.items()):
return n_what res = self._process(val)
processed[opt] = res
def _include_dictionary(self, what): return processed
n_what = {}
for (k, value) in what.iteritems():
n_what[k] = self._do_include(value)
return n_what
def _include_iterable(self, what): def _process_iterable(self, value):
if isinstance(what, (set)): """Process list, set or tuple values."""
n_what = set() processed = []
for v in what: for item in value:
n_what.add(self._do_include(v)) processed.append(self._process(item))
return n_what
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)):
processed = self._process_iterable(value)
else: else:
n_what = [] processed = self._process_asis(value)
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)):
new_value = self._interpolate_string(value)
elif isinstance(value, (dict)):
new_value = self._interpolate_dictionary(value)
elif isinstance(value, (list, set, tuple)):
new_value = self._interpolate_iterable(value)
return new_value
def _interpolate_string(self, what): def _cache(self, conf):
if not re.search(INTERP_PAT, what): """Cache config file into memory to avoid re-reading it from disk."""
# Leave it alone if the sub won't do if conf not in self._cached:
# anything to begin with path = sh.joinpths(self._path, conf + self._conf_ext)
return what if not sh.isfile(path):
raise exceptions.YamlConfigNotFoundException(path)
def replacer(match): # TODO(vnovikov): may be it makes sense to reintroduce load_yaml
who = match.group(1).strip() # for returning OrderedDict with the same order as options placement
key = match.group(2).strip() # in source yaml file...
(is_special, special_value) = self._process_special(who, key) self._cached[conf] = utils.load_yaml(path) or {}
if is_special:
return special_value
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) def _precache(self):
"""Cache and process predefined auto-references"""
for conf, options in self._predefined_refs.items():
if conf not in self._processed:
processed = dict((option, functor())
for option, functor in options.items())
self._cached[conf] = processed
self._processed[conf] = processed
def _process_special(self, who, key): def _load_option(self, conf, opt):
if who and who.lower() in ['auto']: try:
if key not in self.auto_specials: return self._processed[conf][opt]
raise KeyError("Unknown auto key %r" % (key)) except KeyError:
functor = self.auto_specials[key] if (conf, opt) in self._ref_stack:
return (True, functor()) raise exceptions.YamlLoopException(conf, opt, self._ref_stack)
return (False, None) self._ref_stack.append((conf, opt))
def _include_string(self, what): self._cache(conf)
if not re.search(INTERP_PAT, what): try:
# Leave it alone if the sub won't do raw_value = self._cached[conf][opt]
# anything to begin with except KeyError:
return what try:
cur_conf, cur_opt = self._ref_stack[-1]
except IndexError:
cur_conf, cur_opt = None, None
raise exceptions.YamlOptionNotFoundException(
cur_conf, cur_opt, conf, opt
)
result = self._process(raw_value)
self._processed.setdefault(conf, {})[opt] = result
def replacer(match): self._ref_stack.pop()
who = match.group(1).strip() return result
key = match.group(2).strip()
(is_special, special_value) = self._process_special(who, key)
if is_special:
return special_value
# Process there includes and then
# fetch the value that should have been
# populated
self._process_includes(who)
return str(self.included[who][key])
return re.sub(INTERP_PAT, replacer, what) def load(self, conf):
"""Load config `conf` from same yaml file with and resolve all
def _do_include(self, value): references.
new_value = value """
if value and isinstance(value, (basestring, str)): self._precache()
new_value = self._include_string(value) self._cache(conf)
elif isinstance(value, (dict)): # NOTE(imelnikov): some confs may be partially processed, so
new_value = self._include_dictionary(value) # we have to ensure all the options got loaded.
elif isinstance(value, (list, set, tuple)): for opt in self._cached[conf].iterkeys():
new_value = self._include_iterable(value) self._load_option(conf, opt)
return new_value # TODO(imelnikov: can we really restore original order here?
self._processed[conf] = utils.OrderedDict(
def _process_includes(self, root): sorted(self._processed.get(conf, {}).iteritems())
if root in self.included: )
return return self._processed[conf]
pth = sh.joinpths(self.base, "%s.yaml" % (root))
if not sh.isfile(pth):
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)