| 
 | 1 | +import json  | 
 | 2 | +import os  | 
 | 3 | +from ast import literal_eval  | 
 | 4 | +from importlib.util import find_spec  | 
 | 5 | +from typing import Any, Dict  | 
 | 6 | + | 
 | 7 | +from deepdiff.model import TextResult, TreeResult  | 
 | 8 | + | 
 | 9 | + | 
 | 10 | +if os.name == "nt" and find_spec("colorama"):  | 
 | 11 | +    import colorama  | 
 | 12 | + | 
 | 13 | +    colorama.init()  | 
 | 14 | + | 
 | 15 | + | 
 | 16 | +# ANSI color codes  | 
 | 17 | +RED = '\033[31m'  | 
 | 18 | +GREEN = '\033[32m'  | 
 | 19 | +RESET = '\033[0m'  | 
 | 20 | + | 
 | 21 | + | 
 | 22 | +class ColoredView:  | 
 | 23 | +    """A view that shows JSON with color-coded differences."""  | 
 | 24 | + | 
 | 25 | +    def __init__(self, t2: Any, tree_result: TreeResult, compact: bool = False):  | 
 | 26 | +        self.t2 = t2  | 
 | 27 | +        self.tree = tree_result  | 
 | 28 | +        self.compact = compact  | 
 | 29 | +        self.diff_paths = self._collect_diff_paths()  | 
 | 30 | + | 
 | 31 | +    def _collect_diff_paths(self) -> Dict[str, str]:  | 
 | 32 | +        """Collect all paths that have differences and their types."""  | 
 | 33 | +        text_result = TextResult(tree_results=self.tree, verbose_level=2)  | 
 | 34 | +        diff_paths = {}  | 
 | 35 | +        for diff_type, items in text_result.items():  | 
 | 36 | +            if not items:  | 
 | 37 | +                continue  | 
 | 38 | +            try:  | 
 | 39 | +                iter(items)  | 
 | 40 | +            except TypeError:  | 
 | 41 | +                continue  | 
 | 42 | +            for path, item in items.items():  | 
 | 43 | +                if diff_type in ("values_changed", "type_changes"):  | 
 | 44 | +                    changed_path = item.get("new_path") or path  | 
 | 45 | +                    diff_paths[changed_path] = ("changed", item["old_value"], item["new_value"])  | 
 | 46 | +                elif diff_type in ("dictionary_item_added", "iterable_item_added", "set_item_added"):  | 
 | 47 | +                    diff_paths[path] = ("added", None, item)  | 
 | 48 | +                elif diff_type in ("dictionary_item_removed", "iterable_item_removed", "set_item_removed"):  | 
 | 49 | +                    diff_paths[path] = ("removed", item, None)  | 
 | 50 | +        return diff_paths  | 
 | 51 | + | 
 | 52 | +    def _format_value(self, value: Any) -> str:  | 
 | 53 | +        """Format a value for display."""  | 
 | 54 | +        if isinstance(value, bool):  | 
 | 55 | +            return 'true' if value else 'false'  | 
 | 56 | +        elif isinstance(value, str):  | 
 | 57 | +            return f'"{value}"'  | 
 | 58 | +        elif isinstance(value, (dict, list, tuple)):  | 
 | 59 | +            return json.dumps(value)  | 
 | 60 | +        else:  | 
 | 61 | +            return str(value)  | 
 | 62 | + | 
 | 63 | +    def _get_path_removed(self, path: str) -> dict:  | 
 | 64 | +        """Get all removed items for a given path."""  | 
 | 65 | +        removed = {}  | 
 | 66 | +        for key, value in self.diff_paths.items():  | 
 | 67 | +            if value[0] == 'removed' and key.startswith(path + "["):  | 
 | 68 | +                key_suffix = key[len(path):]  | 
 | 69 | +                if key_suffix.count("[") == 1 and key_suffix.endswith("]"):  | 
 | 70 | +                    removed[literal_eval(key_suffix[1:-1])] = value[1]  | 
 | 71 | +        return removed  | 
 | 72 | + | 
 | 73 | +    def _has_differences(self, path_prefix: str) -> bool:  | 
 | 74 | +        """Check if a path prefix has any differences under it."""  | 
 | 75 | +        return any(diff_path.startswith(path_prefix + "[") for diff_path in self.diff_paths)  | 
 | 76 | + | 
 | 77 | +    def _colorize_json(self, obj: Any, path: str = 'root', indent: int = 0) -> str:  | 
 | 78 | +        """Recursively colorize JSON based on differences, with pretty-printing."""  | 
 | 79 | +        INDENT = '  '  | 
 | 80 | +        current_indent = INDENT * indent  | 
 | 81 | +        next_indent = INDENT * (indent + 1)  | 
 | 82 | + | 
 | 83 | +        if path in self.diff_paths and path not in self._colorize_skip_paths:  | 
 | 84 | +            diff_type, old, new = self.diff_paths[path]  | 
 | 85 | +            if diff_type == 'changed':  | 
 | 86 | +                return f"{RED}{self._format_value(old)}{RESET} -> {GREEN}{self._format_value(new)}{RESET}"  | 
 | 87 | +            elif diff_type == 'added':  | 
 | 88 | +                return f"{GREEN}{self._format_value(new)}{RESET}"  | 
 | 89 | +            elif diff_type == 'removed':  | 
 | 90 | +                return f"{RED}{self._format_value(old)}{RESET}"  | 
 | 91 | + | 
 | 92 | +        if isinstance(obj, (dict, list)) and self.compact and not self._has_differences(path):  | 
 | 93 | +            return '{...}' if isinstance(obj, dict) else '[...]'  | 
 | 94 | + | 
 | 95 | +        if isinstance(obj, dict):  | 
 | 96 | +            if not obj:  | 
 | 97 | +                return '{}'  | 
 | 98 | +            items = []  | 
 | 99 | +            for key, value in obj.items():  | 
 | 100 | +                new_path = f"{path}['{key}']" if isinstance(key, str) else f"{path}[{key}]"  | 
 | 101 | +                if new_path in self.diff_paths and self.diff_paths[new_path][0] == 'added':  | 
 | 102 | +                    # Colorize both key and value for added fields  | 
 | 103 | +                    items.append(f'{next_indent}{GREEN}"{key}": {self._colorize_json(value, new_path, indent + 1)}{RESET}')  | 
 | 104 | +                else:  | 
 | 105 | +                    items.append(f'{next_indent}"{key}": {self._colorize_json(value, new_path, indent + 1)}')  | 
 | 106 | +            for key, value in self._get_path_removed(path).items():  | 
 | 107 | +                new_path = f"{path}['{key}']" if isinstance(key, str) else f"{path}[{key}]"  | 
 | 108 | +                items.append(f'{next_indent}{RED}"{key}": {self._colorize_json(value, new_path, indent + 1)}{RESET}')  | 
 | 109 | +            return '{\n' + ',\n'.join(items) + f'\n{current_indent}' + '}'  | 
 | 110 | + | 
 | 111 | +        elif isinstance(obj, (list, tuple)):  | 
 | 112 | +            if not obj:  | 
 | 113 | +                return '[]'  | 
 | 114 | +            removed_map = self._get_path_removed(path)  | 
 | 115 | +            for index in removed_map:  | 
 | 116 | +                self._colorize_skip_paths.add(f"{path}[{index}]")  | 
 | 117 | + | 
 | 118 | +            items = []  | 
 | 119 | +            remove_index = 0  | 
 | 120 | +            for index, value in enumerate(obj):  | 
 | 121 | +                while remove_index == next(iter(removed_map), None):  | 
 | 122 | +                    items.append(f'{next_indent}{RED}{self._format_value(removed_map.pop(remove_index))}{RESET}')  | 
 | 123 | +                    remove_index += 1  | 
 | 124 | +                items.append(f'{next_indent}{self._colorize_json(value, f"{path}[{index}]", indent + 1)}')  | 
 | 125 | +                remove_index += 1  | 
 | 126 | +            for value in removed_map.values():  | 
 | 127 | +                items.append(f'{next_indent}{RED}{self._format_value(value)}{RESET}')  | 
 | 128 | +            return '[\n' + ',\n'.join(items) + f'\n{current_indent}' + ']'  | 
 | 129 | +        else:  | 
 | 130 | +            return self._format_value(obj)  | 
 | 131 | + | 
 | 132 | +    def __str__(self) -> str:  | 
 | 133 | +        """Return the colorized, pretty-printed JSON string."""  | 
 | 134 | +        self._colorize_skip_paths = set()  | 
 | 135 | +        return self._colorize_json(self.t2)  | 
 | 136 | + | 
 | 137 | +    def __iter__(self):  | 
 | 138 | +        """Make the view iterable by yielding the tree results."""  | 
 | 139 | +        yield from self.tree.items()  | 
0 commit comments