Skip to content

Commit eca5dfe

Browse files
committed
Colored compact view
1 parent b25f726 commit eca5dfe

File tree

8 files changed

+249
-14
lines changed

8 files changed

+249
-14
lines changed

deepdiff/colored_view.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
class ColoredView:
1818
"""A view that shows JSON with color-coded differences."""
1919

20-
def __init__(self, t2, tree_results, verbose_level=1):
20+
def __init__(self, t2, tree_results, verbose_level=1, compact=False):
2121
self.t2 = t2
2222
self.tree = tree_results
2323
self.verbose_level = verbose_level
24+
self.compact = compact
2425
self.diff_paths = self._collect_diff_paths()
2526

2627
def _collect_diff_paths(self) -> Dict[str, str]:
@@ -63,11 +64,16 @@ def _get_path_removed(self, path: str) -> dict:
6364
removed[literal_eval(key_suffix[1:-1])] = value[1]
6465
return removed
6566

67+
def _has_differences(self, path_prefix: str) -> bool:
68+
"""Check if a path prefix has any differences under it."""
69+
return any(diff_path.startswith(path_prefix + "[") for diff_path in self.diff_paths)
70+
6671
def _colorize_json(self, obj: Any, path: str = 'root', indent: int = 0) -> str:
6772
"""Recursively colorize JSON based on differences, with pretty-printing."""
6873
INDENT = ' '
6974
current_indent = INDENT * indent
7075
next_indent = INDENT * (indent + 1)
76+
7177
if path in self.diff_paths and path not in self._colorize_skip_paths:
7278
diff_type, old, new = self.diff_paths[path]
7379
if diff_type == 'changed':
@@ -77,6 +83,9 @@ def _colorize_json(self, obj: Any, path: str = 'root', indent: int = 0) -> str:
7783
elif diff_type == 'removed':
7884
return f"{RED}{self._format_value(old)}{RESET}"
7985

86+
if isinstance(obj, (dict, list)) and self.compact and not self._has_differences(path):
87+
return '{...}' if isinstance(obj, dict) else '[...]'
88+
8089
if isinstance(obj, dict):
8190
if not obj:
8291
return '{}'
@@ -99,6 +108,7 @@ def _colorize_json(self, obj: Any, path: str = 'root', indent: int = 0) -> str:
99108
removed_map = self._get_path_removed(path)
100109
for index in removed_map:
101110
self._colorize_skip_paths.add(f"{path}[{index}]")
111+
102112
items = []
103113
index = 0
104114
for value in obj:

deepdiff/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def cli():
5454
@click.option('--significant-digits', required=False, default=None, type=int, show_default=True)
5555
@click.option('--truncate-datetime', required=False, type=click.Choice(['second', 'minute', 'hour', 'day'], case_sensitive=True), show_default=True, default=None)
5656
@click.option('--verbose-level', required=False, default=1, type=click.IntRange(0, 2), show_default=True)
57+
@click.option('--view', required=False, type=click.Choice(['-', 'colored', 'colored_compact'], case_sensitive=True), show_default=True, default="-")
5758
@click.option('--debug', is_flag=True, show_default=False)
58-
@click.option('--view', required=False, type=click.Choice(['tree', 'colored'], case_sensitive=True), show_default=True, default="text")
5959
def diff(
6060
*args, **kwargs
6161
):
@@ -113,7 +113,7 @@ def diff(
113113
sys.stdout.buffer.write(delta.dumps())
114114
else:
115115
try:
116-
if kwargs["view"] == 'colored':
116+
if kwargs["view"] in {'colored', 'colored_compact'}:
117117
print(diff)
118118
else:
119119
print(diff.to_json(indent=2))

deepdiff/diff.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
type_is_subclass_of_type_group, type_in_type_group, get_doc,
2626
number_to_string, datetime_normalize, KEY_TO_VAL_STR, booleans,
2727
np_ndarray, np_floating, get_numpy_ndarray_rows, RepeatedTimer,
28-
TEXT_VIEW, TREE_VIEW, DELTA_VIEW, COLORED_VIEW, detailed__dict__, add_root_to_paths,
28+
TEXT_VIEW, TREE_VIEW, DELTA_VIEW, COLORED_VIEW, COLORED_COMPACT_VIEW, __dict__, add_root_to_paths,
2929
np, get_truncate_datetime, dict_, CannotCompare, ENUM_INCLUDE_KEYS,
3030
PydanticBaseModel, Opcode, SetOrdered, ipranges)
3131
from deepdiff.serialization import SerializationMixin
@@ -81,7 +81,7 @@ def _report_progress(_stats, progress_logger, duration):
8181
PREVIOUS_DIFF_COUNT = 'PREVIOUS DIFF COUNT'
8282
PREVIOUS_DISTANCE_CACHE_HIT_COUNT = 'PREVIOUS DISTANCE CACHE HIT COUNT'
8383
CANT_FIND_NUMPY_MSG = 'Unable to import numpy. This must be a bug in DeepDiff since a numpy array is detected.'
84-
INVALID_VIEW_MSG = 'The only valid values for the view parameter are text and tree. But {} was passed.'
84+
INVALID_VIEW_MSG = "view parameter must be one of 'text', 'tree', 'delta', 'colored' or 'colored_compact'. But {} was passed."
8585
CUTOFF_RANGE_ERROR_MSG = 'cutoff_distance_for_pairs needs to be a positive float max 1.'
8686
VERBOSE_LEVEL_RANGE_MSG = 'verbose_level should be 0, 1, or 2.'
8787
PURGE_LEVEL_RANGE_MSG = 'cache_purge_level should be 0, 1, or 2.'
@@ -366,7 +366,7 @@ def _group_by_sort_key(x):
366366

367367
self.tree.remove_empty_keys()
368368
view_results = self._get_view_results(self.view)
369-
if self.view == COLORED_VIEW:
369+
if self.view in {COLORED_VIEW, COLORED_COMPACT_VIEW}:
370370
self._colored_view = view_results
371371
else:
372372
self.update(view_results)
@@ -1764,7 +1764,9 @@ def _get_view_results(self, view):
17641764
elif view == DELTA_VIEW:
17651765
result = self._to_delta_dict(report_repetition_required=False)
17661766
elif view == COLORED_VIEW:
1767-
result = ColoredView(self.t2, tree_results=self.tree, verbose_level=self.verbose_level)
1767+
result = ColoredView(self.t2, tree_results=result, verbose_level=self.verbose_level)
1768+
elif view == COLORED_COMPACT_VIEW:
1769+
result = ColoredView(self.t2, tree_results=result, verbose_level=self.verbose_level, compact=True)
17681770
else:
17691771
raise ValueError(INVALID_VIEW_MSG.format(view))
17701772
return result
@@ -1906,7 +1908,7 @@ def affected_root_keys(self):
19061908
return result
19071909

19081910
def __str__(self):
1909-
if hasattr(self, '_colored_view') and self.view == COLORED_VIEW:
1911+
if hasattr(self, '_colored_view') and self.view in {COLORED_VIEW, COLORED_COMPACT_VIEW}:
19101912
return str(self._colored_view)
19111913
return super().__str__()
19121914

deepdiff/helper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ class IndexedHash(NamedTuple):
210210
TEXT_VIEW = 'text'
211211
DELTA_VIEW = '_delta'
212212
COLORED_VIEW = 'colored'
213+
COLORED_COMPACT_VIEW = 'colored_compact'
213214

214215
ENUM_INCLUDE_KEYS = ['__objclass__', 'name', 'value']
215216

docs/colored_view.rst

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ To use the `ColoredView`, simply pass the `COLORED_VIEW` option to the `DeepDiff
2323
from deepdiff import DeepDiff
2424
from deepdiff.helper import COLORED_VIEW
2525
26-
t1 = {"name": "John", "age": 30, "scores": [1, 2, 3]}
27-
t2 = {"name": "John", "age": 31, "scores": [1, 2, 4], "new": "value"}
26+
t1 = {"name": "John", "age": 30, "scores": [1, 2, 3], "address": {"city": "New York", "zip": "10001"}}
27+
t2 = {"name": "John", "age": 31, "scores": [1, 2, 4], "address": {"city": "Boston", "zip": "10001"}, "new": "value"}
2828
2929
diff = DeepDiff(t1, t2, view=COLORED_VIEW)
3030
print(diff)
@@ -35,9 +35,6 @@ Or from command line:
3535
3636
deep diff --view colored t1.json t2.json
3737
38-
Example Output
39-
--------------
40-
4138
The output will look something like this:
4239

4340
.. raw:: html
@@ -58,3 +55,47 @@ The output will look something like this:
5855
<span style="color: #00aa00">"new": "value"</span>
5956
}
6057
</pre>
58+
59+
Colored Compact View
60+
--------------------
61+
62+
For a more concise output, especially with deeply nested objects where many parts are unchanged,
63+
the `ColoredView` with the compact option can be used. This view is similar but collapses
64+
unchanged nested dictionaries to `{...}` and unchanged lists/tuples to `[...]`. To use the compact
65+
option do:
66+
67+
.. code-block:: python
68+
69+
from deepdiff import DeepDiff
70+
from deepdiff.helper import COLORED_COMPACT_VIEW
71+
72+
t1 = {"name": "John", "age": 30, "scores": [1, 2, 3], "address": {"city": "New York", "zip": "10001"}}
73+
t2 = {"name": "John", "age": 31, "scores": [1, 2, 4], "address": {"city": "New York", "zip": "10001"}, "new": "value"}
74+
75+
diff = DeepDiff(t1, t2, view=COLORED_COMPACT_VIEW)
76+
print(diff)
77+
78+
Or from command line:
79+
80+
.. code-block:: bash
81+
82+
deep diff --view colored_compact t1.json t2.json
83+
84+
85+
The output will look something like this:
86+
87+
.. raw:: html
88+
89+
<pre style="background-color: #f8f8f8; padding: 1em; border-radius: 4px;">
90+
{
91+
"name": "John",
92+
"age": <span style="color: #ff0000">30</span> -> <span style="color: #00aa00">31</span>,
93+
"scores": [
94+
1,
95+
2,
96+
<span style="color: #ff0000">3</span> -> <span style="color: #00aa00">4</span>
97+
],
98+
"address": {...},
99+
<span style="color: #00aa00">"new": "value"</span>
100+
}
101+
</pre>

docs/commandline.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ to get the options:
7272
--significant-digits INTEGER
7373
--truncate-datetime [second|minute|hour|day]
7474
--verbose-level INTEGER RANGE [default: 1]
75+
--view [-|colored|colored_compact]
76+
[default: -]
77+
Format for displaying differences.
7578
--help Show this message and exit.
7679
7780
@@ -109,6 +112,12 @@ The path is perhaps more readable now: `root['Molotov']['zip']`. It is more clea
109112
.. Note::
110113
The parameters in the deep diff commandline are a subset of those in :ref:`deepdiff_module_label` 's Python API.
111114
115+
To output in a specific format, for example the colored compact view (see :doc:`colored_view` for output details):
116+
117+
.. code-block:: bash
118+
119+
$ deep diff t1.json t2.json --view colored_compact
120+
112121
113122
.. _deep_grep_command:
114123

docs/view.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ You have the options of text view and tree view.
99
The main difference is that the tree view has the capabilities to traverse the objects to see what objects were compared to what other objects.
1010

1111
While the view options decide the format of the output that is mostly machine readable, regardless of the view you choose, you can get a more human readable output by using the pretty() method.
12+
DeepDiff also offers other specialized views such as the :doc:`colored_view` (which includes a compact variant) and :doc:`delta` view for specific use cases.
1213

1314
.. _text_view_label:
1415

0 commit comments

Comments
 (0)