Merge pull request #47 from salt-formulas/develop
Release 1.5.3
diff --git a/.gitignore b/.gitignore
index c5e9682..162c415 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
*.py[co]
.*.sw?
+.DS_Store
/reclass-config.yml
/reclass.egg-info
/build
/dist
/.coverage
+.kitchen
diff --git a/.kitchen-verify.sh b/.kitchen-verify.sh
new file mode 100755
index 0000000..fb3adde
--- /dev/null
+++ b/.kitchen-verify.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+#set -x
+
+# setup
+source /*.env
+INVENTORY_BASE_URI=/tmp/kitchen/test/model/$MODEL
+RECLASS=/tmp/kitchen
+
+# prereq
+python -m ensurepip --default-pip
+pip install pipenv
+
+# env
+cd $RECLASS
+pipenv --venv || pipenv install --python ${PYVER}
+test -e /etc/reclsss || mkdir /etc/reclass
+cp -avf $INVENTORY_BASE_URI/reclass-config* /etc/reclass
+
+# verify
+for n in $(ls $INVENTORY_BASE_URI/nodes/*|sort); do
+ pipenv run python${PYVER} ./reclass.py --inventory-base-uri=$INVENTORY_BASE_URI --nodeinfo $(basename $n .yml)
+done
diff --git a/.kitchen.yml b/.kitchen.yml
new file mode 100644
index 0000000..45be629
--- /dev/null
+++ b/.kitchen.yml
@@ -0,0 +1,41 @@
+---
+driver:
+ name: docker
+ priviledged: false
+ use_sudo: false
+ volume:
+ - <%= ENV['PWD'] %>:/tmp/kitchen
+
+
+provisioner:
+ name: shell
+ script: .kitchen-verify.sh
+
+
+verifier:
+ name: inspec
+
+<%- pyver = ENV['PYTHON_VERSION'] || '2.7' %>
+
+platforms:
+ <% `find test/model -maxdepth 1 -mindepth 1 -type d |sort -u`.split().each do |model| %>
+ <% model=model.split('/')[2] %>
+ - name: <%= model %>
+ driver_config:
+ image: python:<%= pyver %>
+ platform: ubuntu
+ hostname: reclass
+ provision_command:
+ #FIXME, setup reclass env (prereq, configs, upload models)
+ #- apt-get install -y rsync
+ - echo "
+ export LC_ALL=C.UTF-8;\n
+ export LANG=C.UTF-8;\n
+ export PYVER=<%= pyver %>;\n
+ export MODEL=<%= model %>;\n
+ " > /kitchen.env
+ <% end %>
+
+suites:
+ - name: model
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..2a41776
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,116 @@
+sudo: required
+language: python
+dist: trusty
+cache: pip
+python:
+- '2.7'
+- '3.6'
+service:
+- docker
+
+#apt:
+ #update: true
+
+#stages:
+#- name: test
+#- name: coverage
+#- name: models
+#- name: build
+# if: fork = false
+#- name: publish
+# if: tag =~ ^v.* and fork = false and branch = 'master'
+
+env:
+ global:
+ - PACKAGENAME="reclass"
+
+install: &pyinst
+- pip install pyparsing
+- pip install PyYAML
+# To test example models with kitchen:
+- |
+ test -e Gemfile || cat <<EOF > Gemfile
+ source 'https://rubygems.org'
+ gem 'rake'
+ gem 'test-kitchen'
+ gem 'kitchen-docker'
+ gem 'kitchen-inspec'
+ gem 'inspec'
+- bundle install
+
+script:
+- python setup.py install
+- find . reclass -name 'test_*.py' | sort | xargs -n1 -i% bash -c "echo %; python %"
+# To test example models with kitchen:
+- export PYTHON_VERSION=$TRAVIS_PYTHON_VERSION
+- kitchen list
+- if [ "$PYTHON_VERSION" = "2.7" ]; then kitchen test; fi
+
+# NOTE: travis stage builds, below saved for future reference
+#jobs:
+# include:
+# - stage: test
+# script: &unittest
+# - python setup.py install
+# - find . reclass -name 'test_*.py' | sort | xargs -n1 -i% bash -c "echo %; python %"
+#
+# - stage: coverage
+# install: *pyinst
+# script:
+# - python3 -m pytest --cov=. --cov-report=term-missing:skip-covered
+# - coverage xml
+# #- coveralls
+# #- |
+# #[ ! -z "${CODACY_PROJECT_TOKEN}" ] && python-codacy-coverage -r coverage.xml || echo "Codacy coverage NOT exported"
+#
+# - stage: lint
+# script:
+# - python3 -m flake8
+#
+# - stage: models
+# install: &kitchen
+# - pip install PyYAML
+# - pip install virtualenv
+# - |
+# test -e Gemfile || cat <<EOF > Gemfile
+# source 'https://rubygems.org'
+# gem 'rake'
+# gem 'test-kitchen'
+# gem 'kitchen-docker'
+# gem 'kitchen-inspec'
+# gem 'inspec'
+# - bundle install
+# script:
+# - export PYTHON_VERSION=$TRAVIS_PYTHON_VERSION
+# - kitchen list
+# #FIXME- kitchen test
+#
+# - stage: build
+# install: *pyinst
+# script: []
+#
+# - stage: publish
+# install:
+# - "/bin/true"
+# script:
+# - "/bin/true"
+# deploy:
+# provider: pypi
+# user: epcim
+# password:
+# secure: TBD
+# on:
+# tags: true
+# repo: salt-formulas/reclass
+# branch: master
+# #FIXME, $TRAVIS_PYTHON_VERSION == '2.7'
+
+notifications:
+ webhooks:
+ on_success: change # options: [always|never|change] default: always
+ on_failure: never
+ on_start: never
+ on_cancel: never
+ on_error: never
+ email: true
+
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..525e7cc
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,17 @@
+[[source]]
+url = "https://pypi.python.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[dev-packages]
+
+[packages]
+pyparsing = "*"
+PyYAML = "*"
+six = "*"
+pyyaml = "*"
+# FIXME, issues with compile phase
+#"pygit2" = "*"
+
+[requires]
+python_version = "2.7"
diff --git a/README-extentions.rst b/README-extentions.rst
index 97d78af..61364a4 100644
--- a/README-extentions.rst
+++ b/README-extentions.rst
@@ -208,6 +208,82 @@
group_errors: True
+Use references in class names
+-----------------------------
+
+Allows to use references in the class names.
+
+References pointed to in class names cannot themselves reference another key, they should be simple strings.
+
+To avoid pitfalls do not over-engineer your class references. They should be used only for core conditions and only for them.
+A short example: `- system.wrodpress.db.${_class:database_backend}`.
+
+Best practices:
+- use references in class names always load your global class specification prior the reference is used.
+- structure your class references under parameters under one key (for example `_class`).
+- use class references as a kind of "context" or "global" available options you always know what they are set.
+
+Class referencing for existing reclass users. Frequently when constructing your models you had to load or not load some
+classes based on your setup. In most cases this lead to fork of a model or introducing kind of template generator (like cookiecutter) to
+create a model based on the base "context" or "global" variables. Class referencing is a simple way how to avoid
+"pre-processors" like this and if/else conditions around class section.
+
+
+Assuming following class setup:
+
+* node is loading `third.yml` class only
+
+
+Classes:
+
+.. code-block:: yaml
+ #/etc/reclass/classes/global.yml
+ parameters:
+ _class:
+ env:
+ override: 'env.dev'
+ lab:
+ name: default
+
+ #/etc/reclass/classes/lab/env/dev.yml
+ parameters:
+ lab:
+ name: dev
+
+ #/etc/reclass/classes/second.yml
+ classes:
+ - global
+ - lab.${_class:env:override}
+
+ #/etc/reclass/classes/third.yml
+ classes:
+ - global
+ - second
+
+
+Reclass --nodeinfo then returns:
+
+.. code-block:: yaml
+
+ ...
+ ...
+ applications: []
+ environment: base
+ exports: {}
+ classes:
+ - global
+ - lab.${_class:env:override}
+ - second
+ parameters:
+ _class:
+ env:
+ override: env.dev
+ lab:
+ name: dev
+ ...
+ ...
+
+
Inventory Queries
-----------------
diff --git a/reclass/__init__.py b/reclass/__init__.py
index adb421e..a79c8e1 100644
--- a/reclass/__init__.py
+++ b/reclass/__init__.py
@@ -11,6 +11,7 @@
from reclass.storage.loader import StorageBackendLoader
from reclass.storage.memcache_proxy import MemcacheProxy
+
def get_storage(storage_type, nodes_uri, classes_uri, **kwargs):
storage_class = StorageBackendLoader(storage_type).load()
return MemcacheProxy(storage_class(nodes_uri, classes_uri, **kwargs))
diff --git a/reclass/adapters/ansible.py b/reclass/adapters/ansible.py
index 1887245..f6e9af3 100755
--- a/reclass/adapters/ansible.py
+++ b/reclass/adapters/ansible.py
@@ -16,6 +16,8 @@
import os, sys, posix, optparse
+from six import iteritems
+
from reclass import get_storage, output
from reclass.core import Core
from reclass.errors import ReclassException
@@ -81,7 +83,7 @@
apps = data['applications']
if options.applications_postfix:
postfix = options.applications_postfix
- groups.update([(k + postfix, v) for k,v in apps.iteritems()])
+ groups.update([(k + postfix, v) for (k, v) in iteritems(apps)])
else:
groups.update(apps)
diff --git a/reclass/adapters/salt.py b/reclass/adapters/salt.py
index 54adf5a..31179ff 100755
--- a/reclass/adapters/salt.py
+++ b/reclass/adapters/salt.py
@@ -9,6 +9,8 @@
import os, sys, posix
+from six import iteritems
+
from reclass import get_storage, output, get_path_mangler
from reclass.core import Core
from reclass.errors import ReclassException
@@ -68,7 +70,7 @@
else:
data = reclass.inventory()
nodes = {}
- for node_id, node_data in data['nodes'].iteritems():
+ for (node_id, node_data) in iteritems(data['nodes']):
env = node_data['environment']
if env not in nodes:
nodes[env] = {}
diff --git a/reclass/cli.py b/reclass/cli.py
index d1b22b8..f0b6069 100644
--- a/reclass/cli.py
+++ b/reclass/cli.py
@@ -38,9 +38,9 @@
else:
data = reclass.inventory()
- print output(data, options.output, options.pretty_print, options.no_refs)
+ print(output(data, options.output, options.pretty_print, options.no_refs))
- except ReclassException, e:
+ except ReclassException as e:
e.exit_with_message(sys.stderr)
sys.exit(posix.EX_OK)
diff --git a/reclass/core.py b/reclass/core.py
index 9da0ddb..1a08db8 100644
--- a/reclass/core.py
+++ b/reclass/core.py
@@ -15,13 +15,24 @@
import string
import sys
import yaml
+
+from six import iteritems
+
from reclass.settings import Settings
from reclass.output.yaml_outputter import ExplicitDumper
from reclass.datatypes import Entity, Classes, Parameters, Exports
-from reclass.errors import MappingFormatError, ClassNotFound, InvQueryClassNotFound, InvQueryError, InterpolationError
+from reclass.errors import MappingFormatError, ClassNameResolveError, ClassNotFound, InvQueryClassNameResolveError, InvQueryClassNotFound, InvQueryError, InterpolationError, ResolveError
+from reclass.values.parser import Parser
+
+try:
+ basestring
+except NameError:
+ basestring = str
class Core(object):
+ _parser = Parser()
+
def __init__(self, storage, class_mappings, settings, input_data=None):
self._storage = storage
self._class_mappings = class_mappings
@@ -54,7 +65,7 @@
regexp = True
try:
key = lexer.get_token()
- except ValueError, e:
+ except ValueError as e:
raise MappingFormatError('Error in mapping "{0}": missing closing '
'quote (or slash)'.format(instr))
if regexp:
@@ -88,7 +99,7 @@
p = Parameters(self._input_data, self._settings)
return Entity(self._settings, parameters=p, name='input data')
- def _recurse_entity(self, entity, merge_base=None, seen=None, nodename=None, environment=None):
+ def _recurse_entity(self, entity, merge_base=None, context=None, seen=None, nodename=None, environment=None):
if seen is None:
seen = {}
@@ -98,7 +109,19 @@
if merge_base is None:
merge_base = Entity(self._settings, name='empty (@{0})'.format(nodename))
+ if context is None:
+ context = Entity(self._settings, name='empty (@{0})'.format(nodename))
+
for klass in entity.classes.as_list():
+ if klass.count('$') > 0:
+ try:
+ klass = str(self._parser.parse(klass, self._settings).render(merge_base.parameters.as_dict(), {}))
+ except ResolveError as e:
+ try:
+ klass = str(self._parser.parse(klass, self._settings).render(context.parameters.as_dict(), {}))
+ except ResolveError as e:
+ raise ClassNameResolveError(klass, nodename, entity.uri)
+
if klass not in seen:
try:
class_entity = self._storage.get_class(klass, environment, self._settings)
@@ -113,7 +136,7 @@
e.uri = entity.uri
raise
- descent = self._recurse_entity(class_entity, seen=seen,
+ descent = self._recurse_entity(class_entity, context=merge_base, seen=seen,
nodename=nodename, environment=environment)
# on every iteration, we merge the result of the recursive
# descent into what we have so far…
@@ -128,7 +151,7 @@
def _get_automatic_parameters(self, nodename, environment):
if self._settings.automatic_parameters:
- return Parameters({ '_reclass_': { 'name': { 'full': nodename, 'short': string.split(nodename, '.')[0] },
+ return Parameters({ '_reclass_': { 'name': { 'full': nodename, 'short': str.split(nodename, '.')[0] },
'environment': environment } }, self._settings, '__auto__')
else:
return Parameters({}, self._settings, '')
@@ -151,6 +174,8 @@
node = self._node_entity(nodename)
except ClassNotFound as e:
raise InvQueryClassNotFound(e)
+ except ClassNameResolveError as e:
+ raise InvQueryClassNameResolveError(e)
if queries is None:
try:
node.interpolate_exports()
@@ -178,8 +203,8 @@
seen = {}
merge_base = self._recurse_entity(base_entity, seen=seen, nodename=nodename,
environment=node_entity.environment)
- return self._recurse_entity(node_entity, merge_base, seen=seen, nodename=nodename,
- environment=node_entity.environment)
+ return self._recurse_entity(node_entity, merge_base=merge_base, context=merge_base, seen=seen,
+ nodename=nodename, environment=node_entity.environment)
def _nodeinfo(self, nodename, inventory):
try:
@@ -220,7 +245,7 @@
nodes = {}
applications = {}
classes = {}
- for f, nodeinfo in entities.iteritems():
+ for (f, nodeinfo) in iteritems(entities):
d = nodes[f] = self._nodeinfo_as_dict(f, nodeinfo)
for a in d['applications']:
if a in applications:
diff --git a/reclass/datatypes/classes.py b/reclass/datatypes/classes.py
index b8793a2..090ed70 100644
--- a/reclass/datatypes/classes.py
+++ b/reclass/datatypes/classes.py
@@ -7,12 +7,13 @@
# Released under the terms of the Artistic Licence 2.0
#
-import types
+import six
import os
from reclass.errors import InvalidClassnameError
INVALID_CHARACTERS_FOR_CLASSNAMES = ' ' + os.sep
+
class Classes(object):
'''
A very limited ordered set of strings with O(n) uniqueness constraints. It
@@ -51,7 +52,7 @@
self.append_if_new(i)
def _assert_is_string(self, item):
- if not isinstance(item, types.StringTypes):
+ if not isinstance(item, six.string_types):
raise TypeError('%s instances can only contain strings, '\
'not %s' % (self.__class__.__name__, type(item)))
diff --git a/reclass/datatypes/exports.py b/reclass/datatypes/exports.py
index 62ea03f..971befa 100644
--- a/reclass/datatypes/exports.py
+++ b/reclass/datatypes/exports.py
@@ -6,6 +6,8 @@
import copy
+from six import iteritems, next
+
from .parameters import Parameters
from reclass.errors import ResolveError
from reclass.values.value import Value
@@ -25,12 +27,12 @@
self._unrendered.pop(key, None)
def overwrite(self, other):
- overdict = {'~' + key: value for key, value in other.iteritems()}
+ overdict = {'~' + key: value for (key, value) in iteritems(other)}
self.merge(overdict)
def interpolate_from_external(self, external):
while len(self._unrendered) > 0:
- path, v = self._unrendered.iteritems().next()
+ path, v = next(iteritems(self._unrendered))
value = path.get_value(self._base)
if isinstance(value, (Value, ValueList)):
external._interpolate_references(path, value, None)
@@ -51,7 +53,7 @@
required = self._get_required_paths(mainpath)
while len(required) > 0:
while len(required) > 0:
- path, v = required.iteritems().next()
+ path, v = next(iteritems(required))
value = path.get_value(self._base)
if isinstance(value, (Value, ValueList)):
try:
diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py
index ac15925..6ad0d27 100644
--- a/reclass/datatypes/parameters.py
+++ b/reclass/datatypes/parameters.py
@@ -10,12 +10,16 @@
import copy
import sys
import types
+
+from six import iteritems, next
+
from collections import namedtuple
from reclass.utils.dictpath import DictPath
from reclass.values.value import Value
from reclass.values.valuelist import ValueList
from reclass.errors import InfiniteRecursionError, ResolveError, ResolveErrorList, InterpolationError, ParseError, BadReferencesError
+
class Parameters(object):
'''
A class to hold nested dictionaries with the following specialities:
@@ -40,7 +44,7 @@
functionality and does not try to be a really mapping object.
'''
- def __init__(self, mapping, settings, uri, merge_initialise = True):
+ def __init__(self, mapping, settings, uri, parse_strings=True):
self._settings = settings
self._base = {}
self._uri = uri
@@ -50,14 +54,12 @@
self._resolve_errors = ResolveErrorList()
self._needs_all_envs = False
self._keep_overrides = False
+ self._parse_strings = parse_strings
if mapping is not None:
- if merge_initialise:
- # we initialise by merging
- self._keep_overrides = True
- self.merge(mapping)
- self._keep_overrides = False
- else:
- self._base = copy.deepcopy(mapping)
+ # we initialise by merging
+ self._keep_overrides = True
+ self.merge(mapping)
+ self._keep_overrides = False
#delimiter = property(lambda self: self._delimiter)
@@ -99,7 +101,7 @@
return value
else:
try:
- return Value(value, self._settings, self._uri)
+ return Value(value, self._settings, self._uri, parse_string=self._parse_strings)
except InterpolationError as e:
e.context = str(path)
raise
@@ -108,7 +110,7 @@
return [ self._wrap_value(v, path.new_subpath(k)) for (k, v) in enumerate(source) ]
def _wrap_dict(self, source, path):
- return { k: self._wrap_value(v, path.new_subpath(k)) for k, v in source.iteritems() }
+ return { k: self._wrap_value(v, path.new_subpath(k)) for (k, v) in iteritems(source) }
def _update_value(self, cur, new):
if isinstance(cur, Value):
@@ -123,7 +125,7 @@
elif isinstance(new, ValueList):
values.extend(new)
else:
- values.append(Value(new, self._settings, self._uri))
+ values.append(Value(new, self._settings, self._uri, parse_string=self._parse_strings))
return values
@@ -147,8 +149,11 @@
"""
ret = cur
- for key, newvalue in new.iteritems():
+ for (key, newvalue) in iteritems(new):
if key.startswith(self._settings.dict_key_override_prefix) and not self._keep_overrides:
+ if not isinstance(newvalue, Value):
+ newvalue = Value(newvalue, self._settings, self._uri, parse_string=self._parse_strings)
+ newvalue.overwrite = True
ret[key.lstrip(self._settings.dict_key_override_prefix)] = newvalue
else:
ret[key] = self._merge_recurse(ret.get(key), newvalue, path.new_subpath(key))
@@ -173,7 +178,6 @@
"""
-
if cur is None:
return new
elif isinstance(new, dict) and isinstance(cur, dict):
@@ -181,7 +185,7 @@
else:
return self._update_value(cur, new)
- def merge(self, other, wrap=True):
+ def merge(self, other):
"""Merge function (public edition).
Call _merge_recurse on self with either another Parameter object or a
@@ -197,15 +201,9 @@
self._unrendered = None
if isinstance(other, dict):
- if wrap:
- wrapped = self._wrap_dict(other, DictPath(self._settings.delimiter))
- else:
- wrapped = copy.deepcopy(other)
+ wrapped = self._wrap_dict(other, DictPath(self._settings.delimiter))
elif isinstance(other, self.__class__):
- if wrap:
- wrapped = self._wrap_dict(other._base, DictPath(self._settings.delimiter))
- else:
- wrapped = copy.deepcopy(other._base)
+ wrapped = self._wrap_dict(other._base, DictPath(self._settings.delimiter))
else:
raise TypeError('Cannot merge %s objects into %s' % (type(other),
self.__class__.__name__))
@@ -243,7 +241,7 @@
container[key] = value.render(None, None)
def _render_simple_dict(self, dictionary, path):
- for key, value in dictionary.iteritems():
+ for (key, value) in iteritems(dictionary):
self._render_simple_container(dictionary, key, value, path)
def _render_simple_list(self, item_list, path):
@@ -256,7 +254,7 @@
# we could use a view here, but this is simple enough:
# _interpolate_inner removes references from the refs hash after
# processing them, so we cannot just iterate the dict
- path, v = self._unrendered.iteritems().next()
+ path, v = next(iteritems(self._unrendered))
self._interpolate_inner(path, inventory)
if self._resolve_errors.have_errors():
raise self._resolve_errors
diff --git a/reclass/datatypes/tests/test_applications.py b/reclass/datatypes/tests/test_applications.py
index 307a430..6ae07cc 100644
--- a/reclass/datatypes/tests/test_applications.py
+++ b/reclass/datatypes/tests/test_applications.py
@@ -63,7 +63,7 @@
l = ['a', '~b', 'a', '~d']
a = Applications(l)
is_negation = lambda x: x.startswith(a.negation_prefix)
- GOAL = filter(lambda x: not is_negation(x), set(l)) + filter(is_negation, l)
+ GOAL = list(filter(lambda x: not is_negation(x), set(l))) + list(filter(is_negation, l))
self.assertEqual('%r' % a, "%s(%r, '~')" % (a.__class__.__name__, GOAL))
if __name__ == '__main__':
diff --git a/reclass/datatypes/tests/test_parameters.py b/reclass/datatypes/tests/test_parameters.py
index 577bdc4..9b788dc 100644
--- a/reclass/datatypes/tests/test_parameters.py
+++ b/reclass/datatypes/tests/test_parameters.py
@@ -9,6 +9,8 @@
import copy
+from six import iteritems
+
from reclass.settings import Settings
from reclass.datatypes import Parameters
from reclass.errors import InfiniteRecursionError, InterpolationError, ResolveError, ResolveErrorList
@@ -132,7 +134,7 @@
p2, b2 = self._construct_mocked_params(mergee)
p1.merge(p2)
p1.initialise_interpolation()
- for key, value in mergee.iteritems():
+ for (key, value) in iteritems(mergee):
# check that each key, value in mergee resulted in a get call and
# a __setitem__ call against b1 (the merge target)
self.assertIn(mock.call(key), b1.get.call_args_list)
@@ -202,6 +204,15 @@
p1.initialise_interpolation()
self.assertEqual(p1.as_dict()['key'], None)
+ def test_merge_none_over_list_negative(self):
+ l = ['foo', 1, 2]
+ settings = Settings({'allow_none_override': False})
+ p1 = Parameters(dict(key=l[:2]), settings, '')
+ p2 = Parameters(dict(key=None), settings, '')
+ with self.assertRaises(TypeError):
+ p1.merge(p2)
+ p1.initialise_interpolation()
+
def test_merge_none_over_dict(self):
settings = Settings({'allow_none_override': True})
p1 = Parameters(dict(key=SIMPLE), settings, '')
@@ -210,6 +221,14 @@
p1.initialise_interpolation()
self.assertEqual(p1.as_dict()['key'], None)
+ def test_merge_none_over_dict_negative(self):
+ settings = Settings({'allow_none_override': False})
+ p1 = Parameters(dict(key=SIMPLE), settings, '')
+ p2 = Parameters(dict(key=None), settings, '')
+ with self.assertRaises(TypeError):
+ p1.merge(p2)
+ p1.initialise_interpolation()
+
# def test_merge_bare_dict_over_dict(self):
# settings = Settings({'allow_bare_override': True})
# p1 = Parameters(dict(key=SIMPLE), settings, '')
@@ -624,5 +643,38 @@
p1.interpolate()
self.assertEqual(p1.as_dict(), r)
+ def test_complex_overwrites_1(self):
+ # find a better name for this test
+ p1 = Parameters({ 'test': { 'dict': { 'a': '${values:one}', 'b': '${values:two}' } },
+ 'values': { 'one': 1, 'two': 2, 'three': { 'x': 'X', 'y': 'Y' } } }, SETTINGS, '')
+ p2 = Parameters({ 'test': { 'dict': { 'c': '${values:two}' } } }, SETTINGS, '')
+ p3 = Parameters({ 'test': { 'dict': { '~b': '${values:three}' } } }, SETTINGS, '')
+ r = {'test': {'dict': {'a': 1, 'b': {'x': 'X', 'y': 'Y'}, 'c': 2}}, 'values': {'one': 1, 'three': {'x': 'X', 'y': 'Y'}, 'two': 2} }
+ p2.merge(p3)
+ p1.merge(p2)
+ p1.interpolate()
+ self.assertEqual(p1.as_dict(), r)
+
+ def test_escaped_string_overwrites(self):
+ p1 = Parameters({ 'test': '\${not_a_ref}' }, SETTINGS, '')
+ p2 = Parameters({ 'test': '\${also_not_a_ref}' }, SETTINGS, '')
+ r = { 'test': '${also_not_a_ref}' }
+ p1.merge(p2)
+ p1.interpolate()
+ self.assertEqual(p1.as_dict(), r)
+
+ def test_escaped_string_in_ref_dict_overwrite(self):
+ p1 = Parameters({'a': { 'one': '\${not_a_ref}' }, 'b': { 'two': '\${also_not_a_ref}' }}, SETTINGS, '')
+ p2 = Parameters({'c': '${a}'}, SETTINGS, '')
+ p3 = Parameters({'c': '${b}'}, SETTINGS, '')
+ p4 = Parameters({'c': { 'one': '\${again_not_a_ref}' } }, SETTINGS, '')
+ r = {'a': {'one': '${not_a_ref}'}, 'b': {'two': '${also_not_a_ref}'}, 'c': {'one': '${again_not_a_ref}', 'two': '${also_not_a_ref}'}}
+ p1.merge(p2)
+ p1.merge(p3)
+ p1.merge(p4)
+ p1.interpolate()
+ self.assertEqual(p1.as_dict(), r)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/reclass/defaults.py b/reclass/defaults.py
index 980bb92..408307d 100644
--- a/reclass/defaults.py
+++ b/reclass/defaults.py
@@ -7,7 +7,7 @@
# Released under the terms of the Artistic Licence 2.0
#
import os, sys
-from version import RECLASS_NAME
+from .version import RECLASS_NAME
# defaults for the command-line options
OPT_STORAGE_TYPE = 'yaml_fs'
@@ -29,7 +29,7 @@
OPT_ALLOW_SCALAR_OVER_LIST = False
OPT_ALLOW_LIST_OVER_SCALAR = False
OPT_ALLOW_DICT_OVER_SCALAR = False
-OPT_ALLOW_NONE_OVERRIDE = True
+OPT_ALLOW_NONE_OVERRIDE = False
OPT_INVENTORY_IGNORE_FAILED_NODE = False
OPT_INVENTORY_IGNORE_FAILED_RENDER = False
diff --git a/reclass/errors.py b/reclass/errors.py
index a96c47b..349e242 100644
--- a/reclass/errors.py
+++ b/reclass/errors.py
@@ -158,6 +158,17 @@
return msg
+class ClassNameResolveError(InterpolationError):
+ def __init__(self, classname, nodename, uri):
+ super(ClassNameResolveError, self).__init__(msg=None, uri=uri, nodename=nodename)
+ self.name = classname
+
+ def _get_error_message(self):
+ msg = [ 'In {0}'.format(self.uri),
+ 'Class name {0} not resolvable'.format(self.name) ]
+ return msg
+
+
class InvQueryClassNotFound(InterpolationError):
def __init__(self, classNotFoundError, nodename=''):
@@ -172,6 +183,19 @@
return msg
+class InvQueryClassNameResolveError(InterpolationError):
+ def __init__(self, classNameResolveError, nodename=''):
+ super(InvQueryClassNameResolveError, self).__init__(msg=None, nodename=nodename)
+ self.classNameResolveError = classNameResolveError
+ self._traceback = self.classNameResolveError._traceback
+
+ def _get_error_message(self):
+ msg = [ 'Inventory Queries:',
+ '-> {0}'.format(self.classNameResolveError.nodename) ]
+ msg.append(self.classNameResolveError._get_error_message())
+ return msg
+
+
class ResolveError(InterpolationError):
def __init__(self, reference, uri=None, context=None):
diff --git a/reclass/output/__init__.py b/reclass/output/__init__.py
index 58cd101..42fdb0b 100644
--- a/reclass/output/__init__.py
+++ b/reclass/output/__init__.py
@@ -6,13 +6,15 @@
# Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
# Released under the terms of the Artistic Licence 2.0
#
+
+
class OutputterBase(object):
def __init__(self):
pass
def dump(self, data, pretty_print=False):
- raise NotImplementedError, "dump() method not yet implemented"
+ raise NotImplementedError('dump() method not implemented.')
class OutputLoader(object):
@@ -27,6 +29,5 @@
def load(self, attr='Outputter'):
klass = getattr(self._module, attr, None)
if klass is None:
- raise AttributeError, \
- 'Outputter class {0} does not export "{1}"'.format(self._name, klass)
+ raise AttributeError('Outputter class {0} does not export "{1}"'.format(self._name, klass))
return klass
diff --git a/reclass/output/yaml_outputter.py b/reclass/output/yaml_outputter.py
index 9a0d098..ef62979 100644
--- a/reclass/output/yaml_outputter.py
+++ b/reclass/output/yaml_outputter.py
@@ -9,15 +9,17 @@
from reclass.output import OutputterBase
import yaml
+_SafeDumper = yaml.CSafeDumper if yaml.__with_libyaml__ else yaml.SafeDumper
+
class Outputter(OutputterBase):
def dump(self, data, pretty_print=False, no_refs=False):
if (no_refs):
return yaml.dump(data, default_flow_style=not pretty_print, Dumper=ExplicitDumper)
else:
- return yaml.dump(data, default_flow_style=not pretty_print)
+ return yaml.dump(data, default_flow_style=not pretty_print, Dumper=_SafeDumper)
-class ExplicitDumper(yaml.SafeDumper):
+class ExplicitDumper(_SafeDumper):
"""
A dumper that will never emit aliases.
"""
diff --git a/reclass/settings.py b/reclass/settings.py
index 44c58d8..e3fc26e 100644
--- a/reclass/settings.py
+++ b/reclass/settings.py
@@ -2,6 +2,11 @@
import reclass.values.parser_funcs
from reclass.defaults import *
+try:
+ basestring
+except NameError:
+ basestring = str
+
class Settings(object):
def __init__(self, options={}):
diff --git a/reclass/storage/loader.py b/reclass/storage/loader.py
index 77fdecb..10ca74c 100644
--- a/reclass/storage/loader.py
+++ b/reclass/storage/loader.py
@@ -6,13 +6,15 @@
# Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
# Released under the terms of the Artistic Licence 2.0
#
+import importlib
+
class StorageBackendLoader(object):
def __init__(self, storage_name):
self._name = 'reclass.storage.' + storage_name
try:
- self._module = __import__(self._name, globals(), locals(), self._name)
+ self._module = importlib.import_module(self._name)
except ImportError:
raise NotImplementedError
@@ -21,7 +23,6 @@
if klass is None:
raise AttributeError('Storage backend class {0} does not export '
'"{1}"'.format(self._name, klassname))
-
return klass
def path_mangler(self, name='path_mangler'):
diff --git a/reclass/storage/memcache_proxy.py b/reclass/storage/memcache_proxy.py
index 405ea8e..8c5e441 100644
--- a/reclass/storage/memcache_proxy.py
+++ b/reclass/storage/memcache_proxy.py
@@ -35,7 +35,7 @@
return self._real_storage.get_node(name, settings)
try:
return self._nodes_cache[name]
- except KeyError, e:
+ except KeyError as e:
ret = self._real_storage.get_node(name, settings)
self._nodes_cache[name] = ret
return ret
@@ -45,7 +45,7 @@
return self._real_storage.get_class(name, environment, settings)
try:
return self._classes_cache[environment][name]
- except KeyError, e:
+ except KeyError as e:
if environment not in self._classes_cache:
self._classes_cache[environment] = dict()
ret = self._real_storage.get_class(name, environment, settings)
diff --git a/reclass/storage/mixed/__init__.py b/reclass/storage/mixed/__init__.py
index 4651e00..990c931 100644
--- a/reclass/storage/mixed/__init__.py
+++ b/reclass/storage/mixed/__init__.py
@@ -6,6 +6,8 @@
import collections
import copy
+from six import iteritems
+
import reclass.errors
from reclass import get_storage
from reclass.storage import NodeStorageBase
@@ -32,7 +34,7 @@
self._classes_storage = dict()
if 'env_overrides' in classes_uri:
for override in classes_uri['env_overrides']:
- for env, options in override.iteritems():
+ for (env, options) in iteritems(override):
uri = copy.deepcopy(classes_uri)
uri.update(options)
uri = self._uri(uri)
diff --git a/reclass/storage/yaml_fs/__init__.py b/reclass/storage/yaml_fs/__init__.py
index b92cbfe..83f3666 100644
--- a/reclass/storage/yaml_fs/__init__.py
+++ b/reclass/storage/yaml_fs/__init__.py
@@ -93,7 +93,7 @@
relpath = self._nodes[name]
path = os.path.join(self.nodes_uri, relpath)
name = os.path.splitext(relpath)[0]
- except KeyError, e:
+ except KeyError as e:
raise reclass.errors.NodeNotFound(self.name, name, self.nodes_uri)
entity = YamlData.from_file(path).get_entity(name, settings)
return entity
@@ -102,7 +102,7 @@
vvv('GET CLASS {0}'.format(name))
try:
path = os.path.join(self.classes_uri, self._classes[name])
- except KeyError, e:
+ except KeyError as e:
raise reclass.errors.ClassNotFound(self.name, name, self.classes_uri)
entity = YamlData.from_file(path).get_entity(name, settings)
return entity
diff --git a/reclass/storage/yaml_fs/directory.py b/reclass/storage/yaml_fs/directory.py
index 03302b7..614e1c3 100644
--- a/reclass/storage/yaml_fs/directory.py
+++ b/reclass/storage/yaml_fs/directory.py
@@ -7,16 +7,16 @@
# Released under the terms of the Artistic Licence 2.0
#
import os
-import sys
from reclass.errors import NotFoundError
-SKIPDIRS = ( 'CVS', 'SCCS' )
+SKIPDIRS = ('CVS', 'SCCS')
FILE_EXTENSION = '.yml'
def vvv(msg):
#print >>sys.stderr, msg
pass
+
class Directory(object):
def __init__(self, path, fileclass=None):
@@ -39,7 +39,8 @@
files = property(lambda self: self._files)
def walk(self, register_fn=None):
- if not callable(register_fn): register_fn = self._register_files
+ if not callable(register_fn):
+ register_fn = self._register_files
def _error(exc):
raise(exc)
diff --git a/reclass/storage/yaml_git/__init__.py b/reclass/storage/yaml_git/__init__.py
index f4cb287..86c1247 100644
--- a/reclass/storage/yaml_git/__init__.py
+++ b/reclass/storage/yaml_git/__init__.py
@@ -15,6 +15,8 @@
warnings.simplefilter('ignore')
import pygit2
+from six import iteritems
+
import reclass.errors
from reclass.storage import NodeStorageBase
from reclass.storage.common import NameMangler
@@ -180,7 +182,7 @@
def nodes(self, branch, subdir):
ret = {}
- for name, file in self.files[branch].iteritems():
+ for (name, file) in iteritems(self.files[branch]):
if subdir is None or name.startswith(subdir):
node_name = os.path.splitext(file.name)[0]
if node_name in ret:
@@ -209,7 +211,7 @@
self._classes_uri = []
if 'env_overrides' in classes_uri:
for override in classes_uri['env_overrides']:
- for env, options in override.iteritems():
+ for (env, options) in iteritems(override):
uri = GitURI(self._classes_default_uri)
uri.update({ 'branch': env })
uri.update(options)
diff --git a/reclass/storage/yamldata.py b/reclass/storage/yamldata.py
index 0dda2b7..b11312e 100644
--- a/reclass/storage/yamldata.py
+++ b/reclass/storage/yamldata.py
@@ -11,6 +11,8 @@
import os
from reclass.errors import NotFoundError
+_SafeLoader = yaml.CSafeLoader if yaml.__with_libyaml__ else yaml.SafeLoader
+
class YamlData(object):
@classmethod
@@ -22,18 +24,17 @@
if not os.access(abs_path, os.R_OK):
raise NotFoundError('Cannot open: %s' % abs_path)
y = cls('yaml_fs://{0}'.format(abs_path))
- fp = file(abs_path)
- data = yaml.safe_load(fp)
- if data is not None:
- y._data = data
- fp.close()
+ with open(abs_path) as fp:
+ data = yaml.load(fp, Loader=_SafeLoader)
+ if data is not None:
+ y._data = data
return y
@classmethod
def from_string(cls, string, uri):
''' Initialise yaml data from a string '''
y = cls(uri)
- data = yaml.safe_load(string)
+ data = yaml.load(string, Loader=_SafeLoader)
if data is not None:
y._data = data
return y
diff --git a/reclass/utils/dictpath.py b/reclass/utils/dictpath.py
index aec6722..dfb8b32 100644
--- a/reclass/utils/dictpath.py
+++ b/reclass/utils/dictpath.py
@@ -7,7 +7,8 @@
# Released under the terms of the Artistic Licence 2.0
#
-import types, re
+import six
+import re
class DictPath(object):
'''
@@ -61,7 +62,7 @@
else:
if isinstance(contents, list):
self._parts = contents
- elif isinstance(contents, types.StringTypes):
+ elif isinstance(contents, six.string_types):
self._parts = self._split_string(contents)
elif isinstance(contents, tuple):
self._parts = list(contents)
@@ -76,7 +77,7 @@
return self._delim.join(str(i) for i in self._parts)
def __eq__(self, other):
- if isinstance(other, types.StringTypes):
+ if isinstance(other, six.string_types):
other = DictPath(self._delim, other)
return self._parts == other._parts \
diff --git a/reclass/values/compitem.py b/reclass/values/compitem.py
index 2134ea8..c6e8863 100644
--- a/reclass/values/compitem.py
+++ b/reclass/values/compitem.py
@@ -5,7 +5,7 @@
#
from reclass.settings import Settings
-from item import Item
+from .item import Item
class CompItem(Item):
@@ -39,6 +39,21 @@
def get_references(self):
return self._refs
+ def merge_over(self, item):
+ if item.type == Item.SCALAR or item.type == Item.COMPOSITE:
+ return self
+ elif item.type == Item.LIST:
+ if self._settings.allow_scalar_over_list or (self._settings.allow_none_override and self._value in [None, 'none', 'None']):
+ return self
+ else:
+ raise TypeError('allow scalar over list = False: cannot merge %s over %s' % (repr(self), repr(item)))
+ elif item.type == Item.DICTIONARY:
+ if self._settings.allow_scalar_over_dict or (self._settings.allow_none_override and self._value in [None, 'none', 'None']):
+ return self
+ else:
+ raise TypeError('allow scalar over dict = False: cannot merge %s over %s' % (repr(self), repr(item)))
+ raise TypeError('Cannot merge %s over %s' % (repr(self), repr(item)))
+
def render(self, context, inventory):
# Preserve type if only one item
if len(self._items) == 1:
diff --git a/reclass/values/dictitem.py b/reclass/values/dictitem.py
index d778fe2..555bd8f 100644
--- a/reclass/values/dictitem.py
+++ b/reclass/values/dictitem.py
@@ -5,7 +5,7 @@
#
from reclass.settings import Settings
-from item import Item
+from .item import Item
class DictItem(Item):
diff --git a/reclass/values/invitem.py b/reclass/values/invitem.py
index 84ea39d..970321b 100644
--- a/reclass/values/invitem.py
+++ b/reclass/values/invitem.py
@@ -7,7 +7,9 @@
import copy
import pyparsing as pp
-from item import Item
+from six import iteritems
+
+from .item import Item
from reclass.settings import Settings
from reclass.utils.dictpath import DictPath
from reclass.errors import ExpressionError, ParseError, ResolveError
@@ -301,7 +303,7 @@
def _value_expression(self, inventory):
results = {}
- for node, items in inventory.iteritems():
+ for (node, items) in iteritems(inventory):
if self._value_path.exists_in(items):
results[node] = copy.deepcopy(self._resolve(self._value_path, items))
return results
@@ -311,14 +313,14 @@
ExpressionError('Failed to render %s' % str(self), tbFlag=False)
results = {}
- for node, items in inventory.iteritems():
+ for (node, items) in iteritems(inventory):
if self._question.value(context, items) and self._value_path.exists_in(items):
results[node] = copy.deepcopy(self._resolve(self._value_path, items))
return results
def _list_test_expression(self, context, inventory):
results = []
- for node, items in inventory.iteritems():
+ for (node, items) in iteritems(inventory):
if self._question.value(context, items):
results.append(node)
return results
diff --git a/reclass/values/listitem.py b/reclass/values/listitem.py
index c7f29d0..1829e32 100644
--- a/reclass/values/listitem.py
+++ b/reclass/values/listitem.py
@@ -4,7 +4,7 @@
# This file is part of reclass
#
-from item import Item
+from .item import Item
from reclass.settings import Settings
class ListItem(Item):
diff --git a/reclass/values/parser.py b/reclass/values/parser.py
index bdd881d..a8adcf0 100644
--- a/reclass/values/parser.py
+++ b/reclass/values/parser.py
@@ -6,10 +6,10 @@
import pyparsing as pp
-from compitem import CompItem
-from invitem import InvItem
-from refitem import RefItem
-from scaitem import ScaItem
+from .compitem import CompItem
+from .invitem import InvItem
+from .refitem import RefItem
+from .scaitem import ScaItem
from reclass.errors import ParseError
from reclass.values.parser_funcs import STR, REF, INV
diff --git a/reclass/values/refitem.py b/reclass/values/refitem.py
index 0ae65e6..3e3341c 100644
--- a/reclass/values/refitem.py
+++ b/reclass/values/refitem.py
@@ -4,7 +4,7 @@
# This file is part of reclass
#
-from item import Item
+from .item import Item
from reclass.defaults import REFERENCE_SENTINELS
from reclass.settings import Settings
from reclass.utils.dictpath import DictPath
diff --git a/reclass/values/scaitem.py b/reclass/values/scaitem.py
index 466d3c9..f4265b5 100644
--- a/reclass/values/scaitem.py
+++ b/reclass/values/scaitem.py
@@ -5,7 +5,7 @@
#
from reclass.settings import Settings
-from item import Item
+from .item import Item
class ScaItem(Item):
@@ -18,15 +18,15 @@
return self._value
def merge_over(self, item):
- if item.type == Item.SCALAR:
+ if item.type == Item.SCALAR or item.type == Item.COMPOSITE:
return self
elif item.type == Item.LIST:
- if self._settings.allow_scalar_over_list or (self._settings.allow_none_override and self._value in [None, 'none', 'None']):
+ if self._settings.allow_scalar_over_list or (self._settings.allow_none_override and self._value is None):
return self
else:
raise TypeError('allow scalar over list = False: cannot merge %s over %s' % (repr(self), repr(item)))
elif item.type == Item.DICTIONARY:
- if self._settings.allow_scalar_over_dict or (self._settings.allow_none_override and self._value in [None, 'none', 'None']):
+ if self._settings.allow_scalar_over_dict or (self._settings.allow_none_override and self._value is None):
return self
else:
raise TypeError('allow scalar over dict = False: cannot merge %s over %s' % (repr(self), repr(item)))
diff --git a/reclass/values/value.py b/reclass/values/value.py
index 4ec6051..1a5b450 100644
--- a/reclass/values/value.py
+++ b/reclass/values/value.py
@@ -4,25 +4,29 @@
# This file is part of reclass
#
-from parser import Parser
-from dictitem import DictItem
-from listitem import ListItem
-from scaitem import ScaItem
+from .parser import Parser
+from .dictitem import DictItem
+from .listitem import ListItem
+from .scaitem import ScaItem
from reclass.errors import InterpolationError
class Value(object):
_parser = Parser()
- def __init__(self, value, settings, uri):
+ def __init__(self, value, settings, uri, parse_string=True):
self._settings = settings
self._uri = uri
+ self._overwrite = False
if isinstance(value, str):
- try:
- self._item = self._parser.parse(value, self._settings)
- except InterpolationError as e:
- e.uri = self._uri
- raise
+ if parse_string:
+ try:
+ self._item = self._parser.parse(value, self._settings)
+ except InterpolationError as e:
+ e.uri = self._uri
+ raise
+ else:
+ self._item = ScaItem(value, self._settings)
elif isinstance(value, list):
self._item = ListItem(value, self._settings)
elif isinstance(value, dict):
@@ -30,6 +34,14 @@
else:
self._item = ScaItem(value, self._settings)
+ @property
+ def overwrite(self):
+ return self._overwrite
+
+ @overwrite.setter
+ def overwrite(self, overwrite):
+ self._overwrite = overwrite
+
def uri(self):
return self._uri
diff --git a/reclass/values/valuelist.py b/reclass/values/valuelist.py
index 46d8ec7..bdfa01a 100644
--- a/reclass/values/valuelist.py
+++ b/reclass/values/valuelist.py
@@ -4,6 +4,8 @@
# This file is part of reclass
#
+from __future__ import print_function
+
import copy
import sys
@@ -98,18 +100,18 @@
if self._settings.ignore_overwritten_missing_references and not isinstance(output, (dict, list)) and n != (len(self._values)-1):
new = None
last_error = e
- print >>sys.stderr, "[WARNING] Reference '%s' undefined" % (str(value))
+ print("[WARNING] Reference '%s' undefined" % str(value), file=sys.stderr)
else:
raise e
- if output is None:
+ if output is None or value.overwrite:
output = new
deepCopied = False
else:
if isinstance(output, dict) and isinstance(new, dict):
- p1 = Parameters(output, self._settings, None, merge_initialise = False)
- p2 = Parameters(new, self._settings, None, merge_initialise = False)
- p1.merge(p2, wrap=False)
+ p1 = Parameters(output, self._settings, None, parse_strings=False)
+ p2 = Parameters(new, self._settings, None, parse_strings=False)
+ p1.merge(p2)
output = p1.as_dict()
continue
elif isinstance(output, list) and isinstance(new, list):
@@ -122,6 +124,7 @@
raise TypeError('Cannot merge %s over %s' % (repr(self._values[n]), repr(self._values[n-1])))
else:
output = new
+ deepCopied = False
if isinstance(output, (dict, list)) and last_error is not None:
raise last_error
diff --git a/reclass/version.py b/reclass/version.py
index 90c2cb7..664fd6f 100644
--- a/reclass/version.py
+++ b/reclass/version.py
@@ -8,7 +8,7 @@
#
RECLASS_NAME = 'reclass'
DESCRIPTION = 'merge data by recursive descent down an ancestry hierarchy (forked extended version)'
-VERSION = '1.5.2'
+VERSION = '1.5.3'
AUTHOR = 'martin f. krafft / Andrew Pickford / salt-formulas community'
AUTHOR_EMAIL = 'salt-formulas@freelists.org'
MAINTAINER = 'salt-formulas community'
diff --git a/requirements.txt b/requirements.txt
index ea72e95..66f0f4b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
pyparsing
pyyaml
pygit2
+six
diff --git a/setup.cfg b/setup.cfg
index 2f5e543..c5cfe6b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,5 +4,3 @@
# will need to generate wheels for each Python version that you support.
universal=0
-[install]
-prefix: /usr
diff --git a/setup.py b/setup.py
index 2fb77ae..5b4b8b6 100644
--- a/setup.py
+++ b/setup.py
@@ -37,8 +37,8 @@
license = LICENCE,
url = URL,
packages = find_packages(exclude=['*tests']), #FIXME validate this
- entry_point = { 'console_scripts': console_scripts },
- install_requires = ['pyparsing', 'pyyaml'], #FIXME pygit2 (require libffi-dev, libgit2-dev 0.26.x )
+ entry_points = { 'console_scripts': console_scripts },
+ install_requires = ['pyparsing', 'pyyaml', 'six'], #FIXME pygit2 (require libffi-dev, libgit2-dev 0.26.x )
classifiers=[
'Development Status :: 4 - Beta',
diff --git a/test/model/default/classes/first.yml b/test/model/default/classes/first.yml
new file mode 100644
index 0000000..9b72a26
--- /dev/null
+++ b/test/model/default/classes/first.yml
@@ -0,0 +1,32 @@
+parameters:
+ _param:
+ some: param
+ colour: red
+ lab:
+ name: test
+ label: first
+ colour:
+ escaped: \${_param:colour}
+ doubleescaped: \\${_param:colour}
+ unescaped: ${_param:colour}
+ colours:
+ red:
+ name: red
+ blue:
+ name: blue
+ one:
+ a: 1
+ b: 2
+ two:
+ c: 3
+ d: 4
+ three:
+ e: 5
+ list_to_override:
+ - one
+ - two
+ dict_to_override:
+ one: 1
+ two: 2
+
+
diff --git a/test/model/default/classes/lab/env/dev.yml b/test/model/default/classes/lab/env/dev.yml
new file mode 100644
index 0000000..0cce363
--- /dev/null
+++ b/test/model/default/classes/lab/env/dev.yml
@@ -0,0 +1,4 @@
+
+parameters:
+ lab:
+ name: dev
diff --git a/test/model/default/classes/second.yml b/test/model/default/classes/second.yml
new file mode 100644
index 0000000..dab50c7
--- /dev/null
+++ b/test/model/default/classes/second.yml
@@ -0,0 +1,9 @@
+classes:
+- first
+
+parameters:
+ will:
+ warn:
+ at:
+ second: ${_param:notfound}
+ three: ${one}
diff --git a/test/model/default/classes/third.yml b/test/model/default/classes/third.yml
new file mode 100644
index 0000000..135acd4
--- /dev/null
+++ b/test/model/default/classes/third.yml
@@ -0,0 +1,18 @@
+classes:
+- second
+
+parameters:
+ _param:
+ notfound: exist
+ myparam: ${_param:some}
+ will:
+ not:
+ fail:
+ at:
+ tree: ${_param:notfound}
+ three: ${two}
+ empty:
+ list: []
+ dict: {}
+ ~list_to_override: ${empty:list}
+ ~dict_to_override: ${empty:dict}
diff --git a/test/model/default/nodes/reclass.yml b/test/model/default/nodes/reclass.yml
new file mode 100644
index 0000000..94b7519
--- /dev/null
+++ b/test/model/default/nodes/reclass.yml
@@ -0,0 +1,3 @@
+
+classes:
+- third
diff --git a/test/model/default/reclass-config.yml b/test/model/default/reclass-config.yml
new file mode 100644
index 0000000..9d8f30f
--- /dev/null
+++ b/test/model/default/reclass-config.yml
@@ -0,0 +1 @@
+storage_type: yaml_fs
diff --git a/test/model/extensions/classes/first.yml b/test/model/extensions/classes/first.yml
new file mode 100644
index 0000000..96ece27
--- /dev/null
+++ b/test/model/extensions/classes/first.yml
@@ -0,0 +1,6 @@
+parameters:
+ _param:
+ some: param
+ lab:
+ name: test
+ label: first
diff --git a/test/model/extensions/classes/lab/env/dev.yml b/test/model/extensions/classes/lab/env/dev.yml
new file mode 100644
index 0000000..0cce363
--- /dev/null
+++ b/test/model/extensions/classes/lab/env/dev.yml
@@ -0,0 +1,4 @@
+
+parameters:
+ lab:
+ name: dev
diff --git a/test/model/extensions/classes/second.yml b/test/model/extensions/classes/second.yml
new file mode 100644
index 0000000..a9babd3
--- /dev/null
+++ b/test/model/extensions/classes/second.yml
@@ -0,0 +1,8 @@
+classes:
+- first
+
+parameters:
+ will:
+ warn:
+ at:
+ second: ${_param:notfound}
diff --git a/test/model/extensions/classes/third.yml b/test/model/extensions/classes/third.yml
new file mode 100644
index 0000000..20a937c
--- /dev/null
+++ b/test/model/extensions/classes/third.yml
@@ -0,0 +1,13 @@
+classes:
+- missing.class
+- second
+
+parameters:
+ _param:
+ notfound: exist
+ myparam: ${_param:some}
+ will:
+ not:
+ fail:
+ at:
+ tree: ${_param:notfound}
diff --git a/test/model/extensions/nodes/reclass.yml b/test/model/extensions/nodes/reclass.yml
new file mode 100644
index 0000000..94b7519
--- /dev/null
+++ b/test/model/extensions/nodes/reclass.yml
@@ -0,0 +1,3 @@
+
+classes:
+- third
diff --git a/test/model/extensions/reclass-config.yml b/test/model/extensions/reclass-config.yml
new file mode 100644
index 0000000..6e2f101
--- /dev/null
+++ b/test/model/extensions/reclass-config.yml
@@ -0,0 +1,3 @@
+storage_type: yaml_fs
+ignore_class_notfound: True
+ignore_class_regexp: ['.*']