From 9a3e27b2b19fb1faf84b2d1a4ff75017a8f63110 Mon Sep 17 00:00:00 2001 From: Michael Bordash Date: Wed, 20 Nov 2024 18:39:48 -0500 Subject: [PATCH 1/8] custom base exceptions updated for pickling --- runway/exceptions.py | 122 +++++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 44 deletions(-) diff --git a/runway/exceptions.py b/runway/exceptions.py index ea674e2d4..4983237b6 100644 --- a/runway/exceptions.py +++ b/runway/exceptions.py @@ -37,9 +37,9 @@ class ConfigNotFound(RunwayError): looking_for: list[str] message: str - path: Path + path: Path | None - def __init__(self, *, looking_for: list[str] | None = None, path: Path) -> None: + def __init__(self, looking_for: list[str] | None = None, path: Path | None = None) -> None: """Instantiate class. Args: @@ -56,6 +56,9 @@ def __init__(self, *, looking_for: list[str] | None = None, path: Path) -> None: self.message = f"config file not found at path {path}" super().__init__(self.path, self.looking_for) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.looking_for, self.path) class DockerConnectionRefusedError(RunwayError): """Docker connection refused. @@ -89,7 +92,9 @@ class DockerExecFailedError(RunwayError): exit_code: int """The ``StatusCode`` returned by Docker.""" - def __init__(self, response: dict[str, Any]) -> None: + message: str = "error message undefined" + + def __init__(self, response: dict[str, Any] | None = None) -> None: """Instantiate class. Args: @@ -99,9 +104,10 @@ def __init__(self, response: dict[str, Any]) -> None: that may not streamed. """ - self.exit_code = response.get("StatusCode", 1) # we can assume this will be > 0 - error: dict[Any, Any] = response.get("Error") or {} # value from dict could be NoneType - self.message = error.get("Message", "error message undefined") + self.exit_code = response.get("StatusCode", 1) if response else 1 # we can assume this will be > 0 + error: dict[Any, Any] | None = response.get("Error") if response else {} # value from dict could be NoneType + if error: + self.message = error.get("Message", "error message undefined") super().__init__() @@ -114,12 +120,13 @@ class FailedLookup(RunwayError): """ - cause: Exception - lookup: VariableValueLookup + cause: Exception | None + lookup: VariableValueLookup | None message: str = "Failed lookup" def __init__( - self, lookup: VariableValueLookup, cause: Exception, *args: Any, **kwargs: Any + self, lookup: VariableValueLookup | None = None, cause: Exception | None = None + , *args: Any, **kwargs: Any ) -> None: """Instantiate class. @@ -135,6 +142,9 @@ def __init__( self.lookup = lookup super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.lookup, self.cause) class FailedVariableLookup(RunwayError): """Lookup could not be resolved. @@ -143,12 +153,13 @@ class FailedVariableLookup(RunwayError): """ - cause: FailedLookup - variable: Variable + cause: FailedLookup | None + variable: Variable | None message: str def __init__( - self, variable: Variable, lookup_error: FailedLookup, *args: Any, **kwargs: Any + self, variable: Variable | None = None, + lookup_error: FailedLookup | None = None, *args: Any, **kwargs: Any ) -> None: """Instantiate class. @@ -163,9 +174,12 @@ def __init__( self.variable = variable self.message = ( f'Could not resolve lookup "{lookup_error.lookup}" for variable "{variable.name}"' - ) + ) if variable and lookup_error else "Failed variable lookup" super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.cause, self.variable) class HclParserError(RunwayError): """HCL/HCL2 parser error.""" @@ -202,27 +216,32 @@ class InvalidLookupConcatenation(RunwayError): """ - concatenated_lookups: VariableValueConcatenation[Any] - invalid_lookup: VariableValue - message: str + concatenated_lookups: VariableValueConcatenation[Any] | None + invalid_lookup: VariableValue | None + message: str = "Invalid lookup concatenation" def __init__( self, - invalid_lookup: VariableValue, - concat_lookups: VariableValueConcatenation[Any], + invalid_lookup: VariableValue | None = None, + concat_lookups: VariableValueConcatenation[Any] | None = None, *args: Any, **kwargs: Any, ) -> None: """Instantiate class.""" self.concatenated_lookups = concat_lookups self.invalid_lookup = invalid_lookup - self.message = ( - f"expected return value of type {str} but received " - f'{type(invalid_lookup.value)} for lookup "{invalid_lookup}" ' - f'in "{concat_lookups}"' - ) + if concat_lookups and invalid_lookup: + self.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__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.invalid_lookup, self.concatenated_lookups) class KubectlVersionNotSpecified(RunwayError): """kubectl version is required but was not specified. @@ -259,13 +278,13 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: class OutputDoesNotExist(RunwayError): """Raised when a specific stack output does not exist.""" - output: str + output: str | None """Name of the CloudFormation Stack's Output that does not exist.""" - stack_name: str + stack_name: str | None """Name of a CloudFormation Stack.""" - def __init__(self, stack_name: str, output: str, *args: Any, **kwargs: Any) -> None: + def __init__(self, stack_name: str | None = None, output: str | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -281,17 +300,21 @@ def __init__(self, stack_name: str, output: str, *args: Any, **kwargs: Any) -> N self.message = f"Output {output} does not exist on stack {stack_name}" super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.output, self.stack_name) + class RequiredTagNotFoundError(RunwayError): """Required tag not found on resource.""" - resource: str + resource: str | None """An ID or name to identify a resource.""" - tag_key: str + tag_key: str | None """Key of the tag that could not be found.""" - def __init__(self, resource: str, tag_key: str) -> None: + def __init__(self, resource: str | None = None, tag_key: str | None = None) -> None: """Instantiate class. Args: @@ -304,6 +327,10 @@ def __init__(self, resource: str, tag_key: str) -> None: self.message = f"required tag '{tag_key}' not found for {resource}" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.resource, self.tag_key) + class UnknownLookupType(RunwayError): """Lookup type provided does not match a registered lookup. @@ -314,9 +341,9 @@ class UnknownLookupType(RunwayError): """ - message: str + message: str = "Unknown lookup type" - def __init__(self, lookup: VariableValueLookup, *args: Any, **kwargs: Any) -> None: + def __init__(self, lookup: VariableValueLookup | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -325,16 +352,18 @@ def __init__(self, lookup: VariableValueLookup, *args: Any, **kwargs: Any) -> No **kwargs: Arbitrary keyword arguments. """ - self.message = f'Unknown lookup type "{lookup.lookup_name.value}" in "{lookup}"' + if lookup: + self.message = f'Unknown lookup type "{lookup.lookup_name.value}" in "{lookup}"' + super().__init__(*args, **kwargs) class UnresolvedVariable(RunwayError): """Raised when trying to use a variable before it has been resolved.""" - message: str + message: str = "Unresolved variable" - def __init__(self, variable: Variable, *args: Any, **kwargs: Any) -> None: + def __init__(self, variable: Variable | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -343,8 +372,10 @@ def __init__(self, variable: Variable, *args: Any, **kwargs: Any) -> None: **kwargs: Arbitrary keyword arguments. """ - self.message = f'Attempted to use variable "{variable.name}" before it was resolved' - self.variable = variable + if variable: + self.message = f'Attempted to use variable "{variable.name}" before it was resolved' + self.variable = variable + super().__init__(*args, **kwargs) @@ -357,10 +388,10 @@ class UnresolvedVariableValue(RunwayError): """ - lookup: VariableValueLookup + lookup: VariableValueLookup | None message: str = "Unresolved lookup" - def __init__(self, lookup: VariableValueLookup, *args: Any, **kwargs: Any) -> None: + def __init__(self, lookup: VariableValueLookup | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -369,23 +400,26 @@ def __init__(self, lookup: VariableValueLookup, *args: Any, **kwargs: Any) -> No **kwargs: Arbitrary keyword arguments. """ - self.lookup = lookup + if lookup: + self.message = f'Unresolved lookup "{lookup}"' + self.lookup = lookup super().__init__(*args, **kwargs) class VariablesFileNotFound(RunwayError): """Defined variables file could not be found.""" - file_path: Path - message: str + file_path: Path | None + message: str = "Defined variables file not found" - def __init__(self, file_path: Path) -> None: + def __init__(self, file_path: Path | None = None) -> None: """Instantiate class. Args: file_path: Path where the file was expected to be found. """ - self.file_path = file_path - self.message = f"defined variables file not found at path {file_path}" + if file_path: + self.file_path = file_path + self.message = f"defined variables file not found at path {file_path}" super().__init__(self.file_path) From a3e359e2d3a1b480a4d3b577fe2a80150771a3c3 Mon Sep 17 00:00:00 2001 From: Michael Bordash Date: Wed, 20 Nov 2024 18:40:11 -0500 Subject: [PATCH 2/8] add exceptions test class --- tests/unit/test_exceptions.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/unit/test_exceptions.py diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py new file mode 100644 index 000000000..8ee84a2ba --- /dev/null +++ b/tests/unit/test_exceptions.py @@ -0,0 +1,19 @@ +"""Test runway.exceptions.""" + +from __future__ import annotations + +import pickle +from typing import TYPE_CHECKING + +from runway.exceptions import ConfigNotFound + +if TYPE_CHECKING: + from pathlib import Path + +class TestConfigNotFound: + """Test "ConfigNotFound.""" + + def test_pickle(self, tmp_path: Path) -> None: + """Test pickling.""" + exc = ConfigNotFound(["foo"], tmp_path) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) From d0994b58adc13e329ebb5b648e6c1047d38f4bdc Mon Sep 17 00:00:00 2001 From: Michael Bordash Date: Wed, 20 Nov 2024 18:52:11 -0500 Subject: [PATCH 3/8] linting fixes --- runway/exceptions.py | 44 ++++++++++++++++++++++++++--------- tests/unit/test_exceptions.py | 1 + 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/runway/exceptions.py b/runway/exceptions.py index 4983237b6..9c9a232db 100644 --- a/runway/exceptions.py +++ b/runway/exceptions.py @@ -60,6 +60,7 @@ def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: """Support for pickling.""" return self.__class__, (self.looking_for, self.path) + class DockerConnectionRefusedError(RunwayError): """Docker connection refused. @@ -104,8 +105,12 @@ def __init__(self, response: dict[str, Any] | None = None) -> None: that may not streamed. """ - self.exit_code = response.get("StatusCode", 1) if response else 1 # we can assume this will be > 0 - error: dict[Any, Any] | None = response.get("Error") if response else {} # value from dict could be NoneType + self.exit_code = ( + response.get("StatusCode", 1) if response else 1 + ) # we can assume this will be > 0 + error: dict[Any, Any] | None = ( + response.get("Error") if response else {} + ) # value from dict could be NoneType if error: self.message = error.get("Message", "error message undefined") super().__init__() @@ -125,8 +130,11 @@ class FailedLookup(RunwayError): message: str = "Failed lookup" def __init__( - self, lookup: VariableValueLookup | None = None, cause: Exception | None = None - , *args: Any, **kwargs: Any + self, + lookup: VariableValueLookup | None = None, + cause: Exception | None = None, + *args: Any, + **kwargs: Any, ) -> None: """Instantiate class. @@ -146,6 +154,7 @@ def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: """Support for pickling.""" return self.__class__, (self.lookup, self.cause) + class FailedVariableLookup(RunwayError): """Lookup could not be resolved. @@ -158,8 +167,11 @@ class FailedVariableLookup(RunwayError): message: str def __init__( - self, variable: Variable | None = None, - lookup_error: FailedLookup | None = None, *args: Any, **kwargs: Any + self, + variable: Variable | None = None, + lookup_error: FailedLookup | None = None, + *args: Any, + **kwargs: Any, ) -> None: """Instantiate class. @@ -173,14 +185,17 @@ def __init__( self.cause = lookup_error self.variable = variable self.message = ( - f'Could not resolve lookup "{lookup_error.lookup}" for variable "{variable.name}"' - ) if variable and lookup_error else "Failed variable lookup" + (f'Could not resolve lookup "{lookup_error.lookup}" for variable "{variable.name}"') + if variable and lookup_error + else "Failed variable lookup" + ) super().__init__(*args, **kwargs) def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: """Support for pickling.""" return self.__class__, (self.cause, self.variable) + class HclParserError(RunwayError): """HCL/HCL2 parser error.""" @@ -243,6 +258,7 @@ def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: """Support for pickling.""" return self.__class__, (self.invalid_lookup, self.concatenated_lookups) + class KubectlVersionNotSpecified(RunwayError): """kubectl version is required but was not specified. @@ -284,7 +300,9 @@ class OutputDoesNotExist(RunwayError): stack_name: str | None """Name of a CloudFormation Stack.""" - def __init__(self, stack_name: str | None = None, output: str | None = None, *args: Any, **kwargs: Any) -> None: + def __init__( + self, stack_name: str | None = None, output: str | None = None, *args: Any, **kwargs: Any + ) -> None: """Instantiate class. Args: @@ -343,7 +361,9 @@ class UnknownLookupType(RunwayError): message: str = "Unknown lookup type" - def __init__(self, lookup: VariableValueLookup | None = None, *args: Any, **kwargs: Any) -> None: + def __init__( + self, lookup: VariableValueLookup | None = None, *args: Any, **kwargs: Any + ) -> None: """Instantiate class. Args: @@ -391,7 +411,9 @@ class UnresolvedVariableValue(RunwayError): lookup: VariableValueLookup | None message: str = "Unresolved lookup" - def __init__(self, lookup: VariableValueLookup | None = None, *args: Any, **kwargs: Any) -> None: + def __init__( + self, lookup: VariableValueLookup | None = None, *args: Any, **kwargs: Any + ) -> None: """Instantiate class. Args: diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 8ee84a2ba..39d148a5b 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from pathlib import Path + class TestConfigNotFound: """Test "ConfigNotFound.""" From 9d301bd057dba84a84f626d601edabef474a8a5f Mon Sep 17 00:00:00 2001 From: Michael Bordash Date: Thu, 21 Nov 2024 20:21:49 -0500 Subject: [PATCH 4/8] adding cfngin work --- runway/cfngin/exceptions.py | 279 ++++++++++++++++++++++---- tests/unit/test_exceptions.py | 355 +++++++++++++++++++++++++++++++++- 2 files changed, 590 insertions(+), 44 deletions(-) diff --git a/runway/cfngin/exceptions.py b/runway/cfngin/exceptions.py index 3edd4d06b..ccecaa496 100644 --- a/runway/cfngin/exceptions.py +++ b/runway/cfngin/exceptions.py @@ -33,10 +33,10 @@ class CfnginBucketAccessDenied(CfnginError): """ - bucket_name: str + bucket_name: str | None message: str - def __init__(self, *, bucket_name: str) -> None: + def __init__(self, bucket_name: str | None = None) -> None: """Instantiate class. Args: @@ -47,6 +47,10 @@ def __init__(self, *, bucket_name: str) -> None: self.message = f"access denied for cfngin_bucket {bucket_name}" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.bucket_name,) + class CfnginBucketNotFound(CfnginError): """CFNgin bucket specified or default bucket being used but it does not exist. @@ -56,10 +60,10 @@ class CfnginBucketNotFound(CfnginError): """ - bucket_name: str + bucket_name: str | None message: str - def __init__(self, *, bucket_name: str) -> None: + def __init__(self, *, bucket_name: str | None = None) -> None: """Instantiate class. Args: @@ -70,6 +74,10 @@ def __init__(self, *, bucket_name: str) -> None: self.message = f"cfngin_bucket does not exist {bucket_name}" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.bucket_name,) + class CfnginBucketRequired(CfnginError): """CFNgin bucket is required to use a feature but it not provided/disabled.""" @@ -95,25 +103,34 @@ def __init__(self, *, config_path: AnyPath | None = None, reason: str | None = N self.config_path = config_path super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.config_path,) + class CfnginOnlyLookupError(CfnginError): """Attempted to use a CFNgin lookup outside of CFNgin.""" - lookup_name: str + lookup_name: str | None - def __init__(self, lookup_name: str) -> None: + def __init__(self, lookup_name: str | None = None) -> None: """Instantiate class.""" self.lookup_name = lookup_name self.message = f"attempted to use CFNgin only lookup {lookup_name} outside of CFNgin" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.lookup_name,) + class ChangesetDidNotStabilize(CfnginError): """Raised when the applying a changeset fails.""" + id: str | None message: str - def __init__(self, change_set_id: str) -> None: + def __init__(self, change_set_id: str | None = None) -> None: """Instantiate class. Args: @@ -124,13 +141,25 @@ def __init__(self, change_set_id: str) -> None: self.message = f"Changeset '{change_set_id}' did not reach a completed state." super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.id,) + class GraphError(CfnginError): """Raised when the graph is invalid (e.g. acyclic dependencies).""" + exception: Exception | None + stack: str | None + dependency: str | None message: str - def __init__(self, exception: Exception, stack: str, dependency: str) -> None: + def __init__( + self, + exception: Exception | None = None, + stack: str | None = None, + dependency: str | None = None, + ) -> None: """Instantiate class. Args: @@ -149,13 +178,21 @@ def __init__(self, exception: Exception, stack: str, dependency: str) -> None: ) super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.exception, self.stack, self.dependency) + class ImproperlyConfigured(CfnginError): """Raised when a component is improperly configured.""" + error: Exception | None + kls: Any | None message: str - def __init__(self, kls: Any, error: Exception, *args: Any, **kwargs: Any) -> None: + def __init__( + self, kls: Any | None = None, error: Exception | None = None, *args: Any, **kwargs: Any + ) -> None: """Instantiate class. Args: @@ -168,6 +205,10 @@ def __init__(self, kls: Any, error: Exception, *args: Any, **kwargs: Any) -> Non self.message = f'Class "{kls}" is improperly configured: {error}' super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.kls, self.error) + class InvalidConfig(CfnginError): """Provided config file is invalid.""" @@ -194,18 +235,22 @@ def __init__(self, errors: str | list[Exception | str]) -> None: class InvalidDockerizePipConfiguration(CfnginError): """Raised when the provided configuration for dockerized pip is invalid.""" - message: str + message: str = "Invalid configuration for dockerized pip" - def __init__(self, msg: str) -> None: + def __init__(self, msg: str | None = None) -> None: """Instantiate class. Args: msg: The reason for the error being raised. """ - self.message = msg + self.message = msg if msg else self.message super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.message,) + class InvalidUserdataPlaceholder(CfnginError): """Raised when a placeholder name in raw_user_data is not valid. @@ -214,10 +259,16 @@ class InvalidUserdataPlaceholder(CfnginError): """ + blueprint_name: str | None + exception_message: str | None message: str def __init__( - self, blueprint_name: str, exception_message: str, *args: Any, **kwargs: Any + self, + blueprint_name: str | None = None, + exception_message: str | None = None, + *args: Any, + **kwargs: Any, ) -> None: """Instantiate class. @@ -235,13 +286,18 @@ def __init__( ) super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.blueprint_name, self.exception_message) + class MissingEnvironment(CfnginError): """Raised when an environment lookup is used but the key doesn't exist.""" + key: str | None message: str - def __init__(self, key: str, *args: Any, **kwargs: Any) -> None: + def __init__(self, key: str | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -254,13 +310,18 @@ def __init__(self, key: str, *args: Any, **kwargs: Any) -> None: self.message = f"Environment missing key {key}." super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.key,) + class MissingParameterException(CfnginError): """Raised if a required parameter with no default is missing.""" - message: str + parameters: list[str] | None + message: str = "Missing required cloudformation parameters" - def __init__(self, parameters: list[str], *args: Any, **kwargs: Any) -> None: + def __init__(self, parameters: list[str] | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -270,16 +331,29 @@ def __init__(self, parameters: list[str], *args: Any, **kwargs: Any) -> None: """ self.parameters = parameters - self.message = f"Missing required cloudformation parameters: {', '.join(parameters)}" + if parameters: + self.message = f"Missing required cloudformation parameters: {', '.join(parameters)}" super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.parameters,) + class MissingVariable(CfnginError): """Raised when a variable with no default is not provided a value.""" + blueprint_name: str | None + variable_name: str | None message: str - def __init__(self, blueprint_name: str, variable_name: str, *args: Any, **kwargs: Any) -> None: + def __init__( + self, + blueprint_name: str | None = None, + variable_name: str | None = None, + *args: Any, + **kwargs: Any, + ) -> None: """Instantiate class. Args: @@ -292,6 +366,10 @@ def __init__(self, blueprint_name: str, variable_name: str, *args: Any, **kwargs self.message = f'Variable "{variable_name}" in blueprint "{blueprint_name}" is missing' super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.blueprint_name, self.variable_name) + class PipError(CfnginError): """Raised when pip returns a non-zero exit code.""" @@ -326,24 +404,34 @@ def __init__(self) -> None: class PersistentGraphCannotLock(CfnginError): """Raised when the persistent graph in S3 cannot be locked.""" + reason: str | None message: str - def __init__(self, reason: str) -> None: + def __init__(self, reason: str | None = None) -> None: """Instantiate class.""" self.message = f"Could not lock persistent graph; {reason}" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.reason,) + class PersistentGraphCannotUnlock(CfnginError): """Raised when the persistent graph in S3 cannot be unlocked.""" + reason: Exception | str | None message: str - def __init__(self, reason: Exception | str) -> None: + def __init__(self, reason: Exception | str | None = None) -> None: """Instantiate class.""" self.message = f"Could not unlock persistent graph; {reason}" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.reason,) + class PersistentGraphLocked(CfnginError): """Raised when the persistent graph in S3 is lock. @@ -352,9 +440,10 @@ class PersistentGraphLocked(CfnginError): """ + reason: str | None message: str - def __init__(self, *, message: str | None = None, reason: str | None = None) -> None: + def __init__(self, message: str | None = None, reason: str | None = None) -> None: """Instantiate class.""" if message: self.message = message @@ -363,6 +452,10 @@ def __init__(self, *, message: str | None = None, reason: str | None = None) -> self.message = f"Persistent graph is locked. {reason}" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.message, self.reason) + class PersistentGraphLockCodeMismatch(CfnginError): """Raised when the provided persistent graph lock code does not match. @@ -372,9 +465,11 @@ class PersistentGraphLockCodeMismatch(CfnginError): """ + provided_code: str | None + s3_code: str | None message: str - def __init__(self, provided_code: str, s3_code: str | None) -> None: + def __init__(self, provided_code: str | None = None, s3_code: str | None = None) -> None: """Instantiate class.""" self.message = ( f"The provided lock code '{provided_code}' does not match the S3 " @@ -382,6 +477,10 @@ def __init__(self, provided_code: str, s3_code: str | None) -> None: ) super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.provided_code, self.s3_code) + class PersistentGraphUnlocked(CfnginError): """Raised when the persistent graph in S3 is unlock. @@ -390,6 +489,7 @@ class PersistentGraphUnlocked(CfnginError): """ + reason: str | None message: str def __init__(self, message: str | None = None, reason: str | None = None) -> None: @@ -401,13 +501,18 @@ def __init__(self, message: str | None = None, reason: str | None = None) -> Non self.message = f"Persistent graph is unlocked. {reason}" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.message, self.reason) + class PlanFailed(CfnginError): """Raised if any step of a plan fails.""" - message: str + failed_steps: list[Step] | None + message: str = "Plan failed" - def __init__(self, failed_steps: list[Step], *args: Any, **kwargs: Any) -> None: + def __init__(self, failed_steps: list[Step] | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -416,13 +521,18 @@ def __init__(self, failed_steps: list[Step], *args: Any, **kwargs: Any) -> None: **kwargs: Arbitrary keyword arguments. """ - self.failed_steps = failed_steps + if failed_steps: + self.failed_steps = failed_steps - step_names = ", ".join(step.name for step in failed_steps) - self.message = f"The following steps failed: {step_names}" + step_names = ", ".join(step.name for step in failed_steps) + self.message = f"The following steps failed: {step_names}" super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.failed_steps,) + class StackDidNotChange(CfnginError): """Raised when there are no changes to be made by the provider.""" @@ -433,9 +543,10 @@ class StackDidNotChange(CfnginError): class StackDoesNotExist(CfnginError): """Raised when a stack does not exist in AWS.""" + stack_name: str | None message: str - def __init__(self, stack_name: str, *args: Any, **kwargs: Any) -> None: + def __init__(self, stack_name: str | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -450,14 +561,26 @@ def __init__(self, stack_name: str, *args: Any, **kwargs: Any) -> None: ) super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.stack_name,) + class StackUpdateBadStatus(CfnginError): """Raised if the state of a stack can't be handled.""" + stack_name: str | None + stack_status: str | None + reason: str | None message: str def __init__( - self, stack_name: str, stack_status: str, reason: str, *args: Any, **kwargs: Any + self, + stack_name: str | None = None, + stack_status: str | None = None, + reason: str | None = None, + *args: Any, + **kwargs: Any, ) -> None: """Instantiate class. @@ -478,6 +601,10 @@ def __init__( ) super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.stack_name, self.stack_status, self.reason) + class StackFailed(CfnginError): """Raised when a stack action fails. @@ -486,9 +613,11 @@ class StackFailed(CfnginError): """ + stack_name: str | None + status_reason: str | None message: str - def __init__(self, stack_name: str, status_reason: str | None = None) -> None: + def __init__(self, stack_name: str | None = None, status_reason: str | None = None) -> None: """Instantiate class. Args: @@ -504,13 +633,25 @@ def __init__(self, stack_name: str, status_reason: str | None = None) -> None: self.message += f' with reason "{status_reason}"' super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.stack_name, self.status_reason) + class UnableToExecuteChangeSet(CfnginError): """Raised if changeset execution status is not ``AVAILABLE``.""" + stack_name: str | None + change_set_id: str | None + execution_status: str | None message: str - def __init__(self, stack_name: str, change_set_id: str, execution_status: str) -> None: + def __init__( + self, + stack_name: str | None = None, + change_set_id: str | None = None, + execution_status: str | None = None, + ) -> None: """Instantiate class. Args: @@ -531,6 +672,10 @@ def __init__(self, stack_name: str, change_set_id: str, execution_status: str) - super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.stack_name, self.change_set_id, self.execution_status) + class UnhandledChangeSetStatus(CfnginError): """Raised when creating a changeset failed for an unhandled reason. @@ -539,10 +684,18 @@ class UnhandledChangeSetStatus(CfnginError): """ + stack_name: str | None + id: str | None + status: str | None + status_reason: str | None message: str def __init__( - self, stack_name: str, change_set_id: str, status: str, status_reason: str + self, + stack_name: str | None = None, + change_set_id: str | None = None, + status: str | None = None, + status_reason: str | None = None, ) -> None: """Instantiate class. @@ -564,13 +717,25 @@ def __init__( super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.stack_name, self.id, self.status, self.status_reason) + class UnresolvedBlueprintVariable(CfnginError): """Raised when trying to use a variable before it has been resolved.""" - message: str + blueprint_name: str | None + variable: Variable | None + message: str = "Variable has not been resolved" - def __init__(self, blueprint_name: str, variable: Variable, *args: Any, **kwargs: Any) -> None: + def __init__( + self, + blueprint_name: str | None = None, + variable: Variable | None = None, + *args: Any, + **kwargs: Any, + ) -> None: """Instantiate class. Args: @@ -581,18 +746,24 @@ def __init__(self, blueprint_name: str, variable: Variable, *args: Any, **kwargs **kwargs: Arbitrary keyword arguments. """ - self.message = ( - f'Variable "{variable.name}" in blueprint "{blueprint_name}" hasn\'t been resolved' - ) + if variable: + self.message = ( + f'Variable "{variable.name}" in blueprint "{blueprint_name}" hasn\'t been resolved' + ) super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.blueprint_name, self.variable) + class UnresolvedBlueprintVariables(CfnginError): """Raised when trying to use variables before they has been resolved.""" + blueprint_name: str | None message: str - def __init__(self, blueprint_name: str, *args: Any, **kwargs: Any) -> None: + def __init__(self, blueprint_name: str | None = None, *args: Any, **kwargs: Any) -> None: """Instantiate class. Args: @@ -605,17 +776,25 @@ def __init__(self, blueprint_name: str, *args: Any, **kwargs: Any) -> None: self.message = f"Blueprint: \"{blueprint_name}\" hasn't resolved it's variables" super().__init__(*args, **kwargs) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.blueprint_name,) + class ValidatorError(CfnginError): """Used for errors raised by custom validators of blueprint variables.""" + variable: str | None + validator: str | None + value: str | None + exception: Exception | None message: str def __init__( self, - variable: str, - validator: str, - value: str, + variable: str | None = None, + validator: str | None = None, + value: str | None = None, exception: Exception | None = None, ) -> None: """Instantiate class. @@ -640,6 +819,10 @@ def __init__( self.message += f": {self.exception.__class__.__name__}: {self.exception!s}" super().__init__() + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.variable, self.validator, self.value, self.exception) + def __str__(self) -> str: """Return the exception's message when converting to a string.""" return self.message @@ -648,9 +831,17 @@ def __str__(self) -> str: class VariableTypeRequired(CfnginError): """Raised when a variable defined in a blueprint is missing a type.""" + blueprint_name: str | None + variable_name: str | None message: str - def __init__(self, blueprint_name: str, variable_name: str, *args: Any, **kwargs: Any) -> None: + def __init__( + self, + blueprint_name: str | None = None, + variable_name: str | None = None, + *args: Any, + **kwargs: Any, + ) -> None: """Instantiate class. Args: @@ -664,3 +855,7 @@ def __init__(self, blueprint_name: str, variable_name: str, *args: Any, **kwargs f'Variable "{variable_name}" in blueprint "{blueprint_name}" does not have a type' ) super().__init__(*args, **kwargs) + + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.blueprint_name, self.variable_name) diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 39d148a5b..ec26d22de 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -3,12 +3,55 @@ from __future__ import annotations import pickle -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any -from runway.exceptions import ConfigNotFound +from runway.exceptions import ( + ConfigNotFound, + DockerExecFailedError, + FailedLookup, + FailedVariableLookup, + InvalidLookupConcatenation, + OutputDoesNotExist, + RequiredTagNotFoundError, +) + +from runway.cfngin.exceptions import ( + CfnginBucketAccessDenied, + CfnginBucketNotFound, + CfnginBucketRequired, + CfnginOnlyLookupError, + ChangesetDidNotStabilize, + GraphError, + ImproperlyConfigured, + InvalidConfig, + InvalidDockerizePipConfiguration, + InvalidUserdataPlaceholder, + MissingEnvironment, + MissingParameterException, + MissingVariable, + PersistentGraphCannotLock, + PersistentGraphCannotUnlock, + PersistentGraphLocked, + PersistentGraphLockCodeMismatch, + PersistentGraphUnlocked, + PlanFailed, + StackDoesNotExist, + StackUpdateBadStatus, + StackFailed, + UnableToExecuteChangeSet, + UnhandledChangeSetStatus, + UnresolvedBlueprintVariable, + UnresolvedBlueprintVariables, + ValidatorError, + VariableTypeRequired, +) if TYPE_CHECKING: from pathlib import Path + from runway.variables import VariableValueLookup, VariableValue, VariableValueConcatenation + from runway.variables.models import Variable as RunwayVariable + from runway.cfngin.variables import Variable as CfnginVariable + from runway.cfngin.plan import Step class TestConfigNotFound: @@ -18,3 +61,311 @@ def test_pickle(self, tmp_path: Path) -> None: """Test pickling.""" exc = ConfigNotFound(["foo"], tmp_path) assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestDockerExecFailedError: + """Test "DockerExecFailedError.""" + + def test_pickle(self, response: dict[str, Any] = {}) -> None: + """Test pickling.""" + exc = DockerExecFailedError(response) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestFailedLookup: + """Test "FailedLookup.""" + + def test_pickle(self, lookup: VariableValueLookup, cause: Exception) -> None: + """Test pickling.""" + exc = FailedLookup(lookup, cause) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestFailedVariableLookup: + """Test "FailedVariableLookup.""" + + def test_pickle(self, variable: RunwayVariable, lookup_error: FailedLookup) -> None: + """Test pickling.""" + exc = FailedVariableLookup(variable, lookup_error) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestInvalidLookupCombination: + """Test "InvalidLookupCombination.""" + + def test_pickle( + self, invalid_lookup: VariableValue, concat_lookups: VariableValueConcatenation + ) -> None: + """Test pickling.""" + exc = InvalidLookupConcatenation(invalid_lookup, concat_lookups) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestOutputDoesNotExist: + """Test "OutputDoesNotExist.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = OutputDoesNotExist("foo") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestRequiredTagNotFoundError: + """Test "RequiredTagNotFoundError.""" + + def test_pickle(self, tag_key: str) -> None: + """Test pickling.""" + exc = RequiredTagNotFoundError("foo", tag_key) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestVariableTypeRequired: + """Test "VariableTypeRequired.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = VariableTypeRequired("blueprint_name", "variable_name") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestCfnginBucketAccessDenied: + """Test "CfnginBucketAccessDenied.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = CfnginBucketAccessDenied("bucket_name") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestCfnginBucketNotFound: + """Test "CfnginBucketNotFound.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = CfnginBucketNotFound(bucket_name="bucket_name") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestCfnginBucketRequired: + """Test "CfnginBucketRequired.""" + + def test_pickle(self, tmp_path: Path) -> None: + """Test pickling.""" + exc = CfnginBucketRequired(config_path=tmp_path, reason="reason") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestCfnginOnlyLookupError: + """Test "CfnginOnlyLookupError.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = CfnginOnlyLookupError("lookup_name") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestChangesetDidNotStabilize: + """Test "ChangesetDidNotStabilize.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = ChangesetDidNotStabilize("change_set_id") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestGraphError: + """Test "GraphError.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = GraphError(exception=Exception("error"), stack="stack", dependency="dependency") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestImproperlyConfigured: + """Test "ImproperlyConfigured.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = ImproperlyConfigured(kls="Class", error=Exception("error")) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestInvalidConfig: + """Test "InvalidConfig.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = InvalidConfig(errors=["error1", "error2"]) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestInvalidDockerizePipConfiguration: + """Test "InvalidDockerizePipConfiguration.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = InvalidDockerizePipConfiguration("Invalid configuration") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestInvalidUserdataPlaceholder: + """Test "InvalidUserdataPlaceholder.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = InvalidUserdataPlaceholder("blueprint_name", "exception_message") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestMissingEnvironment: + """Test "MissingEnvironment.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = MissingEnvironment("key") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestMissingParameterException: + """Test "MissingParameterException.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = MissingParameterException(["param1", "param2"]) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestMissingVariable: + """Test "MissingVariable.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = MissingVariable("blueprint_name", "variable_name") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestPersistentGraphCannotLock: + """Test "PersistentGraphCannotLock.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = PersistentGraphCannotLock("reason") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestPersistentGraphCannotUnlock: + """Test "PersistentGraphCannotUnlock.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = PersistentGraphCannotUnlock("reason") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestPersistentGraphLocked: + """Test "PersistentGraphLocked.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = PersistentGraphLocked(message="message", reason="reason") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestPersistentGraphLockCodeMismatch: + """Test "PersistentGraphLockCodeMismatch.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = PersistentGraphLockCodeMismatch("provided_code", "s3_code") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestPersistentGraphUnlocked: + """Test "PersistentGraphUnlocked.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = PersistentGraphUnlocked("message", "reason") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestPlanFailed: + """Test "PlanFailed.""" + + def test_pickle(self, step: Step) -> None: + """Test pickling.""" + exc = PlanFailed([step]) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestStackDoesNotExist: + """Test "StackDoesNotExist.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = StackDoesNotExist("stack_name") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestStackUpdateBadStatus: + """Test "StackUpdateBadStatus.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = StackUpdateBadStatus("stack_name", "stack_status", "reason") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestStackFailed: + """Test "StackFailed.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = StackFailed("stack_name", "status_reason") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestUnableToExecuteChangeSet: + """Test "UnableToExecuteChangeSet.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = UnableToExecuteChangeSet("stack_name", "change_set_id", "execution_status") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestUnhandledChangeSetStatus: + """Test "UnhandledChangeSetStatus.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = UnhandledChangeSetStatus("stack_name", "change_set_id", "status", "status_reason") + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestUnresolvedBlueprintVariable: + """Test "UnresolvedBlueprintVariable.""" + + def test_pickle(self, variable: CfnginVariable) -> None: + """Test pickling.""" + exc = UnresolvedBlueprintVariable("blueprint_name", variable) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestUnresolvedBlueprintVariables: + """Test "UnresolvedBlueprintVariables.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = UnresolvedBlueprintVariables("blueprint_name", ["variable1", "variable2"]) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) + + +class TestValidatorError: + """Test "ValidatorError.""" + + def test_pickle(self) -> None: + """Test pickling.""" + exc = ValidatorError("variable", "validator", "value", Exception("error")) + assert str(pickle.loads(pickle.dumps(exc))) == str(exc) From 05d36001b2b9282da30d0248d93754f3d7b910fd Mon Sep 17 00:00:00 2001 From: Michael Bordash Date: Thu, 21 Nov 2024 20:23:11 -0500 Subject: [PATCH 5/8] enforce positional args --- runway/cfngin/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runway/cfngin/exceptions.py b/runway/cfngin/exceptions.py index ccecaa496..f0e74fb68 100644 --- a/runway/cfngin/exceptions.py +++ b/runway/cfngin/exceptions.py @@ -63,7 +63,7 @@ class CfnginBucketNotFound(CfnginError): bucket_name: str | None message: str - def __init__(self, *, bucket_name: str | None = None) -> None: + def __init__(self, bucket_name: str | None = None) -> None: """Instantiate class. Args: @@ -85,7 +85,7 @@ class CfnginBucketRequired(CfnginError): config_path: Path | None message: str - def __init__(self, *, config_path: AnyPath | None = None, reason: str | None = None) -> None: + def __init__(self, config_path: AnyPath | None = None, reason: str | None = None) -> None: """Instantiate class. Args: From d21a6445e34cc26ec6467ee99f9f5b0620511dc9 Mon Sep 17 00:00:00 2001 From: Michael Bordash Date: Fri, 22 Nov 2024 14:49:28 -0500 Subject: [PATCH 6/8] reorder parameters to align with method signature --- runway/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runway/exceptions.py b/runway/exceptions.py index 9c9a232db..938631816 100644 --- a/runway/exceptions.py +++ b/runway/exceptions.py @@ -320,7 +320,7 @@ def __init__( def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: """Support for pickling.""" - return self.__class__, (self.output, self.stack_name) + return self.__class__, (self.stack_name, self.output) class RequiredTagNotFoundError(RunwayError): From e219f1441b990fe3b8597c835d0c6333c4113303 Mon Sep 17 00:00:00 2001 From: Michael Bordash Date: Fri, 22 Nov 2024 14:51:41 -0500 Subject: [PATCH 7/8] more exception work and test updates --- runway/cfngin/exceptions.py | 34 ++++++++++++++++++++++---- tests/unit/test_exceptions.py | 46 ++++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/runway/cfngin/exceptions.py b/runway/cfngin/exceptions.py index f0e74fb68..399199ca1 100644 --- a/runway/cfngin/exceptions.py +++ b/runway/cfngin/exceptions.py @@ -83,6 +83,7 @@ class CfnginBucketRequired(CfnginError): """CFNgin bucket is required to use a feature but it not provided/disabled.""" config_path: Path | None + reason: str | None message: str def __init__(self, config_path: AnyPath | None = None, reason: str | None = None) -> None: @@ -94,6 +95,7 @@ def __init__(self, config_path: AnyPath | None = None, reason: str | None = None """ self.message = "cfngin_bucket is required" + self.reason = reason if reason: self.message += f"; {reason}" if isinstance(config_path, str): @@ -105,7 +107,7 @@ def __init__(self, config_path: AnyPath | None = None, reason: str | None = None def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: """Support for pickling.""" - return self.__class__, (self.config_path,) + return self.__class__, (self.config_path, self.reason) class CfnginOnlyLookupError(CfnginError): @@ -202,6 +204,8 @@ def __init__( **kwargs: Arbitrary keyword arguments. """ + self.kls = kls + self.error = error self.message = f'Class "{kls}" is improperly configured: {error}' super().__init__(*args, **kwargs) @@ -231,6 +235,10 @@ def __init__(self, errors: str | list[Exception | str]) -> None: self.message = errors super().__init__(errors) + def __reduce__(self) -> tuple[type[Exception], tuple[Any, ...]]: + """Support for pickling.""" + return self.__class__, (self.errors,) + class InvalidDockerizePipConfiguration(CfnginError): """Raised when the provided configuration for dockerized pip is invalid.""" @@ -280,6 +288,8 @@ def __init__( **kwargs: Arbitrary keyword arguments. """ + self.blueprint_name = blueprint_name + self.exception_message = exception_message self.message = ( f'{exception_message}. Could not parse userdata in blueprint {blueprint_name}". ' "Make sure to escape all $ symbols with a $$." @@ -363,6 +373,8 @@ def __init__( **kwargs: Arbitrary keyword arguments. """ + self.blueprint_name = blueprint_name + self.variable_name = variable_name self.message = f'Variable "{variable_name}" in blueprint "{blueprint_name}" is missing' super().__init__(*args, **kwargs) @@ -409,6 +421,7 @@ class PersistentGraphCannotLock(CfnginError): def __init__(self, reason: str | None = None) -> None: """Instantiate class.""" + self.reason = reason self.message = f"Could not lock persistent graph; {reason}" super().__init__() @@ -425,6 +438,7 @@ class PersistentGraphCannotUnlock(CfnginError): def __init__(self, reason: Exception | str | None = None) -> None: """Instantiate class.""" + self.reason = reason self.message = f"Could not unlock persistent graph; {reason}" super().__init__() @@ -445,6 +459,7 @@ class PersistentGraphLocked(CfnginError): def __init__(self, message: str | None = None, reason: str | None = None) -> None: """Instantiate class.""" + self.reason = reason if message: self.message = message else: @@ -471,6 +486,9 @@ class PersistentGraphLockCodeMismatch(CfnginError): def __init__(self, provided_code: str | None = None, s3_code: str | None = None) -> None: """Instantiate class.""" + + self.provided_code = provided_code + self.s3_code = s3_code self.message = ( f"The provided lock code '{provided_code}' does not match the S3 " f"object lock code '{s3_code}'" @@ -494,6 +512,7 @@ class PersistentGraphUnlocked(CfnginError): def __init__(self, message: str | None = None, reason: str | None = None) -> None: """Instantiate class.""" + self.reason = reason if message: self.message = message else: @@ -521,9 +540,8 @@ def __init__(self, failed_steps: list[Step] | None = None, *args: Any, **kwargs: **kwargs: Arbitrary keyword arguments. """ + self.failed_steps = failed_steps if failed_steps: - self.failed_steps = failed_steps - step_names = ", ".join(step.name for step in failed_steps) self.message = f"The following steps failed: {step_names}" @@ -555,6 +573,7 @@ def __init__(self, stack_name: str | None = None, *args: Any, **kwargs: Any) -> **kwargs: Arbitrary keyword arguments. """ + self.stack_name = stack_name self.message = ( f'Stack: "{stack_name}" does not exist in outputs or the lookup is ' "not available in this CFNgin run" @@ -594,7 +613,7 @@ def __init__( """ self.stack_name = stack_name self.stack_status = stack_status - + self.reason = reason self.message = ( f'Stack: "{stack_name}" cannot be updated nor re-created from state ' f"{stack_status}: {reason}" @@ -662,7 +681,7 @@ def __init__( """ self.stack_name = stack_name - self.id = change_set_id + self.change_set_id = change_set_id self.execution_status = execution_status self.message = ( @@ -746,6 +765,8 @@ def __init__( **kwargs: Arbitrary keyword arguments. """ + self.blueprint_name = blueprint_name + self.variable = variable if variable: self.message = ( f'Variable "{variable.name}" in blueprint "{blueprint_name}" hasn\'t been resolved' @@ -773,6 +794,7 @@ def __init__(self, blueprint_name: str | None = None, *args: Any, **kwargs: Any) **kwargs: Arbitrary keyword arguments. """ + self.blueprint_name = blueprint_name self.message = f"Blueprint: \"{blueprint_name}\" hasn't resolved it's variables" super().__init__(*args, **kwargs) @@ -851,6 +873,8 @@ def __init__( **kwargs: Arbitrary keyword arguments. """ + self.blueprint_name = blueprint_name + self.variable_name = variable_name self.message = ( f'Variable "{variable_name}" in blueprint "{blueprint_name}" does not have a type' ) diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index ec26d22de..a583e3bcc 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -4,6 +4,16 @@ import pickle from typing import TYPE_CHECKING, Any +from unittest import mock +import pytest + +from runway.variables import ( + Variable, + VariableValueLookup, + VariableValue, + VariableValueConcatenation, +) +from runway.cfngin.plan import Step from runway.exceptions import ( ConfigNotFound, @@ -48,12 +58,30 @@ if TYPE_CHECKING: from pathlib import Path - from runway.variables import VariableValueLookup, VariableValue, VariableValueConcatenation - from runway.variables.models import Variable as RunwayVariable - from runway.cfngin.variables import Variable as CfnginVariable + from runway.variables import ( + Variable, + VariableValueLookup, + VariableValue, + VariableValueConcatenation, + ) from runway.cfngin.plan import Step +@pytest.fixture +def variable() -> Variable: + """Return a Variable instance.""" + return Variable(name="test", value="test") + + +@pytest.fixture +def step() -> Step: + """Return a Step instance.""" + stack = mock.MagicMock() + stack.name = "stack" + stack.fqn = "namespace-stack" + return Step(stack=stack, fn=None) + + class TestConfigNotFound: """Test "ConfigNotFound.""" @@ -84,14 +112,14 @@ def test_pickle(self, lookup: VariableValueLookup, cause: Exception) -> None: class TestFailedVariableLookup: """Test "FailedVariableLookup.""" - def test_pickle(self, variable: RunwayVariable, lookup_error: FailedLookup) -> None: + def test_pickle(self, variable: Variable, lookup_error: FailedLookup) -> None: """Test pickling.""" exc = FailedVariableLookup(variable, lookup_error) assert str(pickle.loads(pickle.dumps(exc))) == str(exc) -class TestInvalidLookupCombination: - """Test "InvalidLookupCombination.""" +class TestInvalidLookupConcatenation: + """Test "InvalidLookupConcatenation.""" def test_pickle( self, invalid_lookup: VariableValue, concat_lookups: VariableValueConcatenation @@ -106,7 +134,7 @@ class TestOutputDoesNotExist: def test_pickle(self) -> None: """Test pickling.""" - exc = OutputDoesNotExist("foo") + exc = OutputDoesNotExist("foo", "bar") assert str(pickle.loads(pickle.dumps(exc))) == str(exc) @@ -347,7 +375,7 @@ def test_pickle(self) -> None: class TestUnresolvedBlueprintVariable: """Test "UnresolvedBlueprintVariable.""" - def test_pickle(self, variable: CfnginVariable) -> None: + def test_pickle(self, variable: Variable) -> None: """Test pickling.""" exc = UnresolvedBlueprintVariable("blueprint_name", variable) assert str(pickle.loads(pickle.dumps(exc))) == str(exc) @@ -358,7 +386,7 @@ class TestUnresolvedBlueprintVariables: def test_pickle(self) -> None: """Test pickling.""" - exc = UnresolvedBlueprintVariables("blueprint_name", ["variable1", "variable2"]) + exc = UnresolvedBlueprintVariables("blueprint_name") assert str(pickle.loads(pickle.dumps(exc))) == str(exc) From 35b6bbe74a063098c8391d14f3c415e82d781287 Mon Sep 17 00:00:00 2001 From: Michael Bordash Date: Fri, 22 Nov 2024 15:17:24 -0500 Subject: [PATCH 8/8] linting updates --- runway/cfngin/exceptions.py | 1 - tests/unit/test_exceptions.py | 46 +++++++++++++++-------------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/runway/cfngin/exceptions.py b/runway/cfngin/exceptions.py index 399199ca1..3a906c93c 100644 --- a/runway/cfngin/exceptions.py +++ b/runway/cfngin/exceptions.py @@ -486,7 +486,6 @@ class PersistentGraphLockCodeMismatch(CfnginError): def __init__(self, provided_code: str | None = None, s3_code: str | None = None) -> None: """Instantiate class.""" - self.provided_code = provided_code self.s3_code = s3_code self.message = ( diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index a583e3bcc..996616b7a 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -5,25 +5,8 @@ import pickle from typing import TYPE_CHECKING, Any from unittest import mock -import pytest - -from runway.variables import ( - Variable, - VariableValueLookup, - VariableValue, - VariableValueConcatenation, -) -from runway.cfngin.plan import Step -from runway.exceptions import ( - ConfigNotFound, - DockerExecFailedError, - FailedLookup, - FailedVariableLookup, - InvalidLookupConcatenation, - OutputDoesNotExist, - RequiredTagNotFoundError, -) +import pytest from runway.cfngin.exceptions import ( CfnginBucketAccessDenied, @@ -41,13 +24,13 @@ MissingVariable, PersistentGraphCannotLock, PersistentGraphCannotUnlock, - PersistentGraphLocked, PersistentGraphLockCodeMismatch, + PersistentGraphLocked, PersistentGraphUnlocked, PlanFailed, StackDoesNotExist, - StackUpdateBadStatus, StackFailed, + StackUpdateBadStatus, UnableToExecuteChangeSet, UnhandledChangeSetStatus, UnresolvedBlueprintVariable, @@ -55,16 +38,25 @@ ValidatorError, VariableTypeRequired, ) +from runway.cfngin.plan import Step +from runway.exceptions import ( + ConfigNotFound, + DockerExecFailedError, + FailedLookup, + FailedVariableLookup, + InvalidLookupConcatenation, + OutputDoesNotExist, + RequiredTagNotFoundError, +) +from runway.variables import ( + Variable, + VariableValue, + VariableValueConcatenation, + VariableValueLookup, +) if TYPE_CHECKING: from pathlib import Path - from runway.variables import ( - Variable, - VariableValueLookup, - VariableValue, - VariableValueConcatenation, - ) - from runway.cfngin.plan import Step @pytest.fixture