diff --git a/runway/cfngin/blueprints/base.py b/runway/cfngin/blueprints/base.py index 40d643254..aecd89e40 100644 --- a/runway/cfngin/blueprints/base.py +++ b/runway/cfngin/blueprints/base.py @@ -11,8 +11,8 @@ from ..exceptions import ( InvalidUserdataPlaceholder, MissingVariable, - UnresolvedVariable, - UnresolvedVariables, + UnresolvedBlueprintVariable, + UnresolvedBlueprintVariables, ValidatorError, VariableTypeRequired, ) @@ -188,8 +188,8 @@ def resolve_variable(var_name, var_def, provided_variable, blueprint_name): Raises: MissingVariable: Raised when a variable with no default is not provided a value. - UnresolvedVariable: Raised when the provided variable is not already - resolved. + UnresolvedBlueprintVariable: Raised when the provided variable is + not already resolved. ValueError: Raised when the value is not the right type and cannot be cast as the correct type. Raised by :func:`runway.cfngin.blueprints.base.validate_variable_type` @@ -204,7 +204,7 @@ def resolve_variable(var_name, var_def, provided_variable, blueprint_name): if provided_variable: if not provided_variable.resolved: - raise UnresolvedVariable(blueprint_name, provided_variable) + raise UnresolvedBlueprintVariable(blueprint_name, provided_variable) value = provided_variable.value else: @@ -425,11 +425,11 @@ def get_variables(self): Dict[str, Variable]: Variables available to the template. Raises: - UnresolvedVariables: If variables are unresolved. + UnresolvedBlueprintVariables: If variables are unresolved. """ if self.resolved_variables is None: - raise UnresolvedVariables(self.name) + raise UnresolvedBlueprintVariables(self.name) return self.resolved_variables def get_cfn_parameters(self): diff --git a/runway/cfngin/blueprints/raw.py b/runway/cfngin/blueprints/raw.py index 792f6117b..572b54141 100644 --- a/runway/cfngin/blueprints/raw.py +++ b/runway/cfngin/blueprints/raw.py @@ -6,7 +6,7 @@ from jinja2 import Template -from ..exceptions import InvalidConfig, UnresolvedVariable +from ..exceptions import InvalidConfig, UnresolvedBlueprintVariable from ..util import parse_cloudformation_template from .base import Blueprint @@ -67,14 +67,14 @@ def resolve_variable(provided_variable, blueprint_name): object: The resolved variable string value. Raises: - UnresolvedVariable: Raised when the provided variable is not already - resolved. + UnresolvedBlueprintVariable: Raised when the provided variable is + not already resolved. """ value = None if provided_variable: if not provided_variable.resolved: - raise UnresolvedVariable(blueprint_name, provided_variable) + raise UnresolvedBlueprintVariable(blueprint_name, provided_variable) value = provided_variable.value diff --git a/runway/cfngin/exceptions.py b/runway/cfngin/exceptions.py index ceafebb40..1ffed87d7 100644 --- a/runway/cfngin/exceptions.py +++ b/runway/cfngin/exceptions.py @@ -21,55 +21,6 @@ def __init__(self, change_set_id): super().__init__(message) -class FailedLookup(Exception): - """Intermediary Exception to be converted to FailedVariableLookup. - - Should be caught by error handling and - :class:`runway.cfngin.exceptions.FailedVariableLookup` raised instead to - construct a propper error message. - - """ - - def __init__(self, lookup, error, *args, **kwargs): - """Instantiate class. - - Args: - lookup (:class:`runway.cfngin.variables.VariableValueLookup`): - Attempted lookup and resulted in an exception being raised. - error (Exception): The exception that was raised. - - """ - self.lookup = lookup - self.error = error - super().__init__("Failed lookup", *args, **kwargs) - - -class FailedVariableLookup(Exception): - """Lookup could not be resolved. - - Raised when an exception is raised when trying to resolve a lookup. - - """ - - def __init__(self, variable_name, lookup, error, *args, **kwargs): - """Instantiate class. - - Args: - variable_name (str): Name of the variable that failed to be - resolved. - lookup (:class:`runway.cfngin.variables.VariableValueLookup`): - Attempted lookup and resulted in an exception being raised. - error (Exception): The exception that was raised. - - """ - self.lookup = lookup - self.error = error - message = "Couldn't resolve lookup in variable `%s`, " % variable_name - message += "lookup: ${%s}: " % repr(lookup) - message += "(%s) %s" % (error.__class__, error) - super().__init__(message, *args, **kwargs) - - class GraphError(Exception): """Raised when the graph is invalid (e.g. acyclic dependencies).""" @@ -137,43 +88,6 @@ def __init__(self, msg): super().__init__(self.message) -class InvalidLookupCombination(Exception): - """Improper use of lookups to result in a non-string return value.""" - - def __init__(self, lookup, lookups, value, *args, **kwargs): - """Instantiate class. - - Args: - lookup (:class:`runway.cfngin.variables.VariableValueLookup`): The - variable lookup that was attempted but did not return a string. - lookups (:class:`runway.cfngin.variables.VariableValueConcatenation`): - The full variable concatenation the failing lookup is part of. - value (Any): The non-string value returned by lookup. - - """ - message = ( - 'Lookup: "{}" has non-string return value, must be only lookup ' - 'present (not {}) in "{}"' - ).format(str(lookup), len(lookups), value) - super().__init__(message, *args, **kwargs) - - -class InvalidLookupConcatenation(Exception): - """Intermediary Exception to be converted to InvalidLookupCombination. - - Should be caught by error handling and - :class:`runway.cfngin.exceptions.InvalidLookupCombination` raised instead - to construct a propper error message. - - """ - - def __init__(self, lookup, lookups, *args, **kwargs): - """Instantiate class.""" - self.lookup = lookup - self.lookups = lookups - super().__init__("", *args, **kwargs) - - class InvalidUserdataPlaceholder(Exception): """Raised when a placeholder name in raw_user_data is not valid. @@ -248,24 +162,6 @@ def __init__(self, blueprint_name, variable_name, *args, **kwargs): super().__init__(message, *args, **kwargs) -class OutputDoesNotExist(Exception): - """Raised when a specific stack output does not exist.""" - - def __init__(self, stack_name, output, *args, **kwargs): - """Instantiate class. - - Args: - stack_name (str): Name of the stack. - output (str): The output that does not exist. - - """ - self.stack_name = stack_name - self.output = output - - message = "Output %s does not exist on stack %s" % (output, stack_name) - super().__init__(message, *args, **kwargs) - - class PipError(Exception): """Raised when pip returns a non-zero exit code.""" @@ -500,27 +396,7 @@ def __init__(self, stack_name, change_set_id, status, status_reason): super().__init__(message) -class UnknownLookupType(Exception): - """Lookup type provided does not match a registered lookup. - - Example: - If a lookup of ``${ query}`` is used and ```` - is not a registered lookup, this exception will be raised. - - """ - - def __init__(self, lookup_type, *args, **kwargs): - """Instantiate class. - - Args: - lookup_type (str): Lookup type that was used but not registered. - - """ - message = 'Unknown lookup type: "{}"'.format(lookup_type) - super().__init__(message, *args, **kwargs) - - -class UnresolvedVariable(Exception): +class UnresolvedBlueprintVariable(Exception): # TODO rename for blueprints only """Raised when trying to use a variable before it has been resolved.""" def __init__(self, blueprint_name, variable, *args, **kwargs): @@ -539,28 +415,7 @@ def __init__(self, blueprint_name, variable, *args, **kwargs): super().__init__(message, *args, **kwargs) -class UnresolvedVariableValue(Exception): - """Intermediary Exception to be converted to UnresolvedVariable. - - Should be caught by error handling and - :class:`runway.cfngin.exceptions.UnresolvedVariable` raised instead to - construct a propper error message. - - """ - - def __init__(self, lookup, *args, **kwargs): - """Instantiate class. - - Args: - lookup (:class:`runway.cfngin.variables.VariableValueLookup`): The - lookup that is not resolved. - - """ - self.lookup = lookup - super().__init__("Unresolved lookup", *args, **kwargs) - - -class UnresolvedVariables(Exception): +class UnresolvedBlueprintVariables(Exception): # TODO rename for blueprints only """Raised when trying to use variables before they has been resolved.""" def __init__(self, blueprint_name, *args, **kwargs): diff --git a/runway/cfngin/hooks/utils.py b/runway/cfngin/hooks/utils.py index 19e58ba78..92a284b3d 100644 --- a/runway/cfngin/hooks/utils.py +++ b/runway/cfngin/hooks/utils.py @@ -8,8 +8,8 @@ from runway.util import load_object_from_string from runway.variables import Variable, resolve_variables +from ...exceptions import FailedVariableLookup from ..blueprints.base import Blueprint -from ..exceptions import FailedVariableLookup LOGGER = logging.getLogger(__name__) diff --git a/runway/cfngin/lookups/registry.py b/runway/cfngin/lookups/registry.py index 1181b8851..bc1010143 100644 --- a/runway/cfngin/lookups/registry.py +++ b/runway/cfngin/lookups/registry.py @@ -4,7 +4,7 @@ from runway.lookups.handlers import cfn, ssm from runway.util import DOC_SITE, load_object_from_string -from ..exceptions import FailedVariableLookup, UnknownLookupType +from ...exceptions import FailedVariableLookup, UnknownLookupType from .handlers import ami, default, dynamodb, envvar from .handlers import file as file_handler from .handlers import hook_data, kms, output, rxref, split, ssmstore, xref diff --git a/runway/core/components/_deployment.py b/runway/core/components/_deployment.py index 7d265fbe9..bf09c21a7 100644 --- a/runway/core/components/_deployment.py +++ b/runway/core/components/_deployment.py @@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from ..._logging import PrefixAdaptor -from ...cfngin.exceptions import UnresolvedVariable from ...config import FutureDefinition, VariablesDefinition +from ...exceptions import UnresolvedVariable from ...util import cached_property, merge_dicts, merge_nested_environment_dicts from ..providers import aws from ._module import Module diff --git a/runway/exceptions.py b/runway/exceptions.py new file mode 100644 index 000000000..6e413d900 --- /dev/null +++ b/runway/exceptions.py @@ -0,0 +1,179 @@ +"""Runway exceptions.""" +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .variables import ( + Variable, + VariableValue, + VariableValueConcatenation, + VariableValueLookup, + ) + + +class FailedLookup(Exception): + """Intermediary Exception to be converted to FailedVariableLookup. + + Should be caught by error handling and + :class:`runway.cfngin.exceptions.FailedVariableLookup` raised instead to + construct a propper error message. + + """ + + lookup: "VariableValueLookup" + cause: Exception + + def __init__( + self, lookup: "VariableValueLookup", cause: Exception, *args: Any, **kwargs: Any + ) -> None: + """Instantiate class. + + Args: + lookup: The variable value lookup that was attempted and + resulted in an exception being raised. + cause: The exception that was raised. + + """ + self.lookup = lookup + self.cause = cause + super().__init__("Failed lookup", *args, **kwargs) + + +class FailedVariableLookup(Exception): + """Lookup could not be resolved. + + Raised when an exception is raised when trying to resolve a lookup. + + """ + + lookup: "Variable" + cause: FailedLookup + + def __init__( + self, + variable: "Variable", + lookup_error: FailedLookup, + *args: Any, + **kwargs: Any + ) -> None: + """Instantiate class. + + Args: + variable: The variable containing the failed lookup. + lookup_error: The exception that was raised directly before this one. + + """ + self.variable = variable + self.cause = lookup_error + super().__init__( + f'Could not resolve lookup "{lookup_error.lookup}" for variable "{variable.name}"', + *args, + **kwargs + ) + + +class InvalidLookupConcatenation(Exception): + """Invalid return value for a concatinated (chained) lookup. + + The return value must be a string when lookups are concatinated. + + """ + + concatenated_lookups: "VariableValueConcatenation" + invalid_lookup: "VariableValue" + + def __init__( + self, + invalid_lookup: "VariableValue", + concat_lookups: "VariableValueConcatenation", + *args: Any, + **kwargs: Any + ) -> None: + """Instantiate class.""" + self.concatenated_lookups = concat_lookups + self.invalid_lookup = invalid_lookup + message = ( + f"expected return value of type {str} but received " + f'{type(invalid_lookup.value)} for lookup "{invalid_lookup}" ' + f'in "{concat_lookups}"' + ) + super().__init__(message, *args, **kwargs) + + +class OutputDoesNotExist(Exception): + """Raised when a specific stack output does not exist.""" + + def __init__(self, stack_name, output, *args, **kwargs): + """Instantiate class. + + Args: + stack_name (str): Name of the stack. + output (str): The output that does not exist. + + """ + self.stack_name = stack_name + self.output = output + + message = "Output %s does not exist on stack %s" % (output, stack_name) + super().__init__(message, *args, **kwargs) + + +class UnknownLookupType(Exception): + """Lookup type provided does not match a registered lookup. + + Example: + If a lookup of ``${ query}`` is used and ```` + is not a registered lookup, this exception will be raised. + + """ + + def __init__( + self, lookup: "VariableValueLookup", *args: Any, **kwargs: Any + ) -> None: + """Instantiate class. + + Args: + lookup: Variable value lookup that could not find a handler. + + """ + message = f'Unknown lookup type "{lookup.lookup_name.value}" in "{lookup}"' + super().__init__(message, *args, **kwargs) + + +class UnresolvedVariable(Exception): + """Raised when trying to use a variable before it has been resolved.""" + + def __init__(self, variable: "Variable", *args: Any, **kwargs: Any) -> None: + """Instantiate class. + + Args: + variable: The unresolved variable. + + """ + message = 'Attempted to use variable "{}" before it was resolved'.format( + variable.name + ) + super().__init__(message, *args, **kwargs) + + +class UnresolvedVariableValue(Exception): + """Intermediary Exception to be converted to UnresolvedVariable. + + Should be caught by error handling and + :class:`runway.cfngin.exceptions.UnresolvedVariable` raised instead to + construct a propper error message. + + """ + + lookup: "VariableValueLookup" + + def __init__( + self, lookup: "VariableValueLookup", *args: Any, **kwargs: Any + ) -> None: + """Instantiate class. + + Args: + lookup: The variable value lookup that is not resolved. + + """ + self.lookup = lookup + super().__init__("Unresolved lookup", *args, **kwargs) diff --git a/runway/lookups/handlers/cfn.py b/runway/lookups/handlers/cfn.py index e402e398a..e04a29b65 100644 --- a/runway/lookups/handlers/cfn.py +++ b/runway/lookups/handlers/cfn.py @@ -49,7 +49,8 @@ from botocore.exceptions import ClientError -from runway.cfngin.exceptions import OutputDoesNotExist, StackDoesNotExist +from runway.cfngin.exceptions import StackDoesNotExist +from runway.exceptions import OutputDoesNotExist from .base import LookupHandler diff --git a/runway/module/cloudformation.py b/runway/module/cloudformation.py index 5f9e91d23..398e7c021 100644 --- a/runway/module/cloudformation.py +++ b/runway/module/cloudformation.py @@ -2,7 +2,7 @@ import logging from .._logging import PrefixAdaptor -from ..cfngin import CFNgin +from ..cfngin.cfngin import CFNgin from . import RunwayModule LOGGER = logging.getLogger(__name__) diff --git a/runway/variables.py b/runway/variables.py index e3f119c20..1e9729c42 100644 --- a/runway/variables.py +++ b/runway/variables.py @@ -9,50 +9,38 @@ Iterator, List, Optional, + Set, Type, Union, cast, ) -from .cfngin.exceptions import ( +from .cfngin.lookups.registry import CFNGIN_LOOKUP_HANDLERS +from .exceptions import ( FailedLookup, FailedVariableLookup, - InvalidLookupCombination, InvalidLookupConcatenation, UnknownLookupType, UnresolvedVariable, UnresolvedVariableValue, ) -from .cfngin.lookups.registry import CFNGIN_LOOKUP_HANDLERS from .lookups.handlers.base import LookupHandler from .lookups.registry import RUNWAY_LOOKUP_HANDLERS if TYPE_CHECKING: + from .cfngin.context import Context as CFNginContext + from .cfngin.providers.base import BaseProvider from .config import VariablesDefinition + from .context import Context as RunwayContext LOGGER = logging.getLogger(__name__) -def resolve_variables(variables, context, provider): - """Given a list of variables, resolve all of them. - - Args: - variables (List[:class:`Variable`]): List of variables. - context (:class:`runway.cfngin.context.Context`): CFNgin context. - provider (:class:`runway.cfngin.providers.base.BaseProvider`): Subclass - of the base provider. - - """ - for variable in variables: - variable.resolve(context=context, provider=provider) - - class Variable: """Represents a variable provided to a Runway directive.""" - def __init__(self, name, value, variable_type="cfngin"): - # type: (str, Any, str) -> None + def __init__(self, name: str, value: Any, variable_type: str = "cfngin") -> None: """Initialize class. Args: @@ -67,8 +55,7 @@ def __init__(self, name, value, variable_type="cfngin"): LOGGER.debug("initalized variable: %s", name) @property - def dependencies(self): - # () -> Set[str] + def dependencies(self) -> Set[str]: """Stack names that this variable depends on. Returns: @@ -78,8 +65,7 @@ def dependencies(self): return self._value.dependencies @property - def resolved(self): - # type: () -> bool + def resolved(self) -> bool: """Boolean for whether the Variable has been resolved. Variables only need to be resolved if they contain lookups. @@ -88,18 +74,25 @@ def resolved(self): return self._value.resolved @property - def value(self): - # type: () -> Any - """Return the current value of the Variable.""" + def value(self) -> Any: + """Return the current value of the Variable. + + Raises: + UnresolvedVariable: Value accessed before it have been resolved. + + """ try: return self._value.value except UnresolvedVariableValue: - raise UnresolvedVariable("", self) - except InvalidLookupConcatenation as err: - raise InvalidLookupCombination(err.lookup, err.lookups, self) + raise UnresolvedVariable(self) from None - def resolve(self, context, provider=None, variables=None, **kwargs): - # type: (Any, Any, 'Optional[VariablesDefinition]', Any) -> None + def resolve( + self, + context: Union["CFNginContext", "RunwayContext"], + provider: Optional["BaseProvider"] = None, + variables: Optional["VariablesDefinition"] = None, + **kwargs: Any + ) -> None: """Resolve the variable value. Args: @@ -107,16 +100,18 @@ def resolve(self, context, provider=None, variables=None, **kwargs): provider: Subclass of the base provider. variables: Object containing variables passed to Runway. + Raises: + FailedVariableLookup + """ try: self._value.resolve( context, provider=provider, variables=variables, **kwargs ) except FailedLookup as err: - raise FailedVariableLookup(self.name, err.lookup, err.error) + raise FailedVariableLookup(self, err) from err.cause - def get(self, key, default=None): - # type: (Any, Any) -> Any + def get(self, key: str, default: Any = None) -> Any: """Implement evaluation of self.get. Args: @@ -126,34 +121,48 @@ def get(self, key, default=None): """ return getattr(self.value, key, default) - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: """Return object representation.""" return "Variable<{}={}>".format(self.name, self._raw_value) +def resolve_variables( + variables: List[Variable], + context: Union["CFNginContext", "RunwayContext"], + provider: "BaseProvider", +) -> None: + """Given a list of variables, resolve all of them. + + Args: + variables: List of variables. + context: CFNgin context. + provider: Subclass of the base provider. + + """ + for variable in variables: + variable.resolve(context=context, provider=provider) + + class VariableValue: """Syntax tree base class to parse variable values.""" @property - def dependencies(self): - # () -> Set[] + def dependencies(self) -> Set: """Stack names that this variable depends on.""" return set() @property - def resolved(self): - # type: () -> bool + def resolved(self) -> bool: """Use to check if the variable value has been resolved. - Should be implimented in subclasses. + Raises: + NotImplementedError: Should be defined in a subclass. """ raise NotImplementedError @property - def simplified(self): - # type: () -> Any + def simplified(self) -> Any: """Return a simplified version of the value. This can be used to concatenate two literals into one literal or @@ -165,17 +174,22 @@ def simplified(self): return self @property - def value(self): - # type: () -> Any + def value(self) -> Any: """Value of the variable. Can be resolved or unresolved. - Should be implimented in subclasses. + Raises: + NotImplementedError: Should be defined in a subclass. """ raise NotImplementedError - def resolve(self, context, provider=None, variables=None, **kwargs): - # type: (Any, Any, 'Optional[VariablesDefinition]', Any) -> None + def resolve( + self, + context: Union["CFNginContext", "RunwayContext"], + provider: Optional["BaseProvider"] = None, + variables: Optional["VariablesDefinition"] = None, + **kwargs: Any + ) -> None: """Resolve the variable value. Args: @@ -186,8 +200,7 @@ def resolve(self, context, provider=None, variables=None, **kwargs): """ @classmethod - def parse(cls, input_object, variable_type="cfngin"): - # type: (Any, str) -> Any + def parse(cls, input_object: Any, variable_type: str = "cfngin") -> Any: """Parse complex variable structures using type appropriate subclasses. Args: @@ -240,20 +253,20 @@ def parse(cls, input_object, variable_type="cfngin"): return tokens.simplified - def __iter__(self): - # type: () -> Iterable + def __iter__(self) -> Iterable: """How the object is iterated. - Should be implimented in subclasses. + Raises: + NotImplementedError: Should be defined in a subclass. """ raise NotImplementedError - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: """Return object representation. - Should be implimented in subclasses. + Raises: + NotImplementedError: Should be defined in a subclass. """ raise NotImplementedError @@ -262,14 +275,12 @@ def __repr__(self): class VariableValueLiteral(VariableValue): """The literal value of a variable as provided.""" - def __init__(self, value): - # type: (Any) -> None + def __init__(self, value: Any) -> None: """Initialize class.""" self._value = value @property - def resolved(self): - # type: () -> bool + def resolved(self) -> bool: """Use to check if the variable value has been resolved. The ValueLiteral will always appear as resolved because it does @@ -279,18 +290,15 @@ def resolved(self): return True @property - def value(self): - # type: () -> Any + def value(self) -> Any: """Value of the variable.""" return self._value - def __iter__(self): - # type: () -> Iterable[Any] + def __iter__(self) -> Iterable[Any]: """How the object is iterated.""" yield self - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: """Return object representation.""" return "Literal<{}>".format(repr(self._value)) @@ -299,8 +307,7 @@ class VariableValueList(VariableValue, list): """A list variable value.""" @property - def dependencies(self): - # () -> Set[str] + def dependencies(self) -> Set[str]: """Stack names that this variable depends on.""" deps = set() for item in self: @@ -308,8 +315,7 @@ def dependencies(self): return deps @property - def resolved(self): - # type: () -> bool + def resolved(self) -> bool: """Use to check if the variable value has been resolved.""" accumulator = True for item in self: @@ -317,8 +323,7 @@ def resolved(self): return accumulator @property - def simplified(self): - # type: () -> List[VariableValue] + def simplified(self) -> List[VariableValue]: """Return a simplified version of the value. This can be used to concatenate two literals into one literal or @@ -328,13 +333,17 @@ def simplified(self): return [item.simplified for item in self] @property - def value(self): - # type: () -> List[Any] + def value(self) -> List[Any]: """Value of the variable. Can be resolved or unresolved.""" return [item.value for item in self] - def resolve(self, context, provider=None, variables=None, **kwargs): - # type: (Any, Any, 'Optional[VariablesDefinition]', Any) -> None + def resolve( + self, + context: Union["CFNginContext", "RunwayContext"], + provider: Optional["BaseProvider"] = None, + variables: Optional["VariablesDefinition"] = None, + **kwargs: Any + ) -> None: """Resolve the variable value. Args: @@ -347,8 +356,9 @@ def resolve(self, context, provider=None, variables=None, **kwargs): item.resolve(context, provider=provider, variables=variables, **kwargs) @classmethod - def parse(cls, input_object, variable_type="cfngin"): - # type: (Any, str) -> VariableValueList + def parse( + cls, input_object: Iterable[Any], variable_type: str = "cfngin" + ) -> "VariableValueList": """Parse list variable structure. Args: @@ -359,13 +369,11 @@ def parse(cls, input_object, variable_type="cfngin"): acc = [VariableValue.parse(obj, variable_type) for obj in input_object] return cls(acc) - def __iter__(self): - # type: () -> Iterator[Any] + def __iter__(self) -> Iterator[Any]: """How the object is iterated.""" return list.__iter__(self) - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: """Return object representation.""" return "List[{}]".format(", ".join([repr(value) for value in self])) @@ -374,8 +382,7 @@ class VariableValueDict(VariableValue, dict): """A dict variable value.""" @property - def dependencies(self): - # () -> Set[str] + def dependencies(self) -> Set[str]: """Stack names that this variable depends on.""" deps = set() for item in self.values(): @@ -383,8 +390,7 @@ def dependencies(self): return deps @property - def resolved(self): - # type: () -> bool + def resolved(self) -> bool: """Use to check if the variable value has been resolved.""" accumulator = True for item in self.values(): @@ -392,8 +398,7 @@ def resolved(self): return accumulator @property - def simplified(self): - # type: () -> Dict[str, VariableValue] + def simplified(self) -> Dict[str, Any]: """Return a simplified version of the value. This can be used to concatenate two literals into one literal or @@ -403,13 +408,17 @@ def simplified(self): return {k: v.simplified for k, v in self.items()} @property - def value(self): - # type: () -> Dict[str, Any] + def value(self) -> Dict[str, Any]: """Value of the variable. Can be resolved or unresolved.""" return {k: v.value for k, v in self.items()} - def resolve(self, context, provider=None, variables=None, **kwargs): - # type: (Any, Any, 'Optional[VariablesDefinition]', Any) -> None + def resolve( + self, + context: Union["CFNginContext", "RunwayContext"], + provider: Optional["BaseProvider"] = None, + variables: Optional["VariablesDefinition"] = None, + **kwargs: Any + ) -> None: """Resolve the variable value. Args: @@ -422,8 +431,9 @@ def resolve(self, context, provider=None, variables=None, **kwargs): item.resolve(context, provider=provider, variables=variables, **kwargs) @classmethod - def parse(cls, input_object, variable_type="cfngin"): - # type: (Any, str) -> VariableValueDict + def parse( + cls, input_object: Any, variable_type: str = "cfngin" + ) -> "VariableValueDict": """Parse list variable structure. Args: @@ -436,13 +446,11 @@ def parse(cls, input_object, variable_type="cfngin"): } return cls(acc) - def __iter__(self): - # type: () -> Iterator[Any] + def __iter__(self) -> Iterator[Any]: """How the object is iterated.""" return dict.__iter__(self) - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: """Return object representation.""" return "Dict[{}]".format( ", ".join(["{}={}".format(k, repr(v)) for k, v in self.items()]) @@ -453,7 +461,7 @@ class VariableValueConcatenation(VariableValue, list): """A concatinated variable value.""" @property - def dependencies(self): + def dependencies(self) -> Set[str]: """Stack names that this variable depends on.""" deps = set() for item in self: @@ -461,8 +469,7 @@ def dependencies(self): return deps @property - def resolved(self): - # type: () -> bool + def resolved(self) -> bool: """Use to check if the variable value has been resolved.""" accumulator = True for item in self: @@ -470,15 +477,16 @@ def resolved(self): return accumulator @property - def simplified(self): - # type: () -> Union[Type[VariableValue], VariableValueConcatenation, VariableValueLiteral] + def simplified( + self, + ) -> Union[VariableValue, "VariableValueConcatenation", VariableValueLiteral]: """Return a simplified version of the value. This can be used to concatenate two literals into one literal or flatten nested concatenations. """ - concat = [] # type: List[Type[VariableValue]] + concat: List[VariableValue] = [] for item in self: if isinstance(item, VariableValueLiteral) and item.value == "": pass @@ -496,7 +504,7 @@ def simplified(self): concat.extend(item.simplified) else: - concat.append(cast(Type[VariableValue], item.simplified)) + concat.append(cast(VariableValue, item.simplified)) if not concat: return VariableValueLiteral("") @@ -505,13 +513,17 @@ def simplified(self): return VariableValueConcatenation(concat) @property - def value(self): - # type: () -> Any - """Value of the variable. Can be resolved or unresolved.""" + def value(self) -> Any: + """Value of the variable. Can be resolved or unresolved. + + Raises: + InvalidLookupConcatenation + + """ if len(self) == 1: return self[0].value - values = [] # type: List[str] + values: List[str] = [] for value in self: resolved_value = value.value if not isinstance(resolved_value, str): @@ -519,8 +531,13 @@ def value(self): values.append(resolved_value) return "".join(values) - def resolve(self, context, provider=None, variables=None, **kwargs): - # type: (Any, Any, 'Optional[VariablesDefinition]', Any) -> None + def resolve( + self, + context: Union["CFNginContext", "RunwayContext"], + provider: Optional["BaseProvider"] = None, + variables: Optional["VariablesDefinition"] = None, + **kwargs: Any + ) -> None: """Resolve the variable value. Args: @@ -532,13 +549,11 @@ def resolve(self, context, provider=None, variables=None, **kwargs): for value in self: value.resolve(context, provider=provider, variables=variables, **kwargs) - def __iter__(self): - # type: () -> Iterator[Type[VariableValue]] + def __iter__(self) -> Iterator[VariableValue]: """How the object is iterated.""" return list.__iter__(self) - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: """Return object representation.""" return "Concatenation[{}]".format(", ".join([repr(value) for value in self])) @@ -548,12 +563,11 @@ class VariableValueLookup(VariableValue): def __init__( self, - lookup_name, # type: VariableValueLiteral - lookup_data, # type: VariableValue - handler=None, # type: Optional[Type[LookupHandler]] - variable_type="cfngin", # type: str - ): - # type: (...) -> None + lookup_name: VariableValueLiteral, + lookup_data: VariableValue, + handler: Optional[Type[LookupHandler]] = None, + variable_type: str = "cfngin", + ) -> None: """Initialize class. Args: @@ -562,6 +576,10 @@ def __init__( handler: Lookup handler that will be use to resolve the value. variable_type: Type of variable (cfngin|runway). + Raises: + UnknownLookupType: Invalid lookup type. + ValueError: Invalid value for variable_type. + """ self._resolved = False self._value = None @@ -590,26 +608,23 @@ def __init__( 'Variable type must be one of "cfngin" or "runway"' ) except KeyError: - raise UnknownLookupType(lookup_name_resolved) + raise UnknownLookupType(self) from None self.handler = handler @property - def dependencies(self): - # () -> Set[str] + def dependencies(self) -> Set[str]: """Stack names that this variable depends on.""" if isinstance(self.handler, type): return self.handler.dependencies(self.lookup_data) return set() @property - def resolved(self): - # type: () -> bool + def resolved(self) -> bool: """Use to check if the variable value has been resolved.""" return self._resolved @property - def simplified(self): - # type: () -> VariableValueLookup + def simplified(self) -> "VariableValueLookup": """Return a simplified version of the value. This can be used to concatenate two literals into one literal or @@ -619,15 +634,24 @@ def simplified(self): return self @property - def value(self): - # type: () -> Any - """Value of the variable. Can be resolved or unresolved.""" + def value(self) -> Any: + """Value of the variable. Can be resolved or unresolved. + + Raises: + UnresolvedVariableValue: Value accessed before it has been resolved. + + """ if self._resolved: return self._value raise UnresolvedVariableValue(self) - def resolve(self, context, provider=None, variables=None, **kwargs): - # type: (Any, Any, 'Optional[VariablesDefinition]', Any) -> None + def resolve( + self, + context: Union["CFNginContext", "RunwayContext"], + provider: Optional["BaseProvider"] = None, + variables: Optional["VariablesDefinition"] = None, + **kwargs: Any + ) -> None: """Resolve the variable value. Args: @@ -666,11 +690,10 @@ def resolve(self, context, provider=None, variables=None, **kwargs): self._resolve_legacy(context=context, provider=provider) ) except Exception as err2: - raise FailedLookup(self, err2) - raise FailedLookup(self, err) + raise FailedLookup(self, err2) from err2 + raise FailedLookup(self, err) from err - def _resolve(self, value): - # type: (Any) -> None + def _resolve(self, value: Any) -> None: """Set _value and _resolved from the result of resolve(). Args: @@ -681,7 +704,9 @@ def _resolve(self, value): self._resolved = True # TODO Remove during the next major release. - def _resolve_legacy(self, context, provider): + def _resolve_legacy( + self, context: "CFNginContext", provider: "BaseProvider" + ) -> Any: """Resolve legacy lookups. Stacker style custom lookups only take 3 args (value, provider, @@ -710,13 +735,11 @@ def _resolve_legacy(self, context, provider): value=self.lookup_data.value, context=context, provider=provider ) - def __iter__(self): - # type: () -> Iterable + def __iter__(self) -> Iterable: """How the object is iterated.""" yield self - def __repr__(self): - # type: () -> str + def __repr__(self) -> str: """Return object representation.""" if self._resolved: return "Lookup<{r} ({t} {d})>".format( @@ -724,7 +747,7 @@ def __repr__(self): ) return "Lookup<{t} {d}>".format(t=self.lookup_name, d=repr(self.lookup_data),) - def __str__(self): + def __str__(self) -> str: # type: () -> str """Object displayed as a string.""" return "${{{type} {data}}}".format( diff --git a/tests/unit/cfngin/blueprints/test_base.py b/tests/unit/cfngin/blueprints/test_base.py index 44aebcc6a..647f609f9 100644 --- a/tests/unit/cfngin/blueprints/test_base.py +++ b/tests/unit/cfngin/blueprints/test_base.py @@ -23,15 +23,15 @@ TroposphereType, ) from runway.cfngin.exceptions import ( - InvalidLookupCombination, InvalidUserdataPlaceholder, MissingVariable, - UnresolvedVariable, - UnresolvedVariables, + UnresolvedBlueprintVariable, + UnresolvedBlueprintVariables, ValidatorError, VariableTypeRequired, ) from runway.cfngin.lookups import register_lookup_handler +from runway.exceptions import InvalidLookupConcatenation from runway.variables import Variable from ..factories import mock_context @@ -176,7 +176,7 @@ class TestBlueprint(Blueprint): """Test blueprint.""" blueprint = TestBlueprint(name="test", context=MagicMock()) - with self.assertRaises(UnresolvedVariables): + with self.assertRaises(UnresolvedBlueprintVariables): blueprint.get_variables() def test_set_description(self): @@ -278,7 +278,7 @@ def test_resolve_variable_provided_not_resolved(self): """Test resolve variable provided not resolved.""" var_name = "testVar" provided_variable = Variable(var_name, "${mock abc}", "cfngin") - with self.assertRaises(UnresolvedVariable): + with self.assertRaises(UnresolvedBlueprintVariable): var_def = {"type": str} blueprint_name = "testBlueprint" @@ -538,7 +538,7 @@ def return_list_something(*_args, **_kwargs): "cfngin", ) variable._value[0].resolve({}, {}) - with self.assertRaises(InvalidLookupCombination): + with self.assertRaises(InvalidLookupConcatenation): variable.value() # pylint: disable=not-callable def test_get_variables(self): diff --git a/tests/unit/cfngin/lookups/handlers/test_hook_data.py b/tests/unit/cfngin/lookups/handlers/test_hook_data.py index bb6891f90..6a0f643f3 100644 --- a/tests/unit/cfngin/lookups/handlers/test_hook_data.py +++ b/tests/unit/cfngin/lookups/handlers/test_hook_data.py @@ -1,9 +1,9 @@ """Tests for runway.cfngin.lookups.handlers.hook_data.""" -# pylint: disable=no-self-use +# pylint: disable=no-self-use,protected-access import pytest from troposphere.awslambda import Code -from runway.cfngin.exceptions import FailedVariableLookup +from runway.exceptions import FailedVariableLookup from runway.variables import Variable @@ -48,8 +48,10 @@ def test_not_found(self, cfngin_context): with pytest.raises(FailedVariableLookup) as err: variable.resolve(cfngin_context) - assert "ValueError" in str(err.value) - assert 'Could not find a value for "fake_hook.bad.result"' in str(err.value) + assert str(err.value) == ( + f'Could not resolve lookup "{variable._raw_value}" for variable "{variable.name}"' + ) + assert "Could not find a value for" in str(err.value.__cause__) def test_troposphere(self, cfngin_context): """Test with troposphere object like returned from lambda hook.""" @@ -93,7 +95,10 @@ def test_legacy_invalid_hook_data(self, cfngin_context): ): variable.resolve(cfngin_context) - assert "ValueError" in str(err.value) + assert str(err.value) == ( + f'Could not resolve lookup "{variable._raw_value}" for variable "{variable.name}"' + ) + assert "Could not find a value for" in str(err.value.__cause__) def test_legacy_bad_value_hook_data(self, cfngin_context): """Test bad value hook data.""" @@ -106,4 +111,7 @@ def test_legacy_bad_value_hook_data(self, cfngin_context): ): variable.resolve(cfngin_context) - assert "ValueError" in str(err.value) + assert str(err.value) == ( + f'Could not resolve lookup "{variable._raw_value}" for variable "{variable.name}"' + ) + assert "Could not find a value for" in str(err.value.__cause__) diff --git a/tests/unit/cfngin/lookups/test_registry.py b/tests/unit/cfngin/lookups/test_registry.py index e610a9917..eb704c181 100644 --- a/tests/unit/cfngin/lookups/test_registry.py +++ b/tests/unit/cfngin/lookups/test_registry.py @@ -4,8 +4,8 @@ from mock import MagicMock -from runway.cfngin.exceptions import FailedVariableLookup, UnknownLookupType from runway.cfngin.lookups.registry import CFNGIN_LOOKUP_HANDLERS +from runway.exceptions import FailedVariableLookup, UnknownLookupType from runway.variables import Variable, VariableValueLookup from ..factories import mock_context, mock_provider @@ -69,7 +69,7 @@ def resolve_lookups_with_output_handler_raise_valueerror(self, variable): with self.assertRaises(FailedVariableLookup) as result: variable.resolve(self.ctx, self.provider) - self.assertIsInstance(result.exception.error, ValueError) + self.assertIsInstance(result.exception.cause.__cause__, ValueError) def test_resolve_lookups_string_failed_variable_lookup(self): """Test resolve lookups string failed variable lookup.""" diff --git a/tests/unit/cfngin/test_cfngin.py b/tests/unit/cfngin/test_cfngin.py index fd2144d2f..eaeb78e1a 100644 --- a/tests/unit/cfngin/test_cfngin.py +++ b/tests/unit/cfngin/test_cfngin.py @@ -5,7 +5,7 @@ import pytest from mock import MagicMock, call, patch -from runway.cfngin import CFNgin +from runway.cfngin.cfngin import CFNgin from runway.core.components import DeployEnvironment from ..factories import MockRunwayContext diff --git a/tests/unit/core/components/test_deployment.py b/tests/unit/core/components/test_deployment.py index 2a549127c..163159a58 100644 --- a/tests/unit/core/components/test_deployment.py +++ b/tests/unit/core/components/test_deployment.py @@ -5,9 +5,10 @@ import pytest from mock import MagicMock, PropertyMock, call, patch -from runway.cfngin.exceptions import UnresolvedVariable from runway.config import DeploymentDefinition, FutureDefinition, VariablesDefinition from runway.core.components import Deployment +from runway.exceptions import UnresolvedVariable +from runway.variables import Variable MODULE = "runway.core.components._deployment" @@ -147,7 +148,13 @@ def test_env_vars_config_unresolved( DeploymentDefinition, "env_vars", PropertyMock( - side_effect=[UnresolvedVariable("test", MagicMock()), expected] + side_effect=[ + UnresolvedVariable( + Variable("test", "something", variable_type="runway"), + MagicMock(), + ), + expected, + ] ), ) monkeypatch.setattr( diff --git a/tests/unit/lookups/handlers/test_cfn.py b/tests/unit/lookups/handlers/test_cfn.py index 97c52947f..c7cbc2c13 100644 --- a/tests/unit/lookups/handlers/test_cfn.py +++ b/tests/unit/lookups/handlers/test_cfn.py @@ -10,7 +10,8 @@ from botocore.stub import Stubber from mock import MagicMock, patch -from runway.cfngin.exceptions import OutputDoesNotExist, StackDoesNotExist +from runway.cfngin.exceptions import StackDoesNotExist +from runway.exceptions import OutputDoesNotExist from runway.lookups.handlers.cfn import TYPE_NAME, CfnLookup, OutputQuery diff --git a/tests/unit/lookups/handlers/test_ssm.py b/tests/unit/lookups/handlers/test_ssm.py index 1d171bc58..333f6fba7 100644 --- a/tests/unit/lookups/handlers/test_ssm.py +++ b/tests/unit/lookups/handlers/test_ssm.py @@ -6,7 +6,7 @@ import pytest import yaml -from runway.cfngin.exceptions import FailedVariableLookup +from runway.exceptions import FailedVariableLookup from runway.variables import Variable @@ -173,5 +173,5 @@ def test_not_found(self, runway_context): with stubber as stub, pytest.raises(FailedVariableLookup) as err: var.resolve(context=runway_context) - assert "ParameterNotFound" in str(err.value) + assert "ParameterNotFound" in str(err.value.__cause__) stub.assert_no_pending_responses() diff --git a/tests/unit/module/test_cloudformation.py b/tests/unit/module/test_cloudformation.py index 46d815de9..ed183b9c4 100644 --- a/tests/unit/module/test_cloudformation.py +++ b/tests/unit/module/test_cloudformation.py @@ -29,21 +29,21 @@ def get_context(name="test", region="us-east-1"): context.env.aws_region = region return context - @patch("runway.cfngin.CFNgin.deploy") + @patch("runway.cfngin.cfngin.CFNgin.deploy") def test_deploy(self, mock_action, tmp_path): """Test deploy.""" module = CloudFormation(self.get_context(), str(tmp_path), self.generic_options) module.deploy() mock_action.assert_called_once() - @patch("runway.cfngin.CFNgin.destroy") + @patch("runway.cfngin.cfngin.CFNgin.destroy") def test_destroy(self, mock_action, tmp_path): """Test destroy.""" module = CloudFormation(self.get_context(), str(tmp_path), self.generic_options) module.destroy() mock_action.assert_called_once() - @patch("runway.cfngin.CFNgin.plan") + @patch("runway.cfngin.cfngin.CFNgin.plan") def test_plan(self, mock_action, tmp_path): """Test plan.""" module = CloudFormation(self.get_context(), str(tmp_path), self.generic_options) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 999711938..52b3688e7 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -10,7 +10,6 @@ from mock import MagicMock, call, patch from packaging.specifiers import InvalidSpecifier -from runway.cfngin.exceptions import UnresolvedVariable from runway.config import ( # tries to test the imported class unless using "as" Config, DeploymentDefinition, @@ -19,6 +18,7 @@ ) from runway.config import TestDefinition as ConfigTestDefinition from runway.config import VariablesDefinition +from runway.exceptions import UnresolvedVariable from runway.util import MutableMap MODULE = "runway.config" diff --git a/tests/unit/test_variables.py b/tests/unit/test_variables.py index db861069c..0b62cf0d3 100644 --- a/tests/unit/test_variables.py +++ b/tests/unit/test_variables.py @@ -6,9 +6,9 @@ from troposphere import s3 from runway.cfngin.blueprints.variables.types import TroposphereType -from runway.cfngin.exceptions import UnresolvedVariable from runway.cfngin.lookups import register_lookup_handler from runway.cfngin.stack import Stack +from runway.exceptions import UnresolvedVariable from runway.util import MutableMap from runway.variables import Variable