Skip to content

Commit

Permalink
cli: handle deserialization errors
Browse files Browse the repository at this point in the history
For the CLI tool, typical user input should not produce stack traces. Show appropriate messages when one or both file
inputs are invalid or do not exist. This implementation respects the current API by not introducing a unified exception
type directly in the loader implementations. Current library consumers should not be broken by this change but cli users
should see a prettier output when things go wrong.

Fixes #71
  • Loading branch information
corytodd authored Sep 6, 2023
2 parents 00e3dc8 + a33a371 commit 6886210
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 30 deletions.
51 changes: 50 additions & 1 deletion jsondiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import json
import yaml

from json import JSONDecodeError
from yaml import YAMLError

from .symbols import *
from .symbols import Symbol

Expand Down Expand Up @@ -55,6 +58,10 @@ def __init__(self, **kwargs):
self.kwargs = kwargs

def __call__(self, src):
"""Parse and return JSON data
:param src: str|file-like source
:return: dict parsed data
"""
if isinstance(src, string_types):
return json.loads(src, **self.kwargs)
else:
Expand All @@ -74,6 +81,47 @@ def __call__(self, src):
"""
return yaml.safe_load(src)

class Serializer:
"""Serializer helper loads and stores object data
:param file_format: str json or yaml
:param indent: int Output indentation in spaces
:raise ValueError: file_path does not contains valid file_format data
"""

def __init__(self, file_format, indent):
# pyyaml _can_ load json but is ~20 times slower and has known issues so use
# the json from stdlib when json is specified.
self.serializers = {
"json": (JsonLoader(), JsonDumper(indent=indent)),
"yaml": (YamlLoader(), YamlDumper(indent=indent)),
}
self.file_format = file_format
if file_format not in self.serializers:
raise ValueError(f"Unsupported serialization format {file_format}, expected one of {self.serializers.keys()}")

def deserialize_file(self, src):
"""Deserialize file from the specified format
:param file_path: str path to file
:param src: str|file-like source
:return dict
:raise ValueError: file_path does not contain valid file_format data
"""
loader, _ = self.serializers[self.file_format]
try:
parsed = loader(src)
except (JSONDecodeError, YAMLError) as ex:
raise ValueError(f"Invalid {self.file_format} file") from ex
return parsed

def serialize_data(self, obj, stream):
"""Serialize obj and write to stream
:param obj: dict to serialize
:param stream: Writeable stream
"""
_, dumper = self.serializers[self.file_format]
dumper(obj, stream)


class JsonDiffSyntax(object):
def emit_set_diff(self, a, b, s, added, removed):
raise NotImplementedError()
Expand Down Expand Up @@ -667,5 +715,6 @@ def similarity(a, b, cls=JsonDiffer, **kwargs):
"JsonDumper",
"JsonLoader",
"YamlDumper",
"YamlLoader"
"YamlLoader",
"Serializer",
]
66 changes: 37 additions & 29 deletions jsondiff/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
import jsondiff
import sys

def load_file(serializer, file_path):
with open(file_path, "r") as f:
parsed = None
try:
parsed = serializer.deserialize_file(f)
except ValueError:
print(f"{file_path} is not valid {serializer.file_format}")
except FileNotFoundError:
print(f"{file_path} does not exist")
return parsed

def main():
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Expand All @@ -18,35 +28,33 @@ def main():

args = parser.parse_args()

# pyyaml _can_ load json but is ~20 times slower and has known issues so use
# the json from stdlib when json is specified.
serializers = {
"json": (jsondiff.JsonLoader(), jsondiff.JsonDumper(indent=args.indent)),
"yaml": (jsondiff.YamlLoader(), jsondiff.YamlDumper(indent=args.indent)),
}
loader, dumper = serializers[args.format]

with open(args.first, "r") as f:
with open(args.second, "r") as g:
jf = loader(f)
jg = loader(g)
if args.patch:
x = jsondiff.patch(
jf,
jg,
marshal=True,
syntax=args.syntax
)
else:
x = jsondiff.diff(
jf,
jg,
marshal=True,
syntax=args.syntax
)

dumper(x, sys.stdout)
serializer = jsondiff.Serializer(args.format, args.indent)

parsed_first = load_file(serializer, args.first)
parsed_second = load_file(serializer, args.second)

if not (parsed_first and parsed_second):
return 1

if args.patch:
x = jsondiff.patch(
parsed_first,
parsed_second,
marshal=True,
syntax=args.syntax
)
else:
x = jsondiff.diff(
parsed_first,
parsed_second,
marshal=True,
syntax=args.syntax
)

serializer.serialize_data(x, sys.stdout)

return 0

if __name__ == '__main__':
main()
ret = main()
sys.exit(ret)

0 comments on commit 6886210

Please sign in to comment.