# SPDX-License-Identifier: MIT OR GPL-3.0-or-later import difflib import math from collections.abc import Mapping from json import dumps as json_dumps from . import convert from .convert import Json from .logger import get_logger logger = get_logger(__name__) def make_json(v: object) -> Json: if v is None: return None if isinstance(v, int): return v if isinstance(v, float): if not math.isfinite(v): logger.warning("%s %s is non-finite value", v, type(v)) return 0.0 return v if isinstance(v, bool): return v if isinstance(v, str): return v mapping = convert.mapping_or_none(v) if mapping is not None: dict_result: dict[str, Json] = {} for key, value in mapping.items(): if isinstance(key, str): dict_result[key] = make_json(value) continue logger.warning("%s %s is unrecognized type for dict key", key, type(key)) return dict_result iterator = convert.iterator_or_none(v) if iterator is not None: return [make_json(x) for x in iterator] logger.warning("%s %s is unrecognized type", v, type(v)) return None def make_json_dict(v: Mapping[str, object]) -> dict[str, Json]: return {key: make_json(value) for key, value in v.items()} def diff( left: Json, right: Json, float_tolerance: float = 0, path: str = "", ) -> list[str]: if isinstance(left, list): if not isinstance(right, list): return [f"{path}: left is list but right is {type(right)}"] if len(left) != len(right): result = [ f"{path}: left length is {len(left)} but right length is {len(right)}" ] right_json_str = json_dumps(right, indent=4, sort_keys=True) right_json_lines = right_json_str.splitlines(keepends=True) left_json_str = json_dumps(left, indent=4, sort_keys=True) left_json_lines = left_json_str.splitlines(keepends=True) if len(right_json_lines) > 1000 or len(left_json_lines) > 1000: return result unified_diff = [ line.rstrip() for line in difflib.unified_diff( right_json_lines, left_json_lines, f"{path}/right", f"{path}/left", ) ] if len(unified_diff) > 1000: return result return result + unified_diff diffs: list[str] = [] for i, (left_child, right_child) in enumerate(zip(left, right)): diffs.extend(diff(left_child, right_child, float_tolerance, f"{path}[{i}]")) return diffs if isinstance(left, dict): if not isinstance(right, dict): return [f"{path}: left is dict but right is {type(right)}"] diffs = [] for key in sorted(set(list(left.keys()) + list(right.keys()))): if key not in left: diffs.append(f'{path}: {key} not in left. right["{key}"]={right[key]}') continue if key not in right: diffs.append(f'{path}: {key} not in right, left["{key}"]={left[key]}') continue diffs.extend( diff(left[key], right[key], float_tolerance, f'{path}["{key}"]') ) return diffs if isinstance(left, bool): if not isinstance(right, bool): return [f"{path}: left is bool but right is {type(right)}"] if left != right: return [f"{path}: left is {left} but right is {right}"] return [] if isinstance(left, str): if not isinstance(right, str): return [f"{path}: left is str but right is {type(right)}"] if left != right: return [f'{path}: left is "{left}" but right is "{right}"'] return [] if left is None and right is not None: return [f"{path}: left is None but right is {type(right)}"] if isinstance(left, int) and isinstance(right, int): if left != right: return [f"{path}: left is {left} but right is {right}"] return [] if isinstance(left, (int, float)) and isinstance(right, (int, float)): if (isinstance(left, float) and not math.isfinite(left)) or ( isinstance(right, float) and not math.isfinite(right) ): return [ f"{path}: left is {left}" + f" but right is {right}, They are not comparable numbers" ] error = math.fabs(float(left) - float(right)) if error > float_tolerance: return [ f"{path}: left is {float(left):20.17f}" + f" but right is {float(right):20.17f}, error={error:19.17f}" ] return [] message = f"{path}: unexpected type left={type(left)} right={type(right)}" raise ValueError(message)