From fd996e9a475475a6107980411b83c99b1c648f0f Mon Sep 17 00:00:00 2001 From: Niels Laukens Date: Tue, 25 Sep 2018 12:35:53 +0200 Subject: [PATCH] Rewrote Lookup-parser The new parser will build the entire AST to support nested lookups. --- stacker/exceptions.py | 42 ++- stacker/stack.py | 28 +- stacker/tests/blueprints/test_base.py | 54 ++-- stacker/tests/lookups/test_registry.py | 38 ++- stacker/tests/test_stack.py | 2 + stacker/tests/test_variables.py | 83 ++--- stacker/variables.py | 422 ++++++++++++++++++++++--- 7 files changed, 488 insertions(+), 181 deletions(-) diff --git a/stacker/exceptions.py b/stacker/exceptions.py index 1602528b6..e1ae8339f 100644 --- a/stacker/exceptions.py +++ b/stacker/exceptions.py @@ -15,17 +15,27 @@ def __init__(self, lookup, lookups, value, *args, **kwargs): message = ( "Lookup: \"{}\" has non-string return value, must be only lookup " "present (not {}) in \"{}\"" - ).format(lookup.raw, len(lookups), value) + ).format(str(lookup), len(lookups), value) super(InvalidLookupCombination, self).__init__(message, *args, **kwargs) +class InvalidLookupConcatenation(Exception): + """ + Intermediary Exception to be converted to InvalidLookupCombination once it + bubbles up there + """ + def __init__(self, lookup, lookups, *args, **kwargs): + self.lookup = lookup + self.lookups = lookups + super(InvalidLookupConcatenation, self).__init__("", *args, **kwargs) + + class UnknownLookupType(Exception): - def __init__(self, lookup, *args, **kwargs): - self.lookup = lookup - message = "Unknown lookup type: \"{}\"".format(lookup.type) + def __init__(self, lookup_type, *args, **kwargs): + message = "Unknown lookup type: \"{}\"".format(lookup_type) super(UnknownLookupType, self).__init__(message, *args, **kwargs) @@ -35,11 +45,22 @@ def __init__(self, variable_name, lookup, error, *args, **kwargs): self.lookup = lookup self.error = error message = "Couldn't resolve lookup in variable `%s`, " % variable_name - message += "lookup: ${%s}: " % lookup.raw + message += "lookup: ${%s}: " % repr(lookup) message += "(%s) %s" % (error.__class__, error) super(FailedVariableLookup, self).__init__(message, *args, **kwargs) +class FailedLookup(Exception): + """ + Intermediary Exception to be converted to FailedVariableLookup once it + bubbles up there + """ + def __init__(self, lookup, error, *args, **kwargs): + self.lookup = lookup + self.error = error + super(FailedLookup, self).__init__("Failed lookup", *args, **kwargs) + + class InvalidUserdataPlaceholder(Exception): def __init__(self, blueprint_name, exception_message, *args, **kwargs): @@ -70,6 +91,17 @@ def __init__(self, blueprint_name, variable, *args, **kwargs): super(UnresolvedVariable, self).__init__(message, *args, **kwargs) +class UnresolvedVariableValue(Exception): + """ + Intermediary Exception to be converted to UnresolvedVariable once it + bubbles up there + """ + def __init__(self, lookup, *args, **kwargs): + self.lookup = lookup + super(UnresolvedVariableValue, self).__init__( + "Unresolved lookup", *args, **kwargs) + + class MissingVariable(Exception): def __init__(self, blueprint_name, variable_name, *args, **kwargs): diff --git a/stacker/stack.py b/stacker/stack.py index a6436265c..127c8c605 100644 --- a/stacker/stack.py +++ b/stacker/stack.py @@ -9,13 +9,8 @@ Variable, resolve_variables, ) -from .lookups.handlers.output import ( - TYPE_NAME as OUTPUT_LOOKUP_TYPE_NAME, - deconstruct, -) from .blueprints.raw import RawTemplateBlueprint -from .exceptions import FailedVariableLookup def _gather_variables(stack_def): @@ -93,22 +88,13 @@ def requires(self): # Add any dependencies based on output lookups for variable in self.variables: - for lookup in variable.lookups: - if lookup.type == OUTPUT_LOOKUP_TYPE_NAME: - - try: - d = deconstruct(lookup.input) - except ValueError as e: - raise FailedVariableLookup(self.name, lookup, e) - - if d.stack_name == self.name: - message = ( - "Variable %s in stack %s has a ciruclar reference " - "within lookup: %s" - ) % (variable.name, self.name, lookup.raw) - raise ValueError(message) - requires.add(d.stack_name) - + deps = variable.dependencies() + if self.name in deps: + message = ( + "Variable %s in stack %s has a ciruclar reference" + ) % (variable.name, self.name) + raise ValueError(message) + requires.update(deps) return requires @property diff --git a/stacker/tests/blueprints/test_base.py b/stacker/tests/blueprints/test_base.py index 141be10c7..52187aaa6 100644 --- a/stacker/tests/blueprints/test_base.py +++ b/stacker/tests/blueprints/test_base.py @@ -41,7 +41,7 @@ from stacker.variables import Variable from stacker.lookups import register_lookup_handler -from ..factories import mock_lookup, mock_context +from ..factories import mock_context def mock_lookup_handler(value, provider=None, context=None, fqn=False, @@ -424,11 +424,8 @@ class TestBlueprint(Blueprint): Variable("Param2", "${output other-stack::Output}"), Variable("Param3", 3), ] - resolved_lookups = { - mock_lookup("other-stack::Output", "output"): "Test Output", - } - for var in variables: - var.replace(resolved_lookups) + + variables[1]._value._resolve("Test Output") blueprint.resolve_variables(variables) self.assertEqual(blueprint.resolved_variables["Param1"], 1) @@ -441,15 +438,14 @@ class TestBlueprint(Blueprint): "Param1": {"type": list}, } + def return_list_something(*_args, **_kwargs): + return ["something"] + + register_lookup_handler("custom", return_list_something) blueprint = TestBlueprint(name="test", context=MagicMock()) variables = [Variable("Param1", "${custom non-string-return-val}")] - lookup = mock_lookup("non-string-return-val", "custom", - "custom non-string-return-val") - resolved_lookups = { - lookup: ["something"], - } for var in variables: - var.replace(resolved_lookups) + var._value.resolve({}, {}) blueprint.resolve_variables(variables) self.assertEqual(blueprint.resolved_variables["Param1"], ["something"]) @@ -460,15 +456,14 @@ class TestBlueprint(Blueprint): "Param1": {"type": Base64}, } + def return_obj(*_args, **_kwargs): + return Base64("test") + + register_lookup_handler("custom", return_obj) blueprint = TestBlueprint(name="test", context=MagicMock()) variables = [Variable("Param1", "${custom non-string-return-val}")] - lookup = mock_lookup("non-string-return-val", "custom", - "custom non-string-return-val") - resolved_lookups = { - lookup: Base64("test"), - } for var in variables: - var.replace(resolved_lookups) + var._value.resolve({}, {}) blueprint.resolve_variables(variables) self.assertEqual(blueprint.resolved_variables["Param1"].data, @@ -480,20 +475,17 @@ class TestBlueprint(Blueprint): "Param1": {"type": list}, } - variables = [ - Variable( - "Param1", - "${custom non-string-return-val},${output some-stack::Output}", - ) - ] - lookup = mock_lookup("non-string-return-val", "custom", - "custom non-string-return-val") - resolved_lookups = { - lookup: ["something"], - } + def return_list_something(*_args, **_kwargs): + return ["something"] + + register_lookup_handler("custom", return_list_something) + variable = Variable( + "Param1", + "${custom non-string-return-val},${output some-stack::Output}", + ) + variable._value[0].resolve({}, {}) with self.assertRaises(InvalidLookupCombination): - for var in variables: - var.replace(resolved_lookups) + variable.value() def test_get_variables(self): class TestBlueprint(Blueprint): diff --git a/stacker/tests/lookups/test_registry.py b/stacker/tests/lookups/test_registry.py index 7ef338013..1dc0b41f1 100644 --- a/stacker/tests/lookups/test_registry.py +++ b/stacker/tests/lookups/test_registry.py @@ -3,19 +3,16 @@ from __future__ import absolute_import import unittest -from mock import patch, MagicMock +from mock import MagicMock from stacker.exceptions import ( UnknownLookupType, FailedVariableLookup, ) -from stacker.lookups.registry import ( - LOOKUP_HANDLERS, - resolve_lookups, -) +from stacker.lookups.registry import LOOKUP_HANDLERS -from stacker.variables import Variable +from stacker.variables import Variable, VariableValueLookup from ..factories import ( mock_context, @@ -43,31 +40,32 @@ def test_autoloaded_lookup_handlers(self): ) def test_resolve_lookups_string_unknown_lookup(self): - variable = Variable("MyVar", "${bad_lookup foo}") - with self.assertRaises(UnknownLookupType): - resolve_lookups(variable, self.ctx, self.provider) + Variable("MyVar", "${bad_lookup foo}") def test_resolve_lookups_list_unknown_lookup(self): - variable = Variable( - "MyVar", [ - "${bad_lookup foo}", "random string", - ] - ) - with self.assertRaises(UnknownLookupType): - resolve_lookups(variable, self.ctx, self.provider) + Variable( + "MyVar", [ + "${bad_lookup foo}", "random string", + ] + ) def resolve_lookups_with_output_handler_raise_valueerror(self, variable): """Mock output handler to throw ValueError, then run resolve_lookups on the given variable. """ mock_handler = MagicMock(side_effect=ValueError("Error")) - with patch.dict(LOOKUP_HANDLERS, {"output": mock_handler}): - with self.assertRaises(FailedVariableLookup) as cm: - resolve_lookups(variable, self.ctx, self.provider) - self.assertIsInstance(cm.exception.error, ValueError) + # find the only lookup in the variable + for value in variable._value: + if isinstance(value, VariableValueLookup): + value.handler = mock_handler + + with self.assertRaises(FailedVariableLookup) as cm: + variable.resolve(self.ctx, self.provider) + + self.assertIsInstance(cm.exception.error, ValueError) def test_resolve_lookups_string_failed_variable_lookup(self): variable = Variable("MyVar", "${output foo::bar}") diff --git a/stacker/tests/test_stack.py b/stacker/tests/test_stack.py index 1e9e309a3..ccdab6622 100644 --- a/stacker/tests/test_stack.py +++ b/stacker/tests/test_stack.py @@ -4,6 +4,7 @@ from mock import MagicMock import unittest +from stacker.lookups import register_lookup_handler from stacker.context import Context from stacker.config import Config from stacker.stack import Stack @@ -20,6 +21,7 @@ def setUp(self): definition=generate_definition("vpc", 1), context=self.context, ) + register_lookup_handler("noop", lambda **kwargs: "test") def test_stack_requires(self): definition = generate_definition( diff --git a/stacker/tests/test_variables.py b/stacker/tests/test_variables.py index 08daf0327..2b1acbc55 100644 --- a/stacker/tests/test_variables.py +++ b/stacker/tests/test_variables.py @@ -13,7 +13,7 @@ from stacker.stack import Stack -from .factories import mock_lookup, generate_definition +from .factories import generate_definition class TestVariables(unittest.TestCase): @@ -24,27 +24,11 @@ def setUp(self): def test_variable_replace_no_lookups(self): var = Variable("Param1", "2") - self.assertEqual(len(var.lookups), 0) - resolved_lookups = { - mock_lookup("fakeStack::FakeOutput", "output"): "resolved", - } - var.replace(resolved_lookups) - self.assertEqual(var.value, "2") - - def test_variable_resolve_no_lookups(self): - var = Variable("Param1", "2") - self.assertEqual(len(var.lookups), 0) - var.resolve(self.context, self.provider) - self.assertTrue(var.resolved) self.assertEqual(var.value, "2") def test_variable_replace_simple_lookup(self): var = Variable("Param1", "${output fakeStack::FakeOutput}") - self.assertEqual(len(var.lookups), 1) - resolved_lookups = { - mock_lookup("fakeStack::FakeOutput", "output"): "resolved", - } - var.replace(resolved_lookups) + var._value._resolve("resolved") self.assertEqual(var.value, "resolved") def test_variable_resolve_simple_lookup(self): @@ -59,32 +43,26 @@ def test_variable_resolve_simple_lookup(self): self.context.get_stack.return_value = stack var = Variable("Param1", "${output fakeStack::FakeOutput}") - self.assertEqual(len(var.lookups), 1) var.resolve(self.context, self.provider) self.assertTrue(var.resolved) self.assertEqual(var.value, "resolved") - self.assertEqual(len(var.lookups), 0) def test_variable_resolve_default_lookup_empty(self): var = Variable("Param1", "${default fakeStack::}") - self.assertEqual(len(var.lookups), 1) var.resolve(self.context, self.provider) self.assertTrue(var.resolved) self.assertEqual(var.value, "") - self.assertEqual(len(var.lookups), 0) def test_variable_replace_multiple_lookups_string(self): var = Variable( "Param1", - "url://${output fakeStack::FakeOutput}@" - "${output fakeStack::FakeOutput2}", + "url://" # 0 + "${output fakeStack::FakeOutput}" # 1 + "@" # 2 + "${output fakeStack::FakeOutput2}", # 3 ) - self.assertEqual(len(var.lookups), 2) - resolved_lookups = { - mock_lookup("fakeStack::FakeOutput", "output"): "resolved", - mock_lookup("fakeStack::FakeOutput2", "output"): "resolved2", - } - var.replace(resolved_lookups) + var._value[1]._resolve("resolved") + var._value[3]._resolve("resolved2") self.assertEqual(var.value, "url://resolved@resolved2") def test_variable_resolve_multiple_lookups_string(self): @@ -93,7 +71,6 @@ def test_variable_resolve_multiple_lookups_string(self): "url://${output fakeStack::FakeOutput}@" "${output fakeStack::FakeOutput2}", ) - self.assertEqual(len(var.lookups), 2) stack = Stack( definition=generate_definition("vpc", 1), @@ -110,23 +87,17 @@ def test_variable_resolve_multiple_lookups_string(self): def test_variable_replace_no_lookups_list(self): var = Variable("Param1", ["something", "here"]) - self.assertEqual(len(var.lookups), 0) - resolved_lookups = { - mock_lookup("fakeStack::FakeOutput", "output"): "resolved", - } - var.replace(resolved_lookups) self.assertEqual(var.value, ["something", "here"]) def test_variable_replace_lookups_list(self): - value = ["something", "${output fakeStack::FakeOutput}", - "${output fakeStack::FakeOutput2}"] + value = ["something", # 0 + "${output fakeStack::FakeOutput}", # 1 + "${output fakeStack::FakeOutput2}" # 2 + ] var = Variable("Param1", value) - self.assertEqual(len(var.lookups), 2) - resolved_lookups = { - mock_lookup("fakeStack::FakeOutput", "output"): "resolved", - mock_lookup("fakeStack::FakeOutput2", "output"): "resolved2", - } - var.replace(resolved_lookups) + + var._value[1]._resolve("resolved") + var._value[2]._resolve("resolved2") self.assertEqual(var.value, ["something", "resolved", "resolved2"]) def test_variable_replace_lookups_dict(self): @@ -135,12 +106,8 @@ def test_variable_replace_lookups_dict(self): "other": "${output fakeStack::FakeOutput2}", } var = Variable("Param1", value) - self.assertEqual(len(var.lookups), 2) - resolved_lookups = { - mock_lookup("fakeStack::FakeOutput", "output"): "resolved", - mock_lookup("fakeStack::FakeOutput2", "output"): "resolved2", - } - var.replace(resolved_lookups) + var._value["something"]._resolve("resolved") + var._value["other"]._resolve("resolved2") self.assertEqual(var.value, {"something": "resolved", "other": "resolved2"}) @@ -157,13 +124,10 @@ def test_variable_replace_lookups_mixed(self): }, } var = Variable("Param1", value) - self.assertEqual(len(var.lookups), 3) - resolved_lookups = { - mock_lookup("fakeStack::FakeOutput", "output"): "resolved", - mock_lookup("fakeStack::FakeOutput2", "output"): "resolved2", - mock_lookup("fakeStack::FakeOutput3", "output"): "resolved3", - } - var.replace(resolved_lookups) + var._value["something"][0]._resolve("resolved") + var._value["here"]["other"]._resolve("resolved2") + var._value["here"]["same"]._resolve("resolved") + var._value["here"]["mixed"][1]._resolve("resolved3") self.assertEqual(var.value, { "something": [ "resolved", @@ -194,11 +158,6 @@ def mock_handler(value, context, provider, **kwargs): "Param1", "${lookup ${lookup ${output fakeStack::FakeOutput}}}", ) - self.assertEqual( - len(var.lookups), - 1, - "should only parse out the first complete lookup first", - ) var.resolve(self.context, self.provider) self.assertTrue(var.resolved) self.assertEqual(var.value, "looked up: looked up: resolved") diff --git a/stacker/variables.py b/stacker/variables.py index c27cdb899..a4b8dc290 100644 --- a/stacker/variables.py +++ b/stacker/variables.py @@ -1,15 +1,20 @@ from __future__ import absolute_import from __future__ import print_function from __future__ import division + +import re + from past.builtins import basestring from builtins import object from string import Template -from .exceptions import InvalidLookupCombination +from .exceptions import InvalidLookupCombination, UnresolvedVariable, \ + UnknownLookupType, FailedVariableLookup, FailedLookup, \ + UnresolvedVariableValue, InvalidLookupConcatenation from .lookups import ( extract_lookups, - resolve_lookups, ) +from .lookups.registry import LOOKUP_HANDLERS class LookupTemplate(Template): @@ -81,54 +86,37 @@ def resolve_variables(variables, context, provider): class Variable(object): - """Represents a variable passed to a stack. Args: name (str): Name of the variable - value (str): Initial value of the variable from the config - + value (any): Initial value of the variable from the config (str, list, + dict) """ def __init__(self, name, value): self.name = name - self._value = value - self._resolved_value = None - - @property - def lookups(self): - """Return any lookups within the value""" - return extract_lookups(self.value) - - @property - def needs_resolution(self): - """Return True if the value has any lookups that need resolving.""" - if self.lookups: - return True - return False + self._raw_value = value + self._value = VariableValue.parse(value) @property def value(self): """Return the current value of the Variable. - - `_resolved_value` takes precedence over `_value`. - """ - if self._resolved_value is not None: - return self._resolved_value - else: - return self._value + try: + return self._value.value() + except UnresolvedVariableValue as e: + raise UnresolvedVariable("", self) + except InvalidLookupConcatenation as e: + raise InvalidLookupCombination(e.lookup, e.lookups, self) @property def resolved(self): """Boolean for whether the Variable has been resolved. Variables only need to be resolved if they contain lookups. - """ - if self.needs_resolution: - return self._resolved_value is not None - return True + return self._value.resolved() def resolve(self, context, provider): """Recursively resolve any lookups with the Variable. @@ -140,21 +128,371 @@ def resolve(self, context, provider): the base provider """ + try: + self._value.resolve(context, provider) + except FailedLookup as e: + raise FailedVariableLookup(self.name, e.lookup, e.error) - while self.lookups: - resolved_lookups = resolve_lookups(self, context, provider) - self.replace(resolved_lookups) + def dependencies(self): + """ + :return: list of stack names that this variable depends on + :rtype: Set[str] + """ + return self._value.dependencies() - def replace(self, resolved_lookups): - """Replace lookups in the Variable with their resolved values. - Args: - resolved_lookups (dict): dict of :class:`stacker.lookups.Lookup` -> - resolved value. +class VariableValue(object): + """ + Abstract Syntax Tree base object to parse the value for a variable + """ + def value(self): + return NotImplementedError() + + def __iter__(self): + return NotImplementedError() + + def resolved(self): + """ + :return: Whether value() will not raise an error + :rtype: bool + """ + return NotImplementedError() + + def resolve(self, context, provider): + pass + + def dependencies(self): + return set() + + def simplified(self): + """ + Return a simplified version of the Value. + This can be used to e.g. concatenate two literals in to one literal, or + to flatten nested Concatenations + :rtype: VariableValue + """ + return self + + @classmethod + def parse(cls, input_object): + if isinstance(input_object, list): + return VariableValueList.parse(input_object) + elif isinstance(input_object, dict): + return VariableValueDict.parse(input_object) + elif not isinstance(input_object, basestring): + return VariableValueLiteral(input_object) + # else: # str + + tokens = VariableValueConcatenation([ + VariableValueLiteral(t) + for t in re.split(r'(\$\{|\}|\s+)', input_object) + ]) + + while True: + last_open = None + next_close = None + for i, t in enumerate(tokens): + if not isinstance(t, VariableValueLiteral): + continue + + if t.value() == '${': + last_open = i + next_close = None + if last_open is not None and \ + t.value() == '}' and \ + next_close is None: + next_close = i + + if next_close is not None: + lookup_data = VariableValueConcatenation( + tokens[(last_open + 3):next_close] + ) + lookup = VariableValueLookup( + lookup_name=tokens[last_open + 1], + lookup_data=lookup_data, + ) + tokens[last_open:(next_close + 1)] = [lookup] + else: + break + + tokens = tokens.simplified() + + return tokens + + +class VariableValueLiteral(VariableValue): + def __init__(self, value): + self._value = value + + def value(self): + return self._value + + def __iter__(self): + yield self + + def resolved(self): + return True + + def __repr__(self): + return "Literal<{}>".format(repr(self._value)) + + +class VariableValueList(VariableValue, list): + @classmethod + def parse(cls, input_object): + acc = [ + VariableValue.parse(obj) + for obj in input_object + ] + return cls(acc) + + def value(self): + return [ + item.value() + for item in self + ] + + def resolved(self): + accumulator = True + for item in self: + accumulator = accumulator and item.resolved() + return accumulator + + def __repr__(self): + return "List[{}]".format(', '.join([repr(value) for value in self])) + + def __iter__(self): + return list.__iter__(self) + + def resolve(self, context, provider): + for item in self: + item.resolve(context, provider) + + def dependencies(self): + deps = set() + for item in self: + deps.update(item.dependencies()) + return deps + + def simplified(self): + return [ + item.simplified() + for item in self + ] + + +class VariableValueDict(VariableValue, dict): + @classmethod + def parse(cls, input_object): + acc = { + k: VariableValue.parse(v) + for k, v in input_object.items() + } + return cls(acc) + + def value(self): + return { + k: v.value() + for k, v in self.items() + } + + def resolved(self): + accumulator = True + for item in self.values(): + accumulator = accumulator and item.resolved() + return accumulator + + def __repr__(self): + return "Dict[{}]".format(', '.join([ + "{}={}".format(k, repr(v)) for k, v in self.items() + ])) + + def __iter__(self): + return dict.__iter__(self) + + def resolve(self, context, provider): + for item in self.values(): + item.resolve(context, provider) + + def dependencies(self): + deps = set() + for item in self.values(): + deps.update(item.dependencies()) + return deps + + def simplified(self): + return { + k: v.simplified() + for k, v in self.items() + } + + +class VariableValueConcatenation(VariableValue, list): + def value(self): + if len(self) == 1: + return self[0].value() + + values = [] + for value in self: + resolved_value = value.value() + if not isinstance(resolved_value, basestring): + raise InvalidLookupConcatenation(value, self) + values.append(resolved_value) + return ''.join(values) + + def __iter__(self): + return list.__iter__(self) + def resolved(self): + accumulator = True + for item in self: + accumulator = accumulator and item.resolved() + return accumulator + + def __repr__(self): + return "Concat[{}]".format(', '.join([repr(value) for value in self])) + + def resolve(self, context, provider): + for value in self: + value.resolve(context, provider) + + def dependencies(self): + deps = set() + for item in self: + deps.update(item.dependencies()) + return deps + + def simplified(self): + concat = [] + for item in self: + if isinstance(item, VariableValueLiteral) and \ + item.value() == '': + pass + + elif isinstance(item, VariableValueLiteral) and \ + len(concat) > 0 and \ + isinstance(concat[-1], VariableValueLiteral): + # Join the literals together + concat[-1] = VariableValueLiteral( + concat[-1].value() + item.value() + ) + + elif isinstance(item, VariableValueConcatenation): + # Flatten concatenations + concat.extend(item.simplified()) + + else: + concat.append(item.simplified()) + + if len(concat) == 0: + return VariableValueLiteral('') + elif len(concat) == 1: + return concat[0] + else: + return VariableValueConcatenation(concat) + + +class VariableValueLookup(VariableValue): + def __init__(self, lookup_name, lookup_data, handler=None): + """ + :param lookup_name: Name of the invoked lookup + :type lookup_name: basestring + :param lookup_data: Data portion of the lookup + :type lookup_data: VariableValue """ - replacements = {} - for lookup, value in resolved_lookups.items(): - replacements[lookup.raw] = value + self._resolved = False + self._value = None + + self.lookup_name = lookup_name + + if isinstance(lookup_data, basestring): + lookup_data = VariableValueLiteral(lookup_data) + self.lookup_data = lookup_data + + if handler is None: + lookup_name_resolved = lookup_name.value() + try: + handler = LOOKUP_HANDLERS[lookup_name_resolved] + except KeyError as e: + raise UnknownLookupType(lookup_name_resolved) + self.handler = handler - self._resolved_value = resolve(self.value, replacements) + def resolve(self, context, provider): + self.lookup_data.resolve(context, provider) + try: + self._resolve(self.handler( + value=self.lookup_data.value(), + context=context, + provider=provider + )) + except Exception as e: + raise FailedLookup(self, e) + + def _resolve(self, value): + self._value = value + self._resolved = True + + def dependencies(self): + if self.lookup_name.resolved() and \ + self.lookup_name.value() == 'output': + # TODO: move this code in to the Output-lookup itself, + # in order to make it generic + + # try to get the stack name + stack_name = '' + for data_item in self.lookup_data: + if not data_item.resolved(): + # We encountered an unresolved substitution. + # StackName is calculated dynamically based on context: + # e.g. ${output ${default var::source}::name} + # Stop here + return set() + stack_name = stack_name + data_item.value() + match = re.search(r'::', stack_name) + if match: + stack_name = stack_name[0:match.start()] + return set([stack_name]) + # else: try to append the next item + + # We added all lookup_data, and still couldn't find a `::`... + # Probably an error... + return set() + + return set() + + def value(self): + if self._resolved: + return self._value + else: + raise UnresolvedVariableValue(self) + + def __iter__(self): + yield self + + def resolved(self): + return self._resolved + + def __repr__(self): + if self._resolved: + return "Lookup<{r} ({t} {d})>".format( + r=self._value, + t=self.lookup_name, + d=repr(self.lookup_data), + ) + else: + return "Lookup<{t} {d}>".format( + t=self.lookup_name, + d=repr(self.lookup_data), + ) + + def __str__(self): + return "${{{type} {data}}}".format( + type=self.lookup_name.value(), + data=self.lookup_data.value(), + ) + + def simplified(self): + return VariableValueLookup( + lookup_name=self.lookup_name, + lookup_data=self.lookup_data.simplified(), + )