Refactoring pt. 2
More boilerplate removed.
diff --git a/.travis.yml b/.travis.yml
index b060639..559ef9a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,8 +25,9 @@
- PACKAGENAME="reclass"
install: &pyinst
-- pip install pyparsing
-- pip install PyYAML
+- pip install -r requirements.txt
+#- pip install pyparsing
+#- pip install PyYAML
# To test example models with kitchen:
- |
test -e Gemfile || cat <<EOF > Gemfile
diff --git a/Pipfile b/Pipfile
index 525e7cc..fc2022b 100644
--- a/Pipfile
+++ b/Pipfile
@@ -10,6 +10,7 @@
PyYAML = "*"
six = "*"
pyyaml = "*"
+enum34 = "*"
# FIXME, issues with compile phase
#"pygit2" = "*"
diff --git a/reclass/datatypes/applications.py b/reclass/datatypes/applications.py
index 8c6ed15..90ae54c 100644
--- a/reclass/datatypes/applications.py
+++ b/reclass/datatypes/applications.py
@@ -33,9 +33,9 @@
self._negations = []
super(Applications, self).__init__(iterable)
- def _get_negation_prefix(self):
+ @property
+ def negation_prefix(self):
return self._negation_prefix
- negation_prefix = property(_get_negation_prefix)
def append_if_new(self, item):
self._assert_is_string(item)
diff --git a/reclass/datatypes/classes.py b/reclass/datatypes/classes.py
index 33d9b93..5270e28 100644
--- a/reclass/datatypes/classes.py
+++ b/reclass/datatypes/classes.py
@@ -11,11 +11,6 @@
from __future__ import print_function
from __future__ import unicode_literals
-#try:
-# from types import StringTypes
-#except ImportError:
-# StringTypes = (str, )
-
import six
import os
from reclass.errors import InvalidClassnameError
@@ -61,7 +56,6 @@
self.append_if_new(i)
def _assert_is_string(self, item):
- #if not isinstance(item, StringTypes):
if not isinstance(item, six.string_types):
raise TypeError('%s instances can only contain strings, '\
'not %s' % (self.__class__.__name__, type(item)))
@@ -81,5 +75,4 @@
self._append_if_new(item)
def __repr__(self):
- return '%s(%r)' % (self.__class__.__name__,
- self._items)
+ return '%s(%r)' % (self.__class__.__name__, self._items)
diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py
index 3c927c3..8133de5 100644
--- a/reclass/datatypes/entity.py
+++ b/reclass/datatypes/entity.py
@@ -22,18 +22,16 @@
for merging. The name and uri of an Entity will be updated to the name and
uri of the Entity that is being merged.
'''
- def __init__(self, settings, classes=None, applications=None, parameters=None,
- exports=None, uri=None, name=None, environment=None):
- self._uri = uri or ''
- self._name = name or ''
- if classes is None: classes = Classes()
- self._set_classes(classes)
- if applications is None: applications = Applications()
- self._set_applications(applications)
- if parameters is None: parameters = Parameters(None, settings, uri)
- if exports is None: exports = Exports(None, settings, uri)
- self._set_parameters(parameters)
- self._set_exports(exports)
+ def __init__(self, settings, classes=None, applications=None,
+ parameters=None, exports=None, uri=None, name=None,
+ environment=None):
+ self._uri = '' if uri is None else uri
+ self._name = '' if name is None else name
+ self._classes = self._set_field(classes, Classes)
+ self._applications = self._set_field(applications, Applications)
+ pars = [None, settings, uri]
+ self._parameters = self._set_field(parameters, Parameters, pars)
+ self._exports = self._set_field(exports, Exports, pars)
self._environment = environment
name = property(lambda s: s._name)
@@ -52,29 +50,15 @@
def environment(self, value):
self._environment = value
- def _set_classes(self, classes):
- if not isinstance(classes, Classes):
- raise TypeError('Entity.classes cannot be set to '\
- 'instance of type %s' % type(classes))
- self._classes = classes
-
- def _set_applications(self, applications):
- if not isinstance(applications, Applications):
- raise TypeError('Entity.applications cannot be set to '\
- 'instance of type %s' % type(applications))
- self._applications = applications
-
- def _set_parameters(self, parameters):
- if not isinstance(parameters, Parameters):
- raise TypeError('Entity.parameters cannot be set to '\
- 'instance of type %s' % type(parameters))
- self._parameters = parameters
-
- def _set_exports(self, exports):
- if not isinstance(exports, Exports):
- raise TypeError('Entity.exports cannot be set to '\
- 'instance of type %s' % type(exports))
- self._exports = exports
+ def _set_field(self, received_value, expected_type, parameters=None):
+ if parameters is None:
+ parameters = []
+ if received_value is None:
+ return expected_type(*parameters)
+ if not isinstance(received_value, expected_type):
+ raise TypeError('Entity.%s cannot be set to instance of type %s' %
+ (type(expected_type), type(received_value)))
+ return received_value
def merge(self, other):
self._classes.merge_unique(other._classes)
diff --git a/reclass/datatypes/parameters.py b/reclass/datatypes/parameters.py
index 4bac31a..1db35eb 100644
--- a/reclass/datatypes/parameters.py
+++ b/reclass/datatypes/parameters.py
@@ -12,11 +12,6 @@
from __future__ import print_function
from __future__ import unicode_literals
-#try:
-# from types import StringTypes
-#except ImportError:
-# StringTypes = (str, )
-
import copy
import sys
import types
@@ -33,6 +28,7 @@
from reclass.errors import ResolveErrorList, InterpolationError, ParseError
from reclass.errors import BadReferencesError
+
class Parameters(object):
'''
A class to hold nested dictionaries with the following specialities:
@@ -62,10 +58,9 @@
self._uri = uri
self._base = ParameterDict(uri=self._uri)
self._unrendered = None
- self._escapes_handled = {}
self._inv_queries = []
- self._resolve_errors = ResolveErrorList()
- self._needs_all_envs = False
+ self.resolve_errors = ResolveErrorList()
+ self.needs_all_envs = False
self._parse_strings = parse_strings
if mapping is not None:
# initialise by merging
@@ -92,13 +87,6 @@
def get_inv_queries(self):
return self._inv_queries
- @property
- def needs_all_envs(self):
- return self._needs_all_envs
-
- def resolve_errors(self):
- return self._resolve_errors
-
def as_dict(self):
return self._base.copy()
@@ -258,8 +246,8 @@
container[key] = value
if value.has_inv_query:
self._inv_queries.append((p, value))
- if value.needs_all_envs():
- self._needs_all_envs = True
+ if value.needs_all_envs:
+ self.needs_all_envs = True
return
else:
value = value.merge()
@@ -276,8 +264,8 @@
container[key] = value
if value.has_inv_query:
self._inv_queries.append((p, value))
- if value.needs_all_envs():
- self._needs_all_envs = True
+ if value.needs_all_envs:
+ self.needs_all_envs = True
else:
container[key] = value.render(None, None)
else:
@@ -303,8 +291,8 @@
# processing them, so we cannot just iterate the dict
path, v = next(iteritems(self._unrendered))
self._interpolate_inner(path, inventory)
- if self._resolve_errors.have_errors():
- raise self._resolve_errors
+ if self.resolve_errors.have_errors():
+ raise self.resolve_errors
def initialise_interpolation(self):
self._unrendered = None
@@ -314,8 +302,8 @@
if self._unrendered is None:
self._unrendered = {}
self._inv_queries = []
- self._needs_all_envs = False
- self._resolve_errors = ResolveErrorList()
+ self.needs_all_envs = False
+ self.resolve_errors = ResolveErrorList()
self._base = self._render_simple_dict(self._base,
DictPath(self._settings.delimiter))
@@ -339,7 +327,7 @@
except ResolveError as e:
e.context = path
if self._settings.group_errors:
- self._resolve_errors.add(e)
+ self.resolve_errors.add(e)
new = None
else:
raise
diff --git a/reclass/utils/dictpath.py b/reclass/utils/dictpath.py
index 6bf152a..70c7bb5 100644
--- a/reclass/utils/dictpath.py
+++ b/reclass/utils/dictpath.py
@@ -81,11 +81,12 @@
return self._delim.join(str(i) for i in self._parts)
def __eq__(self, other):
+ if not (isinstance(other, six.string_types) or
+ isinstance(other, self.__class__)):
+ return False
if isinstance(other, six.string_types):
other = DictPath(self._delim, other)
-
- return self._parts == other._parts \
- and self._delim == other._delim
+ return self._parts == other._parts and self._delim == other._delim
def __ne__(self, other):
return not self.__eq__(other)
@@ -152,18 +153,17 @@
def exists_in(self, container):
item = container
- for i in self._parts:
+ for part in self._parts:
if isinstance(item, (dict, list)):
- if i in item:
+ if part in item:
if isinstance(item, dict):
- item = item[i]
+ item = item[part]
elif isinstance(container, list):
- item = item[int(i)]
+ item = item[int(part)]
else:
return False
else:
if item == self._parts[-1]:
return True
- else:
- return False
+ return False
return True
diff --git a/reclass/values/compitem.py b/reclass/values/compitem.py
index 704ac69..c262f27 100644
--- a/reclass/values/compitem.py
+++ b/reclass/values/compitem.py
@@ -3,64 +3,28 @@
#
# This file is part of reclass
#
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
from reclass.settings import Settings
-from .item import Item
+from reclass.values import item
-class CompItem(Item):
- def __init__(self, items, settings):
- self.type = Item.COMPOSITE
- self._items = items
- self._settings = settings
- self.assembleRefs()
+class CompItem(item.ItemWithReferences):
- # TODO: possibility of confusion. Looks like 'assemble' should be either
- # 'gather' or 'extract'.
- def assembleRefs(self, context={}):
- self._refs = []
- self._allRefs = True
- for item in self._items:
- if item.has_references:
- item.assembleRefs(context)
- self._refs.extend(item.get_references())
- if item.allRefs is False:
- self._allRefs = False
+ type = item.ItemTypes.COMPOSITE
- @property
- def contents(self):
- return self._items
-
- @property
- def allRefs(self):
- return self._allRefs
-
- @property
- def has_references(self):
- return len(self._refs) > 0
-
- def get_references(self):
- return self._refs
-
- def merge_over(self, item):
- if item.type == Item.SCALAR or item.type == Item.COMPOSITE:
+ def merge_over(self, other):
+ if (other.type == item.ItemTypes.SCALAR or
+ other.type == item.ItemTypes.COMPOSITE):
return self
- raise RuntimeError('Trying to merge %s over %s' % (repr(self), repr(item)))
+ raise RuntimeError('Failed to merge %s over %s' % (self, other))
def render(self, context, inventory):
# Preserve type if only one item
- if len(self._items) == 1:
- return self._items[0].render(context, inventory)
+ if len(self.contents) == 1:
+ return self.contents[0].render(context, inventory)
# Multiple items
- strings = [ str(i.render(context, inventory)) for i in self._items ]
+ strings = [str(i.render(context, inventory)) for i in self.contents]
return "".join(strings)
- def __repr__(self):
- return 'CompItem(%r)' % self._items
-
def __str__(self):
- return ''.join([ str(i) for i in self._items ])
+ return ''.join([str(i) for i in self.contents])
diff --git a/reclass/values/dictitem.py b/reclass/values/dictitem.py
index b96875f..0648a39 100644
--- a/reclass/values/dictitem.py
+++ b/reclass/values/dictitem.py
@@ -3,30 +3,10 @@
#
# This file is part of reclass
#
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
-from reclass.settings import Settings
-from .item import Item
+from reclass.values import item
-class DictItem(Item):
- def __init__(self, item, settings):
- self.type = Item.DICTIONARY
- self._dict = item
- self._settings = settings
+class DictItem(item.ContainerItem):
- @property
- def contents(self):
- return self._dict
-
- def is_container(self):
- return True
-
- def render(self, context, inventory):
- return self._dict
-
- def __repr__(self):
- return 'DictItem(%r)' % self._dict
+ type = item.ItemTypes.DICTIONARY
diff --git a/reclass/values/invitem.py b/reclass/values/invitem.py
index 15b66c0..0179f4f 100644
--- a/reclass/values/invitem.py
+++ b/reclass/values/invitem.py
@@ -9,139 +9,72 @@
from __future__ import unicode_literals
import copy
-import functools
+import itertools as it
+import operator
import pyparsing as pp
from six import iteritems
from six import string_types
-from .item import Item
+from reclass.values import item
+from reclass.values import parser_funcs
from reclass.settings import Settings
from reclass.utils.dictpath import DictPath
from reclass.errors import ExpressionError, ParseError, ResolveError
-_OBJ = 'OBJ'
-_TEST = 'TEST'
-_LIST_TEST = 'LIST_TEST'
-_LOGICAL = 'LOGICAL'
-_OPTION = 'OPTION'
-_VALUE = 'VALUE'
-_IF = 'IF'
-_AND = 'AND'
-_OR = 'OR'
+# TODO: generalize expression handling.
+class BaseTestExpression(object):
-_EQUAL = '=='
-_NOT_EQUAL = '!='
-
-_IGNORE_ERRORS = '+IgnoreErrors'
-_ALL_ENVS = '+AllEnvs'
+ known_operators = {}
+ def __init__(self, delimiter):
+ self._delimiter = delimiter
+ self.refs = []
+ self.inv_refs = []
-def _get_parser():
- def tag_with(tag, transform=lambda x:x):
- def inner(tag, string, location, tokens):
- token = transform(tokens[0])
- tokens[0] = (tag, token)
- return functools.partial(inner, tag)
+class EqualityTest(BaseTestExpression):
- _object = tag_with(_OBJ)
- _option = tag_with(_OPTION)
- _expr_list_test = tag_with(_LIST_TEST)
- _test = tag_with(_TEST)
- _logical = tag_with(_LOGICAL)
- _if = tag_with(_IF)
- _expr_var = tag_with(_VALUE)
- _expr_test = tag_with(_TEST)
- _integer = tag_with(_OBJ, int)
- _number = tag_with(_OBJ, float)
-
- end = pp.StringEnd()
- ignore_errors = pp.CaselessLiteral(_IGNORE_ERRORS)
- all_envs = pp.CaselessLiteral(_ALL_ENVS)
- option = (ignore_errors | all_envs).setParseAction(_option)
- options = pp.Group(pp.ZeroOrMore(option))
- operator_test = (pp.Literal(_EQUAL) |
- pp.Literal(_NOT_EQUAL)).setParseAction(_test)
- operator_logical = (pp.CaselessLiteral(_AND) |
- pp.CaselessLiteral(_OR)).setParseAction(_logical)
- begin_if = pp.CaselessLiteral(_IF).setParseAction(_if)
- obj = pp.Word(pp.printables).setParseAction(_object)
- sign = pp.Optional(pp.Literal('-'))
- number = pp.Word(pp.nums)
- dpoint = pp.Literal('.')
- integer = pp.Combine(sign + number + pp.WordEnd()).setParseAction(_integer)
- real = pp.Combine(sign +
- ((number + dpoint + number) |
- (dpoint + number) |
- (number + dpoint))
- ).setParseAction(_number)
- item = integer | real | obj
-
- single_test = item + operator_test + item
- additional_test = operator_logical + single_test
- expr_var = pp.Group(obj + end).setParseAction(_expr_var)
- expr_test = pp.Group(obj + begin_if + single_test +
- pp.ZeroOrMore(additional_test) +
- end).setParseAction(_expr_test)
- expr_list_test = pp.Group(begin_if + single_test +
- pp.ZeroOrMore(additional_test) +
- end).setParseAction(_expr_list_test)
- expr = expr_test | expr_var | expr_list_test
- line = options + expr + end
- return line
-
-
-class Element(object):
+ known_operators = { parser_funcs.EQUAL: operator.eq,
+ parser_funcs.NOT_EQUAL: operator.ne}
def __init__(self, expression, delimiter):
- self._delimiter = delimiter
+ # expression is a list of at least three tuples, of which first element
+ # is a string tag, second is subelement value; other tuples apparently
+ # are not used.
+ # expression[0][1] effectively contains export path and apparently must
+ # be treated as such, also left hand operand in comparison
+ # expression[1][1] appa holds commparison operator == or !=
+ # expression[2][1] is the righhand operand
+ super(EqualityTest, self).__init__(delimiter)
# TODO: this double sommersault must be cleaned
_ = self._get_vars(expression[2][1], *self._get_vars(expression[0][1]))
self._export_path, self._parameter_path, self._parameter_value = _
-
try:
self._export_path.drop_first()
except AttributeError:
raise ExpressionError('No export')
-
- self._inv_refs = [ self._export_path ]
- self._test = expression[1][1]
-
+ try:
+ self._compare = self.known_operators[expression[1][1]]
+ except KeyError as e:
+ msg = 'Unknown test {0}'.format(expression[1][1])
+ raise ExpressionError(msg, tbFlag=False)
+ self.inv_refs = [self._export_path]
if self._parameter_path is not None:
self._parameter_path.drop_first()
- self._refs = [ str(self._parameter_path) ]
- else:
- self._refs = []
-
- def refs(self):
- return self._refs
-
- def inv_refs(self):
- return self._inv_refs
+ self.refs = [str(self._parameter_path)]
def value(self, context, items):
if self._parameter_path is not None:
- self._parameter_value = self._resolve(self._parameter_path, context)
-
- if self._parameter_value is None or self._test is None:
- raise ExpressionError('Failed to render %s' % str(self), tbFlag=False)
-
+ self._parameter_value = self._resolve(self._parameter_path,
+ context)
+ if self._parameter_value is None:
+ raise ExpressionError('Failed to render %s' % str(self),
+ tbFlag=False)
if self._export_path.exists_in(items):
- result = False
export_value = self._resolve(self._export_path, items)
- if self._test == _EQUAL:
- if export_value == self._parameter_value:
- result = True
- elif self._test == _NOT_EQUAL:
- if export_value != self._parameter_value:
- result = True
- else:
- raise ExpressionError('Unknown test {0}'.format(self._test),
- tbFlag=False)
- return result
- else:
- return False
+ return self._compare(export_value, self._parameter_value)
+ return False
def _resolve(self, path, dictionary):
try:
@@ -167,109 +100,82 @@
return export, parameter, value
-class Question(object):
+class LogicTest(BaseTestExpression):
- def __init__(self, expression, delimiter):
- self._elements = []
- self._operators = []
- self._delimiter = delimiter
- self._refs = []
- self._inv_refs = []
- i = 0
- while i < len(expression):
- e = Element(expression[i:], self._delimiter)
- self._elements.append(e)
- self._refs.extend(e.refs())
- self._inv_refs.extend(e.inv_refs())
- i += 3
- if i < len(expression):
- self._operators.append(expression[i][1])
- i += 1
+ known_operators = { parser_funcs.AND: operator.and_,
+ parser_funcs.OR: operator.or_}
- def refs(self):
- return self._refs
-
- def inv_refs(self):
- return self._inv_refs
+ def __init__(self, expr, delimiter):
+ super(LogicTest, self).__init__(delimiter)
+ subtests = list(it.compress(expr, it.cycle([1, 1, 1, 0])))
+ self._els = [EqualityTest(subtests[j:j+3], self._delimiter)
+ for j in range(0, len(subtests), 3)]
+ self.refs = [x.refs for x in self._els]
+ self.inv_refs = [x.inv_refs for x in self._els]
+ try:
+ self._ops = [self.known_operators[x[1]] for x in expr[3::4]]
+ except KeyError as e:
+ msg = 'Unknown operator {0} {1}'.format(e.messsage, self._els)
+ raise ExpressionError(msg, tbFlag=False)
def value(self, context, items):
- if len(self._elements) == 0:
+ if len(self._els) == 0: # NOTE: possible logic error
return True
- elif len(self._elements) == 1:
- return self._elements[0].value(context, items)
- else:
- result = self._elements[0].value(context, items)
- for i in range(0, len(self._elements)-1):
- next_result = self._elements[i+1].value(context, items)
- if self._operators[i] == _AND:
- result = result and next_result
- elif self._operators[i] == _OR:
- result = result or next_result
- else:
- emsg = 'Unknown operator {0} {1}'.format(
- self._operators[i], self.elements)
- raise ExpressionError(emsg, tbFlag=False)
- return result
+ result = self._els[0].value(context, items)
+ for op, next_el in zip(self._ops, self._els[1:]):
+ result = op(result, next_el.value(context, items))
+ return result
-class InvItem(Item):
+class InvItem(item.Item):
+ type = item.ItemTypes.INV_QUERY
- _parser = _get_parser()
-
- def __init__(self, item, settings):
- self.type = Item.INV_QUERY
- self._settings = settings
- self._needs_all_envs = False
- self._ignore_failed_render = self._settings.inventory_ignore_failed_render
- self._expr_text = item.render(None, None)
- self._parse_expression(self._expr_text)
+ def __init__(self, newitem, settings):
+ super(InvItem, self).__init__(newitem.render(None, None), settings)
+ self.needs_all_envs = False
+ self.ignore_failed_render = (
+ self._settings.inventory_ignore_failed_render)
+ self._parse_expression(self.contents)
def _parse_expression(self, expr):
+ parser = parser_funcs.get_expression_parser()
try:
- tokens = self._parser.parseString(expr).asList()
+ tokens = parser.parseString(expr).asList()
except pp.ParseException as e:
raise ParseError(e.msg, e.line, e.col, e.lineno)
- if len(tokens) == 1:
- self._expr_type = tokens[0][0]
- self._expr = list(tokens[0][1])
- elif len(tokens) == 2:
- for opt in tokens[0]:
- if opt[1] == _IGNORE_ERRORS:
- self._ignore_failed_render = True
- elif opt[1] == _ALL_ENVS:
- self._needs_all_envs = True
- self._expr_type = tokens[1][0]
- self._expr = list(tokens[1][1])
- else:
- raise ExpressionError('Failed to parse %s' % str(tokens), tbFlag=False)
+ if len(tokens) == 2: # options are set
+ passed_opts = [x[1] for x in tokens.pop(0)]
+ self.ignore_failed_render = parser_funcs.IGNORE_ERRORS in passed_opts
+ self.needs_all_envs = parser_funcs.ALL_ENVS in passed_opts
+ elif len(tokens) > 2:
+ raise ExpressionError('Failed to parse %s' % str(tokens),
+ tbFlag=False)
+ self._expr_type = tokens[0][0]
+ self._expr = list(tokens[0][1])
- if self._expr_type == _VALUE:
- self._value_path = DictPath(self._settings.delimiter, self._expr[0][1]).drop_first()
- self._question = Question([], self._settings.delimiter)
- self._refs = []
- self._inv_refs = [ self._value_path ]
- elif self._expr_type == _TEST:
- self._value_path = DictPath(self._settings.delimiter, self._expr[0][1]).drop_first()
- self._question = Question(self._expr[2:], self._settings.delimiter)
- self._refs = self._question.refs()
- self._inv_refs = self._question.inv_refs()
- self._inv_refs.append(self._value_path)
- elif self._expr_type == _LIST_TEST:
+ if self._expr_type == parser_funcs.VALUE:
+ self._value_path = DictPath(self._settings.delimiter,
+ self._expr[0][1]).drop_first()
+ self._question = LogicTest([], self._settings.delimiter)
+ self.refs = []
+ self.inv_refs = [self._value_path]
+ elif self._expr_type == parser_funcs.TEST:
+ self._value_path = DictPath(self._settings.delimiter,
+ self._expr[0][1]).drop_first()
+ self._question = LogicTest(self._expr[2:], self._settings.delimiter)
+ self.refs = self._question.refs
+ self.inv_refs = self._question.inv_refs
+ self.inv_refs.append(self._value_path)
+ elif self._expr_type == parser_funcs.LIST_TEST:
self._value_path = None
- self._question = Question(self._expr[1:], self._settings.delimiter)
- self._refs = self._question.refs()
- self._inv_refs = self._question.inv_refs()
+ self._question = LogicTest(self._expr[1:], self._settings.delimiter)
+ self.refs = self._question.refs
+ self.inv_refs = self._question.inv_refs
else:
- raise ExpressionError('Unknown expression type: %s' % self._expr_type, tbFlag=False)
-
- def assembleRefs(self, context):
- return
-
- @property
- def contents(self):
- return self._expr_text
+ msg = 'Unknown expression type: %s'
+ raise ExpressionError(msg % self._expr_type, tbFlag=False)
@property
def has_inv_query(self):
@@ -277,19 +183,13 @@
@property
def has_references(self):
- return len(self._question.refs()) > 0
+ return len(self._question.refs) > 0
def get_references(self):
- return self._question.refs()
+ return self._question.refs
def get_inv_references(self):
- return self._inv_refs
-
- def needs_all_envs(self):
- return self._needs_all_envs
-
- def ignore_failed_render(self):
- return self._ignore_failed_render
+ return self.inv_refs
def _resolve(self, path, dictionary):
try:
@@ -301,17 +201,21 @@
results = {}
for (node, items) in iteritems(inventory):
if self._value_path.exists_in(items):
- results[node] = copy.deepcopy(self._resolve(self._value_path, items))
+ results[node] = copy.deepcopy(self._resolve(self._value_path,
+ items))
return results
def _test_expression(self, context, inventory):
if self._value_path is None:
- ExpressionError('Failed to render %s' % str(self), tbFlag=False)
+ msg = 'Failed to render %s'
+ raise ExpressionError(msg % str(self), tbFlag=False)
results = {}
- 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))
+ 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):
@@ -322,11 +226,11 @@
return results
def render(self, context, inventory):
- if self._expr_type == _VALUE:
+ if self._expr_type == parser_funcs.VALUE:
return self._value_expression(inventory)
- elif self._expr_type == _TEST:
+ elif self._expr_type == parser_funcs.TEST:
return self._test_expression(context, inventory)
- elif self._expr_type == _LIST_TEST:
+ elif self._expr_type == parser_funcs.LIST_TEST:
return self._list_test_expression(context, inventory)
raise ExpressionError('Failed to render %s' % str(self), tbFlag=False)
@@ -334,4 +238,5 @@
return ' '.join(str(j) for i,j in self._expr)
def __repr__(self):
+ # had to leave it here for now as the behaviour differs from basic
return 'InvItem(%r)' % self._expr
diff --git a/reclass/values/item.py b/reclass/values/item.py
index 4ab3f68..ee46995 100644
--- a/reclass/values/item.py
+++ b/reclass/values/item.py
@@ -8,22 +8,20 @@
from __future__ import print_function
from __future__ import unicode_literals
+from enum import Enum
+
from reclass.utils.dictpath import DictPath
+ItemTypes = Enum('ItemTypes',
+ ['COMPOSITE', 'DICTIONARY', 'INV_QUERY', 'LIST',
+ 'REFERENCE', 'SCALAR'])
+
+
class Item(object):
- # TODO: use enum.Enum
- # TODO: consider DotMap
- COMPOSITE = 1
- DICTIONARY = 2
- INV_QUERY = 3
- LIST = 4
- REFERENCE = 5
- SCALAR = 6
-
- TYPE_STR = { COMPOSITE: 'composite', DICTIONARY: 'dictionary',
- INV_QUERY: 'invventory query', LIST: 'list',
- REFERENCE: 'reference', SCALAR: 'scalar' }
+ def __init__(self, item, settings):
+ self._settings = settings
+ self.contents = item
def allRefs(self):
return True
@@ -43,11 +41,6 @@
def is_complex(self):
return (self.has_references | self.has_inv_query)
- @property
- def contents(self):
- msg = "Item class {0} does not implement contents()"
- raise NotImplementedError(msg.format(self.__class__.__name__))
-
def merge_over(self, item):
msg = "Item class {0} does not implement merge_over()"
raise NotImplementedError(msg.format(self.__class__.__name__))
@@ -57,4 +50,41 @@
raise NotImplementedError(msg.format(self.__class__.__name__))
def type_str(self):
- return self.TYPE_STR[self.type]
+ return self.type.name.lower()
+
+ def __repr__(self):
+ return '%s(%r)' % (self.__class__.__name__, self.contents)
+
+
+class ItemWithReferences(Item):
+
+ def __init__(self, items, settings):
+ super(ItemWithReferences, self).__init__(items, settings)
+ self.assembleRefs()
+
+ @property
+ def has_references(self):
+ return len(self._refs) > 0
+
+ def get_references(self):
+ return self._refs
+
+ # NOTE: possibility of confusion. Looks like 'assemble' should be either
+ # 'gather' or 'extract'.
+ def assembleRefs(self, context={}):
+ self._refs = []
+ self.allRefs = True
+ for item in self.contents:
+ if item.has_references:
+ item.assembleRefs(context)
+ self._refs.extend(item.get_references())
+ if item.allRefs is False:
+ self.allRefs = False
+
+class ContainerItem(Item):
+
+ def is_container(self):
+ return True
+
+ def render(self, context, inventory):
+ return self.contents
diff --git a/reclass/values/listitem.py b/reclass/values/listitem.py
index 0f0ee60..24bece1 100644
--- a/reclass/values/listitem.py
+++ b/reclass/values/listitem.py
@@ -3,36 +3,16 @@
#
# This file is part of reclass
#
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
-from .item import Item
-from reclass.settings import Settings
+from reclass.values import item
-class ListItem(Item):
- def __init__(self, item, settings):
- self.type = Item.LIST
- self._list = item
- self._settings = settings
+class ListItem(item.ContainerItem):
- @property
- def contents(self):
- return self._list
+ type = item.ItemTypes.LIST
- def is_container(self):
- return True
-
- def render(self, context, inventory):
- return self._list
-
- def merge_over(self, item):
- if item.type == Item.LIST:
- item._list.extend(self._list)
- return item
- raise RuntimeError('Trying to merge %s over %s' % (repr(self), repr(item)))
-
- def __repr__(self):
- return 'ListItem(%r)' % (self._list)
+ def merge_over(self, other):
+ if other.type == item.ItemTypes.LIST:
+ other.contents.extend(self.contents)
+ return other
+ raise RuntimeError('Failed to merge %s over %s' % (self, other))
diff --git a/reclass/values/parser_funcs.py b/reclass/values/parser_funcs.py
index 46db7cc..50babd0 100644
--- a/reclass/values/parser_funcs.py
+++ b/reclass/values/parser_funcs.py
@@ -9,22 +9,76 @@
from __future__ import unicode_literals
import pyparsing as pp
+import functools
STR = 1
REF = 2
INV = 3
-def _string(string, location, tokens):
- token = tokens[0]
- tokens[0] = (STR, token)
+_OBJ = 'OBJ'
+_LOGICAL = 'LOGICAL'
+_OPTION = 'OPTION'
+_IF = 'IF'
-def _reference(string, location, tokens):
- token = list(tokens[0])
- tokens[0] = (REF, token)
+TEST = 'TEST'
+LIST_TEST = 'LIST_TEST'
-def _invquery(string, location, tokens):
- token = list(tokens[0])
- tokens[0] = (INV, token)
+VALUE = 'VALUE'
+AND = 'AND'
+OR = 'OR'
+
+EQUAL = '=='
+NOT_EQUAL = '!='
+
+IGNORE_ERRORS = '+IgnoreErrors'
+ALL_ENVS = '+AllEnvs'
+
+
+def _tag_with(tag, transform=lambda x:x):
+ def inner(tag, string, location, tokens):
+ token = transform(tokens[0])
+ tokens[0] = (tag, token)
+ return functools.partial(inner, tag)
+
+def get_expression_parser():
+
+ s_end = pp.StringEnd()
+ sign = pp.Optional(pp.Literal('-'))
+ number = pp.Word(pp.nums)
+ dpoint = pp.Literal('.')
+ ignore_errors = pp.CaselessLiteral(IGNORE_ERRORS)
+ all_envs = pp.CaselessLiteral(ALL_ENVS)
+ eq, neq = pp.Literal(EQUAL), pp.Literal(NOT_EQUAL)
+ eand, eor = pp.CaselessLiteral(AND), pp.CaselessLiteral(OR)
+
+ option = (ignore_errors | all_envs).setParseAction(_tag_with(_OPTION))
+ options = pp.Group(pp.ZeroOrMore(option))
+ operator_test = (eq | neq).setParseAction(_tag_with(TEST))
+ operator_logical = (eand | eor).setParseAction(_tag_with(_LOGICAL))
+ begin_if = pp.CaselessLiteral(_IF).setParseAction(_tag_with(_IF))
+ obj = pp.Word(pp.printables).setParseAction(_tag_with(_OBJ))
+
+ integer = pp.Combine(sign + number + pp.WordEnd()).setParseAction(
+ _tag_with(_OBJ, int))
+ real = pp.Combine(sign +
+ ((number + dpoint + number) |
+ (dpoint + number) |
+ (number + dpoint))
+ ).setParseAction(_tag_with(_OBJ, float))
+ expritem = integer | real | obj
+ single_test = expritem + operator_test + expritem
+ additional_test = operator_logical + single_test
+
+ expr_var = pp.Group(obj + s_end).setParseAction(_tag_with(VALUE))
+ expr_test = pp.Group(obj + begin_if + single_test +
+ pp.ZeroOrMore(additional_test) +
+ s_end).setParseAction(_tag_with(TEST))
+ expr_list_test = pp.Group(begin_if + single_test +
+ pp.ZeroOrMore(additional_test) +
+ s_end).setParseAction(_tag_with(LIST_TEST))
+ expr = expr_test | expr_var | expr_list_test
+ line = options + expr + s_end
+ return line
def get_ref_parser(escape_character, reference_sentinels, export_sentinels):
_ESCAPE = escape_character
@@ -50,8 +104,12 @@
_EXCLUDES = _ESCAPE + _REF_OPEN + _REF_CLOSE + _INV_OPEN + _INV_CLOSE
- double_escape = pp.Combine(pp.Literal(_DOUBLE_ESCAPE) + pp.MatchFirst([pp.FollowedBy(_REF_OPEN), pp.FollowedBy(_REF_CLOSE),
- pp.FollowedBy(_INV_OPEN), pp.FollowedBy(_INV_CLOSE)])).setParseAction(pp.replaceWith(_ESCAPE))
+ double_escape = pp.Combine(pp.Literal(_DOUBLE_ESCAPE) +
+ pp.MatchFirst([pp.FollowedBy(_REF_OPEN),
+ pp.FollowedBy(_REF_CLOSE),
+ pp.FollowedBy(_INV_OPEN),
+ pp.FollowedBy(_INV_CLOSE)])).setParseAction(
+ pp.replaceWith(_ESCAPE))
ref_open = pp.Literal(_REF_OPEN).suppress()
ref_close = pp.Literal(_REF_CLOSE).suppress()
@@ -61,10 +119,10 @@
ref_escape_close = pp.Literal(_REF_ESCAPE_CLOSE).setParseAction(pp.replaceWith(_REF_CLOSE))
ref_text = pp.CharsNotIn(_REF_EXCLUDES) | pp.CharsNotIn(_REF_CLOSE_FIRST, exact=1)
ref_content = pp.Combine(pp.OneOrMore(ref_not_open + ref_not_close + ref_text))
- ref_string = pp.MatchFirst([double_escape, ref_escape_open, ref_escape_close, ref_content]).setParseAction(_string)
+ ref_string = pp.MatchFirst([double_escape, ref_escape_open, ref_escape_close, ref_content]).setParseAction(_tag_with(STR))
ref_item = pp.Forward()
ref_items = pp.OneOrMore(ref_item)
- reference = (ref_open + pp.Group(ref_items) + ref_close).setParseAction(_reference)
+ reference = (ref_open + pp.Group(ref_items) + ref_close).setParseAction(_tag_with(REF))
ref_item << (reference | ref_string)
inv_open = pp.Literal(_INV_OPEN).suppress()
@@ -75,13 +133,13 @@
inv_escape_close = pp.Literal(_INV_ESCAPE_CLOSE).setParseAction(pp.replaceWith(_INV_CLOSE))
inv_text = pp.CharsNotIn(_INV_CLOSE_FIRST)
inv_content = pp.Combine(pp.OneOrMore(inv_not_close + inv_text))
- inv_string = pp.MatchFirst([double_escape, inv_escape_open, inv_escape_close, inv_content]).setParseAction(_string)
+ inv_string = pp.MatchFirst([double_escape, inv_escape_open, inv_escape_close, inv_content]).setParseAction(_tag_with(STR))
inv_items = pp.OneOrMore(inv_string)
- export = (inv_open + pp.Group(inv_items) + inv_close).setParseAction(_invquery)
+ export = (inv_open + pp.Group(inv_items) + inv_close).setParseAction(_tag_with(INV))
text = pp.CharsNotIn(_EXCLUDES) | pp.CharsNotIn('', exact=1)
content = pp.Combine(pp.OneOrMore(ref_not_open + inv_not_open + text))
- string = pp.MatchFirst([double_escape, ref_escape_open, inv_escape_open, content]).setParseAction(_string)
+ string = pp.MatchFirst([double_escape, ref_escape_open, inv_escape_open, content]).setParseAction(_tag_with(STR))
item = reference | export | string
line = pp.OneOrMore(item) + pp.StringEnd()
@@ -95,9 +153,9 @@
_INV_CLOSE = export_sentinels[1]
_EXCLUDES = _ESCAPE + _REF_OPEN + _REF_CLOSE + _INV_OPEN + _INV_CLOSE
- string = pp.CharsNotIn(_EXCLUDES).setParseAction(_string)
+ string = pp.CharsNotIn(_EXCLUDES).setParseAction(_tag_with(STR))
ref_open = pp.Literal(_REF_OPEN).suppress()
ref_close = pp.Literal(_REF_CLOSE).suppress()
- reference = (ref_open + pp.Group(string) + ref_close).setParseAction(_reference)
+ reference = (ref_open + pp.Group(string) + ref_close).setParseAction(_tag_with(REF))
line = pp.StringStart() + pp.Optional(string) + reference + pp.Optional(string) + pp.StringEnd()
return line
diff --git a/reclass/values/refitem.py b/reclass/values/refitem.py
index 5713346..df713e1 100644
--- a/reclass/values/refitem.py
+++ b/reclass/values/refitem.py
@@ -3,56 +3,24 @@
#
# This file is part of reclass
#
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
-from .item import Item
-from reclass.defaults import REFERENCE_SENTINELS
-from reclass.settings import Settings
+from reclass.values import item
from reclass.utils.dictpath import DictPath
from reclass.errors import ResolveError
-class RefItem(Item):
+class RefItem(item.ItemWithReferences):
- def __init__(self, items, settings):
- self.type = Item.REFERENCE
- self._settings = settings
- self._items = items
- self.assembleRefs()
+ type = item.ItemTypes.REFERENCE
def assembleRefs(self, context={}):
- self._refs = []
- self._allRefs = True
- for item in self._items:
- if item.has_references:
- item.assembleRefs(context)
- self._refs.extend(item.get_references())
- if item.allRefs == False:
- self._allRefs = False
+ super(RefItem, self).assembleRefs(context)
try:
- strings = [ str(i.render(context, None)) for i in self._items ]
+ strings = [str(i.render(context, None)) for i in self.contents]
value = "".join(strings)
self._refs.append(value)
except ResolveError as e:
- self._allRefs = False
-
- @property
- def contents(self):
- return self._items
-
- @property
- def allRefs(self):
- return self._allRefs
-
- @property
- def has_references(self):
- return len(self._refs) > 0
-
- def get_references(self):
- return self._refs
+ self.allRefs = False
def _resolve(self, ref, context):
path = DictPath(self._settings.delimiter, ref)
@@ -62,14 +30,13 @@
raise ResolveError(ref)
def render(self, context, inventory):
- if len(self._items) == 1:
- return self._resolve(self._items[0].render(context, inventory), context)
- strings = [ str(i.render(context, inventory)) for i in self._items ]
+ if len(self.contents) == 1:
+ return self._resolve(self.contents[0].render(context, inventory),
+ context)
+ strings = [str(i.render(context, inventory)) for i in self.contents]
return self._resolve("".join(strings), context)
- def __repr__(self):
- return 'RefItem(%r)' % self._items
-
def __str__(self):
- strings = [ str(i) for i in self._items ]
- return '{0}{1}{2}'.format(REFERENCE_SENTINELS[0], ''.join(strings), REFERENCE_SENTINELS[1])
+ strings = [str(i) for i in self.contents]
+ rs = self._settings.reference_sentinels
+ return '{0}{1}{2}'.format(rs[0], ''.join(strings), rs[1])
diff --git a/reclass/values/scaitem.py b/reclass/values/scaitem.py
index c65f302..1bcbd2c 100644
--- a/reclass/values/scaitem.py
+++ b/reclass/values/scaitem.py
@@ -9,29 +9,23 @@
from __future__ import unicode_literals
from reclass.settings import Settings
-from .item import Item
+from reclass.values import item
-class ScaItem(Item):
+
+class ScaItem(item.Item):
+
+ type = item.ItemTypes.SCALAR
def __init__(self, value, settings):
- self.type = Item.SCALAR
- self._value = value
- self._settings = settings
+ super(ScaItem, self).__init__(value, settings)
- @property
- def contents(self):
- return self._value
-
- def merge_over(self, item):
- if item.type == Item.SCALAR or item.type == Item.COMPOSITE:
+ def merge_over(self, other):
+ if other.type in [item.ItemTypes.SCALAR, item.ItemTypes.COMPOSITE]:
return self
- raise RuntimeError('Trying to merge %s over %s' % (repr(self), repr(item)))
+ raise RuntimeError('Failed to merge %s over %s' % (self, other))
def render(self, context, inventory):
- return self._value
-
- def __repr__(self):
- return 'ScaItem({0!r})'.format(self._value)
+ return self.contents
def __str__(self):
- return str(self._value)
+ return str(self.contents)
diff --git a/reclass/values/tests/test_compitem.py b/reclass/values/tests/test_compitem.py
index 3d63d3b..71a6f0e 100644
--- a/reclass/values/tests/test_compitem.py
+++ b/reclass/values/tests/test_compitem.py
@@ -96,7 +96,6 @@
self.assertEquals(result, composite)
-
def test_merge_over_merge_composite(self):
val1 = Value(None, SETTINGS, '')
val2 = Value(None, SETTINGS, '')
@@ -107,32 +106,6 @@
self.assertEquals(result, composite2)
- @unittest.skip("self._value bug")
- def test_merge_over_merge_list_scalar_allowed(self):
- # This nice bunch of lines below breaks merge because fuck you that's
- # why. Seriously so, merger_over simply is not working for Composites
- sets = Settings()
- sets.allow_scalar_override = True
- val1 = Value(None, SETTINGS, '')
- listitem = ListItem([1], SETTINGS)
- composite = CompItem([val1], sets)
-
- result = composite.merge_over(listitem)
-
- self.assertEquals(result, composite2)
-
- @unittest.skip("self._value bug")
- def test_merge_over_merge_list_override_allowed(self):
- sets = Settings()
- sets.allow_none_override = True
- val1 = Value(None, SETTINGS, '')
- listitem = ListItem([1], SETTINGS)
- composite = CompItem([val1], sets)
-
- result = composite.merge_over(listitem)
-
- self.assertEquals(result, composite2)
-
def test_merge_over_merge_list_not_allowed(self):
val1 = Value(None, SETTINGS, '')
listitem = ListItem([1], SETTINGS)
@@ -140,31 +113,6 @@
self.assertRaises(RuntimeError, composite.merge_over, listitem)
-
- @unittest.skip("self._value bug")
- def test_merge_dict_scalar_allowed(self):
- sets = Settings()
- sets.allow_scalar_override = True
- val1 = Value(None, SETTINGS, '')
- dictitem = DictItem({'foo': 'bar'}, SETTINGS)
- composite = CompItem([val1], sets)
-
- result = composite.merge_over(dictitem)
-
- self.assertEquals(result, composite)
-
- @unittest.skip("self._value bug")
- def test_merge_dict_override_allowed(self):
- sets = Settings()
- sets.allow_none_override = True
- val1 = Value(None, SETTINGS, '')
- dictitem = DictItem([1], SETTINGS)
- composite = CompItem([val1], sets)
-
- result = composite.merge_over(dictitem)
-
- self.assertEquals(result, composite)
-
def test_merge_dict_dict_not_allowed(self):
val1 = Value(None, SETTINGS, '')
dictitem = DictItem({'foo': 'bar'}, SETTINGS)
diff --git a/reclass/values/value.py b/reclass/values/value.py
index 613d553..affd944 100644
--- a/reclass/values/value.py
+++ b/reclass/values/value.py
@@ -23,7 +23,7 @@
def __init__(self, value, settings, uri, parse_string=True):
self._settings = settings
self._uri = uri
- self._overwrite = False
+ self.overwrite = False
self._constant = False
if isinstance(value, string_types):
if parse_string:
@@ -42,12 +42,8 @@
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
@property
def constant(self):
@@ -57,10 +53,6 @@
def constant(self, constant):
self._constant = constant
- @property
- def uri(self):
- return self._uri
-
def item_type(self):
return self._item.type
@@ -82,14 +74,15 @@
def has_inv_query(self):
return self._item.has_inv_query
+ @property
def needs_all_envs(self):
if self._item.has_inv_query:
- return self._item.needs_all_envs()
+ return self._item.needs_all_envs
else:
return False
def ignore_failed_render(self):
- return self._item.ignore_failed_render()
+ return self._item.ignore_failed_render
@property
def is_complex(self):
diff --git a/reclass/values/valuelist.py b/reclass/values/valuelist.py
index b4a089d..a56395b 100644
--- a/reclass/values/valuelist.py
+++ b/reclass/values/valuelist.py
@@ -8,31 +8,28 @@
from __future__ import print_function
from __future__ import unicode_literals
-from __future__ import print_function
-
import copy
import sys
from reclass.errors import ChangedConstantError, ResolveError, TypeMergeError
-
class ValueList(object):
def __init__(self, value, settings):
self._settings = settings
self._refs = []
- self._allRefs = True
- self._values = [ value ]
+ self.allRefs = True
+ self._values = [value]
self._inv_refs = []
self._has_inv_query = False
- self._ignore_failed_render = False
+ self.ignore_failed_render = False
self._is_complex = False
self._update()
@property
def uri(self):
- return '; '.join([ str(x.uri) for x in self._values ])
+ return '; '.join([str(x.uri) for x in self._values])
def append(self, value):
self._values.append(value)
@@ -69,34 +66,27 @@
def get_references(self):
return self._refs
- @property
- def allRefs(self):
- return self._allRefs
-
- def ignore_failed_render(self):
- return self._ignore_failed_render
-
def _check_for_inv_query(self):
self._has_inv_query = False
- self._ignore_failed_render = True
+ self.ignore_failed_render = True
for value in self._values:
if value.has_inv_query:
self._inv_refs.extend(value.get_inv_references)
self._has_inv_query = True
if vale.ignore_failed_render() is False:
- self._ignore_failed_render = False
+ self.ignore_failed_render = False
if self._has_inv_query is False:
- self._ignore_failed_render = False
+ self.ignore_failed_render = False
def assembleRefs(self, context={}):
self._refs = []
- self._allRefs = True
+ self.allRefs = True
for value in self._values:
value.assembleRefs(context)
if value.has_references:
self._refs.extend(value.get_references())
if value.allRefs is False:
- self._allRefs = False
+ self.allRefs = False
def merge(self):
output = None
@@ -118,12 +108,17 @@
try:
new = value.render(context, inventory)
except ResolveError as e:
- # only ignore failed renders if ignore_overwritten_missing_references is set and we are dealing with a scalar value
- # and it's not the last item in the values list
- if self._settings.ignore_overwritten_missing_references and not isinstance(output, (dict, list)) and n != (len(self._values)-1):
+ # only ignore failed renders if
+ # ignore_overwritten_missing_references is set and we are
+ # dealing with a scalar value and it's not the last item in the
+ # values list
+ 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("[WARNING] Reference '%s' undefined" % str(value), file=sys.stderr)
+ print("[WARNING] Reference '%s' undefined" % str(value),
+ file=sys.stderr)
else:
raise e
@@ -190,6 +185,3 @@
raise last_error
return output
-
- def __repr__(self):
- return 'ValueList(%r)' % self._values
diff --git a/requirements.txt b/requirements.txt
index 66f0f4b..5b3aadd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
pyparsing
pyyaml
-pygit2
six
+enum34
diff --git a/setup.py b/setup.py
index 789b0fd..884be88 100644
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,7 @@
url = URL,
packages = find_packages(exclude=['*tests']), #FIXME validate this
entry_points = { 'console_scripts': console_scripts },
- install_requires = ['pyparsing', 'pyyaml', 'six'], #FIXME pygit2 (require libffi-dev, libgit2-dev 0.26.x )
+ install_requires = ['pyparsing', 'pyyaml', 'six', 'enum34'], #FIXME pygit2 (require libffi-dev, libgit2-dev 0.26.x )
classifiers=[
'Development Status :: 4 - Beta',