Source code for sceptre.diffing.diff_writer

import datetime
import json
import re
from abc import abstractmethod
from typing import TextIO, Generic, List
from colorama import Fore

import cfn_flip
import yaml
from deepdiff import DeepDiff
from deepdiff.serialization import json_convertor_default

from sceptre.diffing.stack_differ import StackConfiguration, StackDiff, DiffType

deepdiff_json_defaults = {
    datetime.date: lambda x: x.isoformat(),
    StackConfiguration: lambda x: dict(x._asdict()),
}


[docs]class DiffWriter(Generic[DiffType]): """A component responsible for taking a StackDiff and writing it in a way that is useful and readable. This is an abstract base class, so the abstract methods need to be implemented to create a DiffWriter for a given DiffType. """ STAR_BAR = "*" * 80 LINE_BAR = "-" * 80 def __init__( self, stack_diff: StackDiff, output_stream: TextIO, output_format: str ): """Initializes the DiffWriter :param stack_diff: The diff this writer will be outputting :param output_stream: The stream this writer should output to; Generally, this will be stdout :param output_format: Output format specified for the base Sceptre cli command; This should be one of "yaml", "json", or "text" """ self.stack_name = stack_diff.stack_name self.stack_diff = stack_diff self.template_diff = stack_diff.template_diff self.config_diff = stack_diff.config_diff self.is_deployed = stack_diff.is_deployed self.output_stream = output_stream self.output_format = output_format @property def has_difference(self) -> bool: return self.has_config_difference or self.has_template_difference
[docs] def write(self): """Writes the diff to the output stream.""" self._output(self.STAR_BAR) if not self.has_difference: self._output(f"No difference to deployed stack {self.stack_name}") return self._output(f"--> Difference detected for stack {self.stack_name}!") if not self.is_deployed: self._write_new_stack_details() return self._output(self.LINE_BAR) self._write_config_difference() self._output(self.LINE_BAR) self._write_template_difference()
def _write_new_stack_details(self): stack_config_text = self._dump_stack_config(self.stack_diff.generated_config) self._output( "This stack is not deployed yet!", self.LINE_BAR, "New Config:", "", stack_config_text, self.LINE_BAR, "New Template:", "", self.stack_diff.generated_template, ) return def _output(self, *lines: str): lines_with_breaks = [f"{line}\n" for line in lines] self.output_stream.writelines(lines_with_breaks) def _dump_stack_config(self, stack_config: StackConfiguration) -> str: stack_config_dict = dict(stack_config._asdict()) dumped = self._dump_dict(stack_config_dict) return dumped def _dump_dict(self, dict_to_dump: dict) -> str: if self.output_format == "json": # There's not really a viable way to dump a template as "text" -> YAML is very readable dumper = cfn_flip.dump_json else: dumper = cfn_flip.dump_yaml dumped = dumper(dict_to_dump) return dumped def _write_config_difference(self): if not self.has_config_difference: self._output("No stack config difference") return diff_text = self.dump_diff(self.config_diff) self._output(f"Config difference for {self.stack_name}:", "", diff_text) def _write_template_difference(self): if not self.has_template_difference: self._output("No template difference") return diff_text = self.dump_diff(self.template_diff) self._output(f"Template difference for {self.stack_name}:", "", diff_text)
[docs] @abstractmethod def dump_diff(self, diff: DiffType) -> str: """ "Implement this method to write the DiffType to string"""
@property @abstractmethod def has_config_difference(self) -> bool: """Implement this to indicate whether or not there is a config difference""" @property @abstractmethod def has_template_difference(self) -> bool: """Implement this to indicate whether or not there is a template difference"""
[docs]class DeepDiffWriter(DiffWriter[DeepDiff]): """A DiffWriter for StackDiffs where the DiffType is a DeepDiff object.""" @property def has_config_difference(self) -> bool: return len(self.config_diff) > 0 @property def has_template_difference(self) -> bool: return len(self.template_diff) > 0
[docs] def dump_diff(self, diff: DeepDiff) -> str: as_diff_dict = diff.to_dict() if self.output_format == "json": return json.dumps( as_diff_dict, indent=4, default=json_convertor_default(default_mapping=deepdiff_json_defaults), ) compatible = self._make_strings_block_compatible(as_diff_dict) return yaml.dump(compatible, indent=4)
def _make_strings_block_compatible(self, obj): """A recursive method that strips out extraneous spaces that precede line breaks. PyYaml disallows block styling for multiline strings if any of the lines has a space followed by a line break. DeepDiff will actually provide a difflib-style diff for multiline strings when there has been a value changed from one multiline string to another multiline string. However, when it produces that diff, every line ends with at least one space. This keeps it from being formatted as a block (the most useful way to display it) by PyYaml. Therefore, this function recurses into the deepdiff-generated data structure and strips all strings of those extraneous spaces that precede line breaks. :param obj: The DeepDiff generated diff dict (or some value this method has recursed into from that dict). :return: The object, stripped of extraneous spaces that precede line breaks. """ if isinstance(obj, dict): return { key: self._make_strings_block_compatible(value) for key, value in obj.items() } elif isinstance(obj, list): return [self._make_strings_block_compatible(item) for item in obj] elif isinstance(obj, str): return re.sub("[ ]*\n", "\n", obj) else: return obj
[docs]class DiffLibWriter(DiffWriter[List[str]]): """A DiffWriter for StackDiffs where the DiffType is a a list of strings.""" @property def has_config_difference(self) -> bool: return len(self.config_diff) > 0 @property def has_template_difference(self) -> bool: return len(self.template_diff) > 0
[docs] def dump_diff(self, diff: List[str]) -> str: # Difflib doesn't care about the output format since it only outputs strings. We would have # accounted for the output format in the differ itself rather than here. return "\n".join(diff)
[docs]class ColouredDiffLibWriter(DiffLibWriter): """A DiffWriter for StackDiffs where the DiffType is a a list of strings with coloured diffs.""" def _colour_diff(self, diff: List[str]): for line in diff: if line.startswith("+"): yield Fore.GREEN + line + Fore.RESET elif line.startswith("-"): yield Fore.RED + line + Fore.RESET elif line.startswith("^"): yield Fore.BLUE + line + Fore.RESET else: yield line
[docs] def dump_diff(self, diff: List[str]) -> str: coloured_diff = self._colour_diff(diff) return super().dump_diff(coloured_diff)