8000 Introduce tools for pretty printing JSON like objects and their differences. by fressi-elastic · Pull Request #1924 · elastic/rally · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
8000

Introduce tools for pretty printing JSON like objects and their differences. #1924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions esrally/utils/pretty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ module = [
"esrally.utils.cases",
"esrally.utils.modules",
"esrally.utils.io",
"esrally.utils.pretty",
"esrally.utils.process",
]
disallow_incomplete_defs = true
Expand Down
191 changes: 191 additions & 0 deletions tests/utils/pretty_test.py
4412
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# 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.
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from esrally.utils import cases, pretty


@dataclass
class DumpCase:
o: Any
want: str
flags: pretty.Flag | None = None


@cases.cases(
# fmt: off
null=DumpCase(None, "null"),
string=DumpCase("string", '"string"'),
integer=DumpCase(1, "1"),
float=DumpCase(1.0, "1.0"),
list=DumpCase(
[1, 2, 3],
"[\n"
" 1,\n"
" 2,\n"
" 3\n"
"]"
),
tuple=DumpCase(
(2, 3),
"[\n"
" 2,\n"
" 3\n"
"]"
),
object=DumpCase(
{"a": "a", "b": 2},
'{\n'
' "a": "a",\n'
' "b": 2\n'
'}'
),
flat_dict=DumpCase(
{"a": {"b": "c"}},
'{\n'
' "a.b": "c"\n'
'}',
flags=pretty.Flag.FLAT_DICT
),
# fmt: on
)
def test_dump(case: DumpCase):
params: dict[str, Any] = {}
if case.flags is not None:
params["flags"] = case.flags
got = pretty.dump(case.o, **params)
assert got == case.want


@dataclass
class DiffCase:
old: Any
new: Any
want: str
flags: pretty.Flag | None = None


@cases.cases(
# fmt: off
none_and_none=DiffCase(None, None, ""),
none_and_string=DiffCase(
None,
"something",
'- null\n'
'+ "something"'
),
strings=DiffCase(
"cat",
"cut",
'- "cat"\n'
'? ^\n'
'\n'
'+ "cut"\n'
'? ^\n'
),
equal_strings=DiffCase("same", "same", ""),
integers=DiffCase(
123,
132,
"- 123\n"
"+ 132"
),
equal_integers=DiffCase(42, 42, ""),
floats=DiffCase(
1.23,
13.2,
"- 1.23\n"
"? -\n"
"\n"
"+ 13.2\n"
"? +\n"
),
equal_floats=DiffCase(3.140, 3.14e0, ""),
float_and_integer=DiffCase(1.0, 1, ""),
lists=DiffCase(
[1, 2, 3],
[1, 3, 4],
" [\n"
" 1,\n"
"- 2,\n"
"- 3\n"
"+ 3,\n"
"? +\n"
"\n"
"+ 4\n"
" ]"),
equal_lists=DiffCase([2, 3], [2, 3], ""),
tuples=DiffCase(
(1, 2, 3),
(1, 3, 4),
" [\n"
" 1,\n"
"- 2,\n"
"- 3\n"
"+ 3,\n"
"? +\n"
"\n"
"+ 4\n"
" ]"
),
equal_tuples=DiffCase((2, 3), (2, 3), ""),
list_and_tuples=DiffCase((3, 4), [3, 4], ""),
objects=DiffCase(
{"a": 1, "b": 2},
{"b": 2, "c": 3},
' {\n'
'- "a": 1,\n'
'- "b": 2\n'
'+ "b": 2,\n'
'? +\n'
'\n'
'+ "c": 3\n'
' }'
),
flat_dict=DiffCase(
{"a": {"b": "c"}},
{"a": {"c": "d"}},
' {\n'
'- "a.b": "c"\n'
'? ^ ^\n'
'\n'
'+ "a.c": "d"\n'
'? ^ ^\n'
'\n'
' }',
flags=pretty.Flag.FLAT_DICT,
),
dump_equals=DiffCase(
{"a": 1, "b": 2},
{"a": 1, "b": 2},
' {\n'
' "a": 1,\n'
' "b": 2\n'
' }',
flags=pretty.Flag.DUMP_EQUALS,
),
# fmt: on
)
def test_diff(case: DiffCase):
params: dict[str, Any] = {}
if case.flags is not None:
params["flags"] = case.flags
got = pretty.diff(case.old, case.new, **params)
assert got == case.want
0