esrally/utils/pretty.py (45 lines of code) (raw):
# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import difflib
import enum
import json
import re
from collections import abc
from typing import Any
class Flag(enum.Flag):
FLAT_DICT = enum.auto()
DUMP_EQUALS = enum.auto()
def dump(o: Any, flags: Flag = Flag(0)) -> str:
"""dump creates a human-readable multiline text to make easy to visualize the content of a JSON like object.
:param o: the object the dump has to be obtained from.
:param flags:
flags & FLAT_DICT != 0: it will squash nested objects to make simple reading them.
:return: JSON human-readable multiline text representation of the input object.
"""
lines: abc.Sequence[str] = _dump(o, flags)
return "\n".join(lines)
_HAS_DIFF = re.compile(r"^\+ ", flags=re.MULTILINE)
def diff(old: Any, new: Any, flags: Flag = Flag(0)) -> str:
"""diff creates a human-readable multiline text to make easy to visualize the difference of content between two JSON like object.
:param old: the old object the diff dump has to be obtained from.
:param new: the new object the diff dump has to be obtained from.
:param flags:
flags & Flags.FLAT_DICT: it squashes nested objects to make simple reading them;
flags & Flags.DUMP_EQUALS: in case there is no difference it will print the same as dump function.
:return: JSON human-readable multiline text representation of the difference between input objects, if any, or '' otherwise.
"""
if Flag.DUMP_EQUALS not in flags and old == new:
return ""
ret = "\n".join(difflib.ndiff(_dump(old, flags), _dump(new, flags)))
if Flag.DUMP_EQUALS not in flags and _HAS_DIFF.search(ret) is None:
return ""
return ret
def _dump(o: Any, flags: Flag) -> abc.Sequence[str]:
"""Lower level wrapper to json.dump method"""
if Flag.FLAT_DICT in flags:
# It reduces nested dictionary to a flat one to improve readability.
o = flat(o)
return json.dumps(o, indent=2, sort_keys=True).splitlines()
def flat(o: Any) -> dict[str, str]:
"""Given a JSON like object, it produces a key value flat dictionary of strings easy to read and compare.
:param o: a JSON like object
:return: a flat dictionary
"""
return dict(_flat(o))
def _flat(o: Any) -> abc.Generator[tuple[str, str], None, None]:
"""Recursive helper function generating the content for the flat dictionary.
:param o: a JSON like object
:return: a generator of (key, value) pairs.
"""
if isinstance(o, (str, bytes)):
yield "", str(o)
elif isinstance(o, abc.Mapping):
for k1, v1 in o.items():
for k2, v2 in _flat(v1):
if k2:
yield f"{k1}.{k2}", v2
else:
yield k1, v2
elif isinstance(o, abc.Sequence):
for k1, v1 in enumerate(o):
for k2, v2 in _flat(v1):
if k2:
yield f"{k1}.{k2}", v2
else:
yield str(k1), v2
else:
yield "", json.dumps(o)