292 lines
9.4 KiB
Python
292 lines
9.4 KiB
Python
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
# Licensed under the MIT License. See LICENSE in the project root
|
|
# for license information.
|
|
|
|
"""Improved JSON serialization.
|
|
"""
|
|
|
|
import builtins
|
|
import json
|
|
import numbers
|
|
import operator
|
|
|
|
|
|
JsonDecoder = json.JSONDecoder
|
|
|
|
|
|
class JsonEncoder(json.JSONEncoder):
|
|
"""Customizable JSON encoder.
|
|
|
|
If the object implements __getstate__, then that method is invoked, and its
|
|
result is serialized instead of the object itself.
|
|
"""
|
|
|
|
def default(self, value):
|
|
try:
|
|
get_state = value.__getstate__
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
return get_state()
|
|
return super().default(value)
|
|
|
|
|
|
class JsonObject(object):
|
|
"""A wrapped Python object that formats itself as JSON when asked for a string
|
|
representation via str() or format().
|
|
"""
|
|
|
|
json_encoder_factory = JsonEncoder
|
|
"""Used by __format__ when format_spec is not empty."""
|
|
|
|
json_encoder = json_encoder_factory(indent=4)
|
|
"""The default encoder used by __format__ when format_spec is empty."""
|
|
|
|
def __init__(self, value):
|
|
assert not isinstance(value, JsonObject)
|
|
self.value = value
|
|
|
|
def __getstate__(self):
|
|
raise NotImplementedError
|
|
|
|
def __repr__(self):
|
|
return builtins.repr(self.value)
|
|
|
|
def __str__(self):
|
|
return format(self)
|
|
|
|
def __format__(self, format_spec):
|
|
"""If format_spec is empty, uses self.json_encoder to serialize self.value
|
|
as a string. Otherwise, format_spec is treated as an argument list to be
|
|
passed to self.json_encoder_factory - which defaults to JSONEncoder - and
|
|
then the resulting formatter is used to serialize self.value as a string.
|
|
|
|
Example::
|
|
|
|
format("{0} {0:indent=4,sort_keys=True}", json.repr(x))
|
|
"""
|
|
if format_spec:
|
|
# At this point, format_spec is a string that looks something like
|
|
# "indent=4,sort_keys=True". What we want is to build a function call
|
|
# from that which looks like:
|
|
#
|
|
# json_encoder_factory(indent=4,sort_keys=True)
|
|
#
|
|
# which we can then eval() to create our encoder instance.
|
|
make_encoder = "json_encoder_factory(" + format_spec + ")"
|
|
encoder = eval(
|
|
make_encoder, {"json_encoder_factory": self.json_encoder_factory}
|
|
)
|
|
else:
|
|
encoder = self.json_encoder
|
|
return encoder.encode(self.value)
|
|
|
|
|
|
# JSON property validators, for use with MessageDict.
|
|
#
|
|
# A validator is invoked with the actual value of the JSON property passed to it as
|
|
# the sole argument; or if the property is missing in JSON, then () is passed. Note
|
|
# that None represents an actual null in JSON, while () is a missing value.
|
|
#
|
|
# The validator must either raise TypeError or ValueError describing why the property
|
|
# value is invalid, or else return the value of the property, possibly after performing
|
|
# some substitutions - e.g. replacing () with some default value.
|
|
|
|
|
|
def _converter(value, classinfo):
|
|
"""Convert value (str) to number, otherwise return None if is not possible"""
|
|
for one_info in classinfo:
|
|
if issubclass(one_info, numbers.Number):
|
|
try:
|
|
return one_info(value)
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
def of_type(*classinfo, **kwargs):
|
|
"""Returns a validator for a JSON property that requires it to have a value of
|
|
the specified type. If optional=True, () is also allowed.
|
|
|
|
The meaning of classinfo is the same as for isinstance().
|
|
"""
|
|
|
|
assert len(classinfo)
|
|
optional = kwargs.pop("optional", False)
|
|
assert not len(kwargs)
|
|
|
|
def validate(value):
|
|
if (optional and value == ()) or isinstance(value, classinfo):
|
|
return value
|
|
else:
|
|
converted_value = _converter(value, classinfo)
|
|
if converted_value:
|
|
return converted_value
|
|
|
|
if not optional and value == ():
|
|
raise ValueError("must be specified")
|
|
raise TypeError("must be " + " or ".join(t.__name__ for t in classinfo))
|
|
|
|
return validate
|
|
|
|
|
|
def default(default):
|
|
"""Returns a validator for a JSON property with a default value.
|
|
|
|
The validator will only allow property values that have the same type as the
|
|
specified default value.
|
|
"""
|
|
|
|
def validate(value):
|
|
if value == ():
|
|
return default
|
|
elif isinstance(value, type(default)):
|
|
return value
|
|
else:
|
|
raise TypeError("must be {0}".format(type(default).__name__))
|
|
|
|
return validate
|
|
|
|
|
|
def enum(*values, **kwargs):
|
|
"""Returns a validator for a JSON enum.
|
|
|
|
The validator will only allow the property to have one of the specified values.
|
|
|
|
If optional=True, and the property is missing, the first value specified is used
|
|
as the default.
|
|
"""
|
|
|
|
assert len(values)
|
|
optional = kwargs.pop("optional", False)
|
|
assert not len(kwargs)
|
|
|
|
def validate(value):
|
|
if optional and value == ():
|
|
return values[0]
|
|
elif value in values:
|
|
return value
|
|
else:
|
|
raise ValueError("must be one of: {0!r}".format(list(values)))
|
|
|
|
return validate
|
|
|
|
|
|
def array(validate_item=False, vectorize=False, size=None):
|
|
"""Returns a validator for a JSON array.
|
|
|
|
If the property is missing, it is treated as if it were []. Otherwise, it must
|
|
be a list.
|
|
|
|
If validate_item=False, it's treated as if it were (lambda x: x) - i.e. any item
|
|
is considered valid, and is unchanged. If validate_item is a type or a tuple,
|
|
it's treated as if it were json.of_type(validate).
|
|
|
|
Every item in the list is replaced with validate_item(item) in-place, propagating
|
|
any exceptions raised by the latter. If validate_item is a type or a tuple, it is
|
|
treated as if it were json.of_type(validate_item).
|
|
|
|
If vectorize=True, and the value is neither a list nor a dict, it is treated as
|
|
if it were a single-element list containing that single value - e.g. "foo" is
|
|
then the same as ["foo"]; but {} is an error, and not [{}].
|
|
|
|
If size is not None, it can be an int, a tuple of one int, a tuple of two ints,
|
|
or a set. If it's an int, the array must have exactly that many elements. If it's
|
|
a tuple of one int, it's the minimum length. If it's a tuple of two ints, they
|
|
are the minimum and the maximum lengths. If it's a set, it's the set of sizes that
|
|
are valid - e.g. for {2, 4}, the array can be either 2 or 4 elements long.
|
|
"""
|
|
|
|
if not validate_item:
|
|
validate_item = lambda x: x
|
|
elif isinstance(validate_item, type) or isinstance(validate_item, tuple):
|
|
validate_item = of_type(validate_item)
|
|
|
|
if size is None:
|
|
validate_size = lambda _: True
|
|
elif isinstance(size, set):
|
|
size = {operator.index(n) for n in size}
|
|
validate_size = lambda value: (
|
|
len(value) in size
|
|
or "must have {0} elements".format(
|
|
" or ".join(str(n) for n in sorted(size))
|
|
)
|
|
)
|
|
elif isinstance(size, tuple):
|
|
assert 1 <= len(size) <= 2
|
|
size = tuple(operator.index(n) for n in size)
|
|
min_len, max_len = (size + (None,))[0:2]
|
|
validate_size = lambda value: (
|
|
"must have at least {0} elements".format(min_len)
|
|
if len(value) < min_len
|
|
else "must have at most {0} elements".format(max_len)
|
|
if max_len is not None and len(value) < max_len
|
|
else True
|
|
)
|
|
else:
|
|
size = operator.index(size)
|
|
validate_size = lambda value: (
|
|
len(value) == size or "must have {0} elements".format(size)
|
|
)
|
|
|
|
def validate(value):
|
|
if value == ():
|
|
value = []
|
|
elif vectorize and not isinstance(value, (list, dict)):
|
|
value = [value]
|
|
|
|
of_type(list)(value)
|
|
|
|
size_err = validate_size(value) # True if valid, str if error
|
|
if size_err is not True:
|
|
raise ValueError(size_err)
|
|
|
|
for i, item in enumerate(value):
|
|
try:
|
|
value[i] = validate_item(item)
|
|
except (TypeError, ValueError) as exc:
|
|
raise type(exc)(f"[{repr(i)}] {exc}")
|
|
return value
|
|
|
|
return validate
|
|
|
|
|
|
def object(validate_value=False):
|
|
"""Returns a validator for a JSON object.
|
|
|
|
If the property is missing, it is treated as if it were {}. Otherwise, it must
|
|
be a dict.
|
|
|
|
If validate_value=False, it's treated as if it were (lambda x: x) - i.e. any
|
|
value is considered valid, and is unchanged. If validate_value is a type or a
|
|
tuple, it's treated as if it were json.of_type(validate_value).
|
|
|
|
Every value in the dict is replaced with validate_value(value) in-place, propagating
|
|
any exceptions raised by the latter. If validate_value is a type or a tuple, it is
|
|
treated as if it were json.of_type(validate_value). Keys are not affected.
|
|
"""
|
|
|
|
if isinstance(validate_value, type) or isinstance(validate_value, tuple):
|
|
validate_value = of_type(validate_value)
|
|
|
|
def validate(value):
|
|
if value == ():
|
|
return {}
|
|
|
|
of_type(dict)(value)
|
|
if validate_value:
|
|
for k, v in value.items():
|
|
try:
|
|
value[k] = validate_value(v)
|
|
except (TypeError, ValueError) as exc:
|
|
raise type(exc)(f"[{repr(k)}] {exc}")
|
|
return value
|
|
|
|
return validate
|
|
|
|
|
|
def repr(value):
|
|
return JsonObject(value)
|
|
|
|
|
|
dumps = json.dumps
|
|
loads = json.loads
|