| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- # 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
|