Repository: Sceptre/sceptre

Issue:
Stack differ raises AttributeError if a values is a bool
### Subject of the issue
This relates to an earlier issue 
https://github.com/Sceptre/sceptre/issues/1336

The Stack Differ raises an exception if a parameter value is of type bool.
It apparently would also fail if a parameter value is of any type other str.


### Your environment
* version of sceptre - master branch
* version of python (python --version) - 3.10
* which OS/distro - Ubuntu

### Steps to reproduce

Create a config like this:

```
parameters:
  Foo: true
```



### Expected behaviour
This should work.

### Actual behaviour

```
Traceback (most recent call last):                                                                                                                                                                                                                                                                       
  File "/usr/local/bin/sceptre", line 8, in <module>                                                                                                                                                                                                                                                     
    sys.exit(cli())                                                                                                                                                                                                                                                                                      
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1130, in __call__                                                                                                                                                                                                                   
    return self.main(*args, **kwargs)                                                                                                                                                                                                                                                                    
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1055, in main                                                                                                                                                                                                                       
    rv = self.invoke(ctx)                                                                                                                                                                                                                                                                                
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1657, in invoke                                                                                                                                                                                                                     
    return _process_result(sub_ctx.command.invoke(sub_ctx))                                                                                                                                                                                                                                              
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1404, in invoke                                                                                                                                                                                                                     
    return ctx.invoke(self.callback, **ctx.params)                                                                                                                                                                                                                                                       
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 760, in invoke                                                                                                                                                                                                                      
    return __callback(*args, **kwargs)                                                                                                                                                                                                                                                                   
  File "/usr/local/lib/python3.10/site-packages/click/decorators.py", line 26, in new_func                                                                                                                                                                                                               
    return f(get_current_context(), *args, **kwargs)                                                                                                                                                                                                                                                     
  File "/app/sceptre/sceptre/cli/helpers.py", line 46, in decorated                                                                                                                                                                                                                                      
    return func(*args, **kwargs)                                                                                                                                                                                                                                                                         
  File "/app/sceptre/sceptre/cli/diff.py", line 140, in diff_command                                                                                                                                                                                                                                     
    diffs: Dict[Stack, StackDiff] = plan.diff(stack_differ)                                                                                                                                                                                                                                              
  File "/app/sceptre/sceptre/plan/plan.py", line 419, in diff                                                                                                                                                                                                                                            
    return self._execute(*args)                                                                                                                                                                                                                                                                          
  File "/app/sceptre/sceptre/plan/plan.py", line 30, in wrapped                                                                                                                                                                                                                                          
    return func(self, *args, **kwargs)                                                                                                                                                                                                                                                                   
  File "/app/sceptre/sceptre/plan/plan.py", line 56, in _execute                                                                                                                                                                                                                                         
    return executor.execute(*args)                                                                                                                                                                                                                                                                       
  File "/app/sceptre/sceptre/plan/executor.py", line 52, in execute                                                                                                                                                                                                                                      
    stack, status = future.result()                                                                                                                                                                                                                                                                      
  File "/usr/local/lib/python3.10/concurrent/futures/_base.py", line 451, in result                                                                                                                                                                                                                      
    return self.__get_result()                                                                                                                                                                                                                                                                           
  File "/usr/local/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result                                                                                                                                                                                                                
    raise self._exception                                                                                                                                                                                                                                                                                
  File "/usr/local/lib/python3.10/concurrent/futures/thread.py", line 58, in run                                                                                                                                                                                                                         
    result = self.fn(*self.args, **self.kwargs)                                                                                                                                                                                                                                                          
  File "/app/sceptre/sceptre/plan/executor.py", line 59, in _execute                                                                                                                                                                                                                                     
    result = getattr(actions, self.command)(*args)                                                                                                                                                                                                                                                       
  File "/app/sceptre/sceptre/hooks/__init__.py", line 95, in decorated                                                                                                                                                                                                                                   
    response = func(self, *args, **kwargs)                                                                                                                                                                                                                                                               
  File "/app/sceptre/sceptre/plan/actions.py", line 1004, in diff                                                                                                                                                                                                                                        
    return stack_differ.diff(self)                                                                                                                                                                                                                                                                       
  File "/app/sceptre/sceptre/diffing/stack_differ.py", line 122, in diff                                                                                                                                                                                                                                 
    generated_config = self._create_generated_config(stack_actions.stack)                                                                                                                                                                                                                                
  File "/app/sceptre/sceptre/diffing/stack_differ.py", line 150, in _create_generated_config                                                                                                                                                                                                             
    parameters = self._extract_parameters_from_generated_stack(stack)                                                                                                                                                                                                                                    
  File "/app/sceptre/sceptre/diffing/stack_differ.py", line 178, in _extract_parameters_from_generated_stack                                                                                                                                                                                             
    formatted_parameters[key] = value.rstrip("\n")                                                                                                                                                                                                                                                       
AttributeError: 'bool' object has no attribute 'rstrip'                                                                                                                                                                                                                                                  
```



Discussion:

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b3cd598..08fb35b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -85,7 +85,7 @@ $ git clone git@github.org:<github_username>/sceptre.git
    [virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/))
 
 ```bash
-   $ poetry install --all-extras -v
+$ poetry install --all-extras -v
 ```
 
 4. Create a branch for local development:
diff --git a/sceptre/diffing/stack_differ.py b/sceptre/diffing/stack_differ.py
index 87ad33e..6f1603e 100644
--- a/sceptre/diffing/stack_differ.py
+++ b/sceptre/diffing/stack_differ.py
@@ -19,6 +19,7 @@ import yaml
 from cfn_tools import ODict
 from yaml import Dumper
 
+from sceptre.exceptions import SceptreException
 from sceptre.plan.actions import StackActions
 from sceptre.stack import Stack
 
@@ -173,9 +174,18 @@ class StackDiffer(Generic[DiffType]):
             if value is None:
                 continue
 
-            if isinstance(value, list):
-                value = ",".join(item.rstrip("\n") for item in value)
-            formatted_parameters[key] = value.rstrip("\n")
+            try:
+                if isinstance(value, list):
+                    value = ",".join(item.rstrip("\n") for item in value)
+                formatted_parameters[key] = value.rstrip("\n")
+            # Other unexpected data can get through and this would blow up the differ
+            # and lead to quite confusing exceptions being raised. This check here could
+            # be removed in a future version of Sceptre if the reader class did sanity checking.
+            except AttributeError:
+                raise SceptreException(
+                    f"Parameter '{key}' whose value is {value} "
+                    f"is of type {type(value)} and not expected here"
+                )
 
         return formatted_parameters
 
@@ -361,7 +371,7 @@ class DeepDiffStackDiffer(StackDiffer[deepdiff.DeepDiff]):
         self,
         show_no_echo=False,
         *,
-        universal_template_loader: Callable[[str], Tuple[dict, str]] = cfn_flip.load
+        universal_template_loader: Callable[[str], Tuple[dict, str]] = cfn_flip.load,
     ):
         """Initializes a DeepDiffStackDiffer.
 
@@ -409,7 +419,7 @@ class DifflibStackDiffer(StackDiffer[List[str]]):
         self,
         show_no_echo=False,
         *,
-        universal_template_loader: Callable[[str], Tuple[dict, str]] = cfn_flip.load
+        universal_template_loader: Callable[[str], Tuple[dict, str]] = cfn_flip.load,
     ):
         """Initializes a DifflibStackDiffer.
 


Test patch:
diff --git a/tests/test_diffing/test_stack_differ.py b/tests/test_diffing/test_stack_differ.py
index 1c6d272..83bf7b4 100644
--- a/tests/test_diffing/test_stack_differ.py
+++ b/tests/test_diffing/test_stack_differ.py
@@ -16,6 +16,7 @@ from sceptre.diffing.stack_differ import (
     DeepDiffStackDiffer,
     DifflibStackDiffer,
 )
+from sceptre.exceptions import SceptreException
 from sceptre.plan.actions import StackActions
 from sceptre.stack import Stack
 
@@ -343,6 +344,17 @@ class TestStackDiffer:
             self.expected_deployed_config, expected_generated
         )
 
+    def test_diff__generated_stack_has_a_bool(
+        self,
+    ):
+        self.parameters_on_stack_config["new"] = True
+        message = (
+            "Parameter 'new' whose value is True is of type "
+            "<class 'bool'> and not expected here"
+        )
+        with pytest.raises(SceptreException, match=message):
+            self.differ.diff(self.actions)
+
     def test_diff__stack_exists_with_same_config_but_template_does_not__compares_identical_configs(
         self,
     ):