Skip to content

Commit 3ecc247

Browse files
authored
Fix the ogd diff function (#56)
* Improve odg diff mechanism * Add text diff by default, with options for data and internal variants * Fixup bugs and add testing
1 parent 7becb66 commit 3ecc247

File tree

10 files changed

+561
-106
lines changed

10 files changed

+561
-106
lines changed

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ python_requires = >=3.10, <4
3131
install_requires =
3232
jsonschema
3333
colorama
34-
deepdiff<8.0.0
34+
deepdiff
3535

3636
[options.packages.find]
3737
where = src

src/objdictgen/__main__.py

+18-48
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,15 @@
2323
import logging
2424
import sys
2525
from dataclasses import dataclass, field
26-
from pprint import pformat
27-
from typing import TYPE_CHECKING, Callable, Generator, Sequence, TypeVar
26+
from typing import Callable, Sequence, TypeVar
2827

2928
from colorama import Fore, Style, init
3029

3130
import objdictgen
3231
from objdictgen import jsonod
3332
from objdictgen.node import Node
34-
from objdictgen.printing import format_node
35-
from objdictgen.typing import TDiffEntries, TDiffNodes, TPath
33+
from objdictgen.printing import format_diff_nodes, format_node
34+
from objdictgen.typing import TPath
3635

3736
T = TypeVar('T')
3837

@@ -88,38 +87,6 @@ def open_od(fname: TPath|str, validate=True, fix=False) -> "Node":
8887
raise
8988

9089

91-
def print_diffs(diffs: TDiffNodes, show=False):
92-
""" Print the differences between two object dictionaries"""
93-
94-
def _pprint(text: str):
95-
for line in pformat(text).splitlines():
96-
print(" ", line)
97-
98-
def _printlines(entries: TDiffEntries):
99-
for chtype, change, path in entries:
100-
if 'removed' in chtype:
101-
print(f"<<< {path} only in LEFT")
102-
if show:
103-
_pprint(change.t1)
104-
elif 'added' in chtype:
105-
print(f" >>> {path} only in RIGHT")
106-
if show:
107-
_pprint(change.t2)
108-
elif 'changed' in chtype:
109-
print(f"<< - >> {path} value changed from '{change.t1}' to '{change.t2}'")
110-
else:
111-
print(f"{Fore.RED}{chtype} {path} {change}{Style.RESET_ALL}")
112-
113-
rest = diffs.pop('', None)
114-
if rest:
115-
print(f"{Fore.GREEN}Changes:{Style.RESET_ALL}")
116-
_printlines(rest)
117-
118-
for index in sorted(diffs):
119-
print(f"{Fore.GREEN}Index 0x{index:04x} ({index}){Style.RESET_ALL}")
120-
_printlines(diffs[index])
121-
122-
12390
@debug_wrapper()
12491
def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
12592
""" Main command dispatcher """
@@ -144,6 +111,7 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
144111
opt_debug = dict(action='store_true', help="Debug: enable tracebacks on errors")
145112
opt_od = dict(metavar='od', default=None, help="Object dictionary")
146113
opt_novalidate = dict(action='store_true', help="Don't validate input files")
114+
opt_nocolor = dict(action='store_true', help="Disable colored output")
147115

148116
parser.add_argument('--version', action='version', version='%(prog)s ' + objdictgen.__version__)
149117
parser.add_argument('--no-color', action='store_true', help="Disable colored output")
@@ -183,11 +151,13 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
183151
""", aliases=['compare'])
184152
subp.add_argument('od1', **opt_od) # type: ignore[arg-type]
185153
subp.add_argument('od2', **opt_od) # type: ignore[arg-type]
154+
subp.add_argument('--show', action="store_true", help="Show difference data")
186155
subp.add_argument('--internal', action="store_true", help="Diff internal object")
156+
subp.add_argument('--data', action="store_true", help="Show difference as data")
157+
subp.add_argument('--raw', action="store_true", help="Show raw difference")
158+
subp.add_argument('--no-color', **opt_nocolor) # type: ignore[arg-type]
187159
subp.add_argument('--novalidate', **opt_novalidate) # type: ignore[arg-type]
188-
subp.add_argument('--show', action="store_true", help="Show difference data")
189160
subp.add_argument('-D', '--debug', **opt_debug) # type: ignore[arg-type]
190-
subp.add_argument('--no-color', action='store_true', help="Disable colored output")
191161

192162
# -- EDIT --
193163
subp = subparser.add_parser('edit', help="""
@@ -210,9 +180,9 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
210180
subp.add_argument('--short', action="store_true", help="Do not list sub-index")
211181
subp.add_argument('--unused', action="store_true", help="Include unused profile parameters")
212182
subp.add_argument('--internal', action="store_true", help="Show internal data")
213-
subp.add_argument('-D', '--debug', **opt_debug) # type: ignore[arg-type]
183+
subp.add_argument('--no-color', **opt_nocolor) # type: ignore[arg-type]
214184
subp.add_argument('--novalidate', **opt_novalidate) # type: ignore[arg-type]
215-
subp.add_argument('--no-color', action='store_true', help="Disable colored output")
185+
subp.add_argument('-D', '--debug', **opt_debug) # type: ignore[arg-type]
216186

217187
# -- NETWORK --
218188
subp = subparser.add_parser('network', help="""
@@ -306,22 +276,22 @@ def main(debugopts: DebugOpts, args: Sequence[str]|None = None):
306276

307277
# -- DIFF command --
308278
elif opts.command in ("diff", "compare"):
279+
309280
od1 = open_od(opts.od1, validate=not opts.novalidate)
310281
od2 = open_od(opts.od2, validate=not opts.novalidate)
311282

312-
diffs = jsonod.diff_nodes(
313-
od1, od2, asdict=not opts.internal,
314-
validate=not opts.novalidate,
315-
)
283+
lines = list(format_diff_nodes(od1, od2, data=opts.data, raw=opts.raw,
284+
internal=opts.internal, show=opts.show))
285+
286+
for line in lines:
287+
print(line)
316288

317-
if diffs:
318-
errcode = 1
289+
errcode = 1 if lines else 0
290+
if errcode:
319291
print(f"{objdictgen.ODG_PROGRAM}: '{opts.od1}' and '{opts.od2}' differ")
320292
else:
321-
errcode = 0
322293
print(f"{objdictgen.ODG_PROGRAM}: '{opts.od1}' and '{opts.od2}' are equal")
323294

324-
print_diffs(diffs, show=opts.show)
325295
if errcode:
326296
parser.exit(errcode)
327297

src/objdictgen/jsonod.py

+55-52
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
1818
# USA
1919

20+
from __future__ import annotations
21+
2022
import copy
2123
import json
2224
import logging
@@ -36,7 +38,7 @@
3638
from objdictgen.typing import (TDiffNodes, TIndexEntry, TODJson, TODObjJson,
3739
TODObj, TODSubObj, TODSubObjJson, TODValue, TParamEntry, TPath, TProfileMenu)
3840
from objdictgen.utils import (copy_in_order, exc_amend, maybe_number,
39-
str_to_int)
41+
str_to_int, strip_brackets)
4042

4143
T = TypeVar('T')
4244
M = TypeVar('M', bound=Mapping)
@@ -173,6 +175,10 @@ class ValidationError(Exception):
173175
# Copied from https://github.com/NickolaiBeloguzov/jsonc-parser/blob/master/jsonc_parser/parser.py#L11-L39
174176
RE_JSONC = re.compile(r"(\".*?(?<!\\)\"|\'.*?\')|(\s*/\*.*?\*/\s*|\s*//[^\r\n]*$)", re.MULTILINE | re.DOTALL)
175177

178+
# Regexs to handle parsing of diffing the JSON
179+
RE_DIFF_ROOT = re.compile(r"^(root(\[.*?\]))(.*)")
180+
RE_DIFF_INDEX = re.compile(r"\['dictionary'\]\[(\d+)\](.*)")
181+
176182

177183
def remove_jsonc(text: str) -> str:
178184
""" Remove jsonc annotations """
@@ -1394,67 +1400,64 @@ def _validate_dictionary(index, obj):
13941400
raise
13951401

13961402

1397-
def diff_nodes(node1: "Node", node2: "Node", asdict=True, validate=True) -> TDiffNodes:
1403+
def diff(node1: Node, node2: Node, internal=False) -> TDiffNodes:
13981404
"""Compare two nodes and return the differences."""
13991405

14001406
diffs: dict[int|str, list] = {}
14011407

1402-
if asdict:
1403-
jd1 = node_todict(node1, sort=True, validate=validate)
1404-
jd2 = node_todict(node2, sort=True, validate=validate)
1408+
if internal:
14051409

1406-
dt = datetime.isoformat(datetime.now())
1407-
jd1['$date'] = jd2['$date'] = dt
1410+
# Simply diff the python data structure for the nodes
1411+
diff = deepdiff.DeepDiff(node1.__dict__, node2.__dict__, exclude_paths=[
1412+
"IndexOrder"
1413+
], view='tree')
14081414

1409-
# DeepDiff does not have typing, but the diff object is a dict-like object
1410-
# DeepDiff[str, deepdiff.model.PrettyOrderedSet].
1411-
# PrettyOrderedSet is a list-like object
1412-
# PrettyOrderedSet[deepdiff.model.DiffLevel]
1415+
else:
14131416

1414-
diff = deepdiff.DeepDiff(jd1, jd2, exclude_paths=[
1415-
"root['dictionary']"
1416-
], view='tree')
1417+
# Don't use rich format for diffing, as it will contain comments which confuse the output
1418+
jd1 = node_todict(node1, sort=True, rich=False, internal=True)
1419+
jd2 = node_todict(node2, sort=True, rich=False, internal=True)
14171420

1418-
chtype: str
1419-
for chtype, changes in diff.items():
1420-
change: deepdiff.model.DiffLevel
1421-
for change in changes:
1422-
path: str = change.path(force='fake') # pyright: ignore[reportAssignmentType]
1423-
entries = diffs.setdefault('', [])
1424-
entries.append((chtype, change, path.replace('root', '')))
1425-
1426-
diff = deepdiff.DeepDiff(jd1['dictionary'], jd2['dictionary'], view='tree', group_by='index')
1427-
1428-
res = re.compile(r"root\[('0x[0-9a-fA-F]+'|\d+)\]")
1429-
1430-
for chtype, changes in diff.items():
1431-
for change in changes:
1432-
path = change.path(force='fake') # pyright: ignore[reportAssignmentType]
1433-
m = res.search(path)
1434-
if m:
1435-
num = str_to_int(m.group(1).strip("'"))
1436-
entries = diffs.setdefault(num, [])
1437-
entries.append((chtype, change, path.replace(m.group(0), '')))
1438-
else:
1439-
entries = diffs.setdefault('', [])
1440-
entries.append((chtype, change, path.replace('root', '')))
1421+
# Convert the dictionary list to a dict to ensure the order of the objects
1422+
jd1["dictionary"] = {obj["index"]: obj for obj in jd1["dictionary"]}
1423+
jd2["dictionary"] = {obj["index"]: obj for obj in jd2["dictionary"]}
14411424

1442-
else:
1443-
diff = deepdiff.DeepDiff(node1.__dict__, node2.__dict__, exclude_paths=[
1444-
"root.IndexOrder"
1445-
], view='tree')
1425+
# Diff the two nodes in json object format
1426+
diff = deepdiff.DeepDiff(jd1, jd2, view='tree')
1427+
1428+
# Iterate over the changes
1429+
for chtype, changes in diff.items():
1430+
for change in changes:
1431+
path = change.path()
1432+
1433+
# Match the root[<obj>]... part of the path
1434+
m = RE_DIFF_ROOT.match(path)
1435+
if not m:
1436+
raise ValueError(f"Unexpected path '{path}' in compare")
14461437

1447-
res = re.compile(r"root\.(Profile|Dictionary|ParamsDictionary|UserMapping|DS302)\[(\d+)\]")
1438+
# Path is the display path, root the categorization
1439+
path = m[2] + m[3]
1440+
root = m[2]
1441+
1442+
if not internal:
1443+
if m[1] == "root['dictionary']":
1444+
# Extract the index from the path
1445+
m = RE_DIFF_INDEX.match(path)
1446+
root = f"Index {m[1]}"
1447+
path = m[2]
14481448

1449-
for chtype, changes in diff.items():
1450-
for change in changes:
1451-
path = change.path(force='fake') # pyright: ignore[reportAssignmentType]
1452-
m = res.search(path)
1453-
if m:
1454-
entries = diffs.setdefault(int(m.group(2)), [])
1455-
entries.append((chtype, change, path.replace(m.group(0), m.group(1))))
14561449
else:
1457-
entries = diffs.setdefault('', [])
1458-
entries.append((chtype, change, path.replace('root.', '')))
1450+
root = "Header fields"
1451+
1452+
# Append the change to the list of changes
1453+
entries = diffs.setdefault(strip_brackets(root), [])
1454+
entries.append((chtype, change, strip_brackets(path)))
1455+
1456+
# Ensure the Index entries are sorted correctly
1457+
def _sort(text):
1458+
if text.startswith("Index "):
1459+
return f"zz 0x{int(text[6:]):04x}"
1460+
return text
14591461

1460-
return diffs
1462+
# Sort the entries
1463+
return {k: diffs[k] for k in sorted(diffs, key=_sort)}

0 commit comments

Comments
 (0)