"""Customizable JSON library for Python."""
from __future__ import annotations
__all__: list[str] = [
"Decoder",
"Encoder",
"JSONSyntaxError",
"Manipulator",
"TruncatedSyntaxError",
"apply_filter",
"apply_patch",
"detect_encoding",
"dump",
"dumps",
"format_syntax_error",
"load",
"load_query_value",
"loads",
"make_patch",
"read",
"select_nodes",
"write",
]
__version__: str = "2.5.0"
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
from jsonyx._decoder import (
Decoder, JSONSyntaxError, TruncatedSyntaxError, detect_encoding,
)
from jsonyx._differ import make_patch
from jsonyx._encoder import Encoder
from jsonyx._manipulator import Manipulator
from jsonyx.allow import NOTHING
if TYPE_CHECKING:
from collections.abc import Callable, Container
from os import PathLike
_T_co = TypeVar("_T_co", covariant=True)
_T_contra = TypeVar("_T_contra", contravariant=True)
# pylint: disable-next=R0903
class Reader(Protocol[_T_co]):
"""Reader."""
def read(self, length: int = ..., /) -> _T_co: # type: ignore
"""Read string."""
Reader.__module__ = "io"
# pylint: disable-next=R0903
class Writer(Protocol[_T_contra]):
"""Writer."""
def write(self, s: _T_contra, /) -> object:
"""Write string."""
Writer.__module__ = "io"
_ClassInfo = type | tuple["_ClassInfo", ...]
_Formatter = Callable[[object], str]
_Node = tuple[dict[Any, Any] | list[Any], Any]
_Operation = dict[str, Any]
_StrPath = PathLike[str] | str
_Hook = Callable[[Any], Any]
[docs]
def read(
filename: _StrPath,
*,
allow: Container[str] = NOTHING,
cache_keys: bool = False,
hooks: dict[str, _Hook] | None = None,
) -> Any:
"""Deserialize a JSON file to a Python object.
.. versionchanged:: 2.0 Replaced ``use_decimal`` with ``hooks``.
.. versionchanged:: 2.2 Added ``cache_keys`` (default ``False`` instead of
``True``).
:param filename: the path to the JSON file
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param cache_keys: re-use the keys of objects
:param hooks: the :ref:`hooks <decoding_hooks>` used for transforming data
:raises OSError: if the file can't be opened
:raises RecursionError: if the JSON file is too deeply nested
:raises TruncatedSyntaxError: when failing to decode the file
:raises ValueError: if a number is too big
:return: a Python object.
Example:
>>> import jsonyx as json
>>> from os.path import join
>>> from tempfile import TemporaryDirectory
>>> with TemporaryDirectory() as tmpdir:
... filename = join(tmpdir, "file.json")
... with open(filename, "w", encoding="utf-8") as fp:
... _ = fp.write('["filesystem API"]')
... json.load(filename)
...
['filesystem API']
.. tip:: Use :class:`jsonyx.Decoder` directly for better performance.
.. seealso::
- :func:`jsonyx.load` for deserializing from an open file.
- :func:`jsonyx.loads` for deserializing from a string or bytes.
- :func:`jsonyx.write` for serializing to a file.
"""
return Decoder(
allow=allow, cache_keys=cache_keys, hooks=hooks,
).read(filename)
[docs]
def load(
fp: Reader[bytes | str],
*,
allow: Container[str] = NOTHING,
cache_keys: bool = False,
hooks: dict[str, _Hook] | None = None,
root: _StrPath = ".",
) -> Any:
"""Deserialize an open JSON file to a Python object.
.. versionchanged:: 2.0 Replaced ``use_decimal`` with ``hooks``.
.. versionchanged:: 2.2 Added ``cache_keys`` (default ``False`` instead of
``True``).
:param fp: an open JSON file
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param cache_keys: re-use the keys of objects
:param hooks: the :ref:`hooks <decoding_hooks>` used for transforming data
:param root: the path to the archive containing this JSON file
:raises RecursionError: if the JSON file is too deeply nested
:raises TruncatedSyntaxError: when failing to decode the file
:raises ValueError: if a number is too big
:return: a Python object
Example:
>>> import jsonyx as json
>>> from io import StringIO
>>> io = StringIO('["reader protocol"]')
>>> json.load(io)
['reader protocol']
.. tip::
- Specify ``root`` to display the zip filename in error messages.
- Use :class:`jsonyx.Decoder` directly for better performance.
.. note::
- The input is not read incrementally, but in one-shot.
- The encoding is detected using :func:`jsonyx.detect_encoding`.
.. seealso::
- :func:`jsonyx.read` for deserializing from a file.
- :func:`jsonyx.loads` for deserializing from a string or bytes.
- :func:`jsonyx.dump` for serializing to an open file.
"""
return Decoder(
allow=allow, cache_keys=cache_keys, hooks=hooks,
).load(fp, root=root)
[docs]
def loads(
s: bytearray | bytes | str,
*,
allow: Container[str] = NOTHING,
cache_keys: bool = False,
filename: _StrPath = "<string>",
hooks: dict[str, _Hook] | None = None,
) -> Any:
r"""Deserialize a JSON string or bytes to a Python object.
.. versionchanged:: 2.0 Replaced ``use_decimal`` with ``hooks``.
.. versionchanged:: 2.2 Added ``cache_keys`` (default ``False`` instead of
``True``).
:param s: a JSON string or bytes
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param cache_keys: re-use the keys of objects
:param filename: the path to the JSON file
:param hooks: the :ref:`hooks <decoding_hooks>` used for transforming data
:raises RecursionError: if the JSON string is too deeply nested
:raises TruncatedSyntaxError: when failing to decode the string
:raises ValueError: if a number is too big
:return: a Python object
Example:
>>> import jsonyx as json
>>> json.loads('{"foo": ["bar", null, 1.0, 2]}')
{'foo': ['bar', None, 1.0, 2]}
>>> json.loads(r'"\"foo\bar"')
'"foo\x08ar'
>>> json.loads(b'\x00"\x00f\x00o\x00o\x00"')
'foo'
.. tip::
- Specify ``filename`` to display the filename in error messages.
- Use :class:`jsonyx.Decoder` directly for better performance.
.. note:: The encoding is detected using :func:`jsonyx.detect_encoding`.
.. seealso::
- :func:`jsonyx.read` for deserializing from a file.
- :func:`jsonyx.load` for deserializing from an open file.
- :func:`jsonyx.dumps` for serializing to a string.
"""
return Decoder(
allow=allow, cache_keys=cache_keys, hooks=hooks,
).loads(s, filename=filename)
[docs]
def write(
obj: object,
filename: _StrPath,
encoding: str = "utf-8",
*,
allow: Container[str] = NOTHING,
check_circular: bool = True,
commas: bool = True,
end: str = "\n",
ensure_ascii: bool = False,
formatters: dict[str, _Formatter] | None = None,
hook: _Hook | None = None,
indent: int | str | None = None,
indent_leaves: bool = True,
max_indent_level: int | None = None,
quoted_keys: bool = True,
separators: tuple[str, str] = (", ", ": "),
skipkeys: bool = False,
sort_keys: bool = False,
trailing_comma: bool = False,
types: dict[str, _ClassInfo] | None = None,
) -> None:
r"""Serialize a Python object to a JSON file.
.. versionchanged:: 2.0
- Added ``commas``, ``encoding``, ``indent_leaves``,
``max_indent_level``, ``quoted_keys`` and ``types``.
- Allowed overriding serialization in subclasses of :class:`float` and
:class:`int` (e.g. :class:`enum.Enum`).
- Made :class:`tuple` serializable by default instead of
:class:`decimal.Decimal`.
- Replaced ``item_separator`` and ``key_separator`` with
``separators``.
.. versionchanged:: 2.1 Added ``check_circular``, ``hook`` and
``skipkeys``.
.. versionchanged:: 2.4
- Added ``formatters``.
- Allowed overriding serialization in subclasses of :class:`str` (e.g.
:class:`enum.Enum`).
- Made :class:`frozendict` serializable by default.
:param obj: a Python object
:param filename: the path to the JSON file
:param encoding: the JSON encoding
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param check_circular: check for circular references
:param commas: separate items by commas when indented
:param end: the string to append at the end
:param ensure_ascii: escape non-ASCII characters
:param formatters: a dictionary of :ref:`formatters <formatters>`
:param hook: the :ref:`hook <encoding_hook>` used for transforming data
:param indent: the number of spaces or string to indent with
:param indent_leaves: indent leaf objects and arrays
:param max_indent_level: the level up to which to indent
:param quoted_keys: quote keys which are :ref:`identifiers <identifiers>`
:param separators: the item and key separator
:param skipkeys: skip non-string keys
:param sort_keys: sort the keys of objects
:param trailing_comma: add a trailing comma when indented
:param types: a dictionary of :ref:`additional types <protocol_types>`
:raises OSError: if the file can't be opened
:raises RecursionError: if the object is too deeply nested
:raises TypeError: for unserializable values
:raises TruncatedSyntaxError: when failing to encode the file
:raises ValueError: for invalid values
Example:
>>> import jsonyx as json
>>> from os.path import join
>>> from tempfile import TemporaryDirectory
>>> with TemporaryDirectory() as tmpdir:
... filename = join(tmpdir, "file.json")
... json.write(["filesystem API"], filename)
... with open(filename, "r", encoding="utf-8") as fp:
... fp.read()
...
'["filesystem API"]\n'
.. tip:: Use :class:`jsonyx.Encoder` directly for better performance.
.. note::
- The item separator is automatically stripped when indented.
- The output is not written incrementally, but in one-shot.
.. warning:: Avoid specifying ABCs for ``types``, that is very slow.
.. seealso::
- :func:`jsonyx.dump` for serializing to an open file.
- :func:`jsonyx.dumps` for serializing to a string.
- :func:`jsonyx.read` for deserializing from a file.
"""
Encoder(
allow=allow,
check_circular=check_circular,
commas=commas,
end=end,
ensure_ascii=ensure_ascii,
formatters=formatters,
hook=hook,
indent=indent,
indent_leaves=indent_leaves,
max_indent_level=max_indent_level,
quoted_keys=quoted_keys,
separators=separators,
skipkeys=skipkeys,
sort_keys=sort_keys,
trailing_comma=trailing_comma,
types=types,
).write(obj, filename, encoding)
[docs]
def dump(
obj: object,
fp: Writer[str] | None = None,
*,
allow: Container[str] = NOTHING,
check_circular: bool = True,
commas: bool = True,
end: str = "\n",
ensure_ascii: bool = False,
formatters: dict[str, _Formatter] | None = None,
hook: _Hook | None = None,
indent: int | str | None = None,
indent_leaves: bool = True,
max_indent_level: int | None = None,
quoted_keys: bool = True,
separators: tuple[str, str] = (", ", ": "),
skipkeys: bool = False,
sort_keys: bool = False,
trailing_comma: bool = False,
types: dict[str, _ClassInfo] | None = None,
) -> None:
r"""Serialize a Python object to an open JSON file.
.. versionchanged:: 2.0
- Added ``commas``, ``indent_leaves``, ``max_indent_level``,
``quoted_keys`` and ``types``.
- Allowed overriding serialization in subclasses of :class:`float` and
:class:`int` (e.g. :class:`enum.Enum`).
- Made :class:`tuple` serializable by default instead of
:class:`decimal.Decimal`.
- Replaced ``item_separator`` and ``key_separator`` with
``separators``.
.. versionchanged:: 2.1 Added ``check_circular``, ``hook`` and
``skipkeys``.
.. versionchanged:: 2.4
- Added ``formatters``.
- Allowed overriding serialization in subclasses of :class:`str` (e.g.
:class:`enum.Enum`).
- Made :class:`frozendict` serializable by default.
:param obj: a Python object
:param fp: an open JSON file
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param check_circular: check for circular references
:param commas: separate items by commas when indented
:param end: the string to append at the end
:param ensure_ascii: escape non-ASCII characters
:param formatters: a dictionary of :ref:`formatters <formatters>`
:param hook: the :ref:`hook <encoding_hook>` used for transforming data
:param indent: the number of spaces or string to indent with
:param indent_leaves: indent leaf objects and arrays
:param max_indent_level: the level up to which to indent
:param quoted_keys: quote keys which are :ref:`identifiers <identifiers>`
:param separators: the item and key separator
:param skipkeys: skip non-string keys
:param sort_keys: sort the keys of objects
:param trailing_comma: add a trailing comma when indented
:param types: a dictionary of :ref:`additional types <protocol_types>`
:raises RecursionError: if the object is too deeply nested
:raises TypeError: for unserializable values
:raises TruncatedSyntaxError: when failing to write to the file
:raises ValueError: for invalid values
Example:
Writing to standard output:
>>> import jsonyx as json
>>> json.dump('"foo\bar')
"\"foo\bar"
>>> json.dump("\\")
"\\"
>>> json.dump("\u20AC")
"€"
Writing to an open file:
>>> import jsonyx as json
>>> from io import StringIO
>>> io = StringIO()
>>> json.dump(["writer protocol"], io)
>>> io.getvalue()
'["writer protocol"]\n'
.. tip:: Use :class:`jsonyx.Encoder` directly for better performance.
.. note::
- The item separator is automatically stripped when indented.
- The output is not written incrementally, but in one-shot.
.. warning:: Avoid specifying ABCs for ``types``, that is very slow.
.. seealso::
- :func:`jsonyx.write` for serializing to a file.
- :func:`jsonyx.dumps` for serializing to a string.
- :func:`jsonyx.load` for deserializing from an open file.
"""
Encoder(
allow=allow,
check_circular=check_circular,
commas=commas,
end=end,
ensure_ascii=ensure_ascii,
formatters=formatters,
hook=hook,
indent=indent,
indent_leaves=indent_leaves,
max_indent_level=max_indent_level,
quoted_keys=quoted_keys,
separators=separators,
skipkeys=skipkeys,
sort_keys=sort_keys,
trailing_comma=trailing_comma,
types=types,
).dump(obj, fp)
[docs]
def dumps(
obj: object,
*,
allow: Container[str] = NOTHING,
check_circular: bool = True,
commas: bool = True,
end: str = "\n",
ensure_ascii: bool = False,
formatters: dict[str, _Formatter] | None = None,
hook: _Hook | None = None,
indent: int | str | None = None,
indent_leaves: bool = True,
max_indent_level: int | None = None,
quoted_keys: bool = True,
separators: tuple[str, str] = (", ", ": "),
skipkeys: bool = False,
sort_keys: bool = False,
trailing_comma: bool = False,
types: dict[str, _ClassInfo] | None = None,
) -> str:
r"""Serialize a Python object to a JSON string.
.. versionchanged:: 2.0
- Added ``commas``, ``indent_leaves``, ``max_indent_level``,
``quoted_keys`` and ``types``.
- Allowed overriding serialization in subclasses of :class:`float` and
:class:`int` (e.g. :class:`enum.Enum`).
- Made :class:`tuple` serializable by default instead of
:class:`decimal.Decimal`.
- Replaced ``item_separator`` and ``key_separator`` with
``separators``.
.. versionchanged:: 2.1 Added ``check_circular``, ``hook`` and
``skipkeys``.
.. versionchanged:: 2.4
- Added ``formatters``.
- Allowed overriding serialization in subclasses of :class:`str` (e.g.
:class:`enum.Enum`).
- Made :class:`frozendict` serializable by default.
:param obj: a Python object
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param check_circular: check for circular references
:param commas: separate items by commas when indented
:param end: the string to append at the end
:param ensure_ascii: escape non-ASCII characters
:param formatters: a dictionary of :ref:`formatters <formatters>`
:param hook: the :ref:`hook <encoding_hook>` used for transforming data
:param indent: the number of spaces or string to indent with
:param indent_leaves: indent leaf objects and arrays
:param max_indent_level: the level up to which to indent
:param quoted_keys: quote keys which are :ref:`identifiers <identifiers>`
:param separators: the item and key separator
:param skipkeys: skip non-string keys
:param sort_keys: sort the keys of objects
:param trailing_comma: add a trailing comma when indented
:param types: a dictionary of :ref:`additional types <protocol_types>`
:raises RecursionError: if the object is too deeply nested
:raises TypeError: for unserializable values
:raises ValueError: for invalid values
:return: a JSON string
Example:
>>> import jsonyx as json
>>> json.dumps(["foo", {"bar": ("baz", None, 1.0, 2)}])
'["foo", {"bar": ["baz", null, 1.0, 2]}]\n'
.. tip:: Use :class:`jsonyx.Encoder` directly for better performance.
.. note:: The item separator is automatically stripped when indented.
.. warning:: Avoid specifying ABCs for ``types``, that is very slow.
.. seealso::
- :func:`jsonyx.write` for serializing to a file.
- :func:`jsonyx.dump` for serializing to an open file.
- :func:`jsonyx.loads` for deserializing from a string.
"""
return Encoder(
allow=allow,
check_circular=check_circular,
commas=commas,
end=end,
ensure_ascii=ensure_ascii,
formatters=formatters,
hook=hook,
indent=indent,
indent_leaves=indent_leaves,
max_indent_level=max_indent_level,
quoted_keys=quoted_keys,
separators=separators,
skipkeys=skipkeys,
sort_keys=sort_keys,
trailing_comma=trailing_comma,
types=types,
).dumps(obj)
[docs]
def apply_patch(
obj: Any,
patch: _Operation | list[_Operation],
*,
allow: Container[str] = NOTHING,
) -> Any:
"""Apply a JSON patch to a Python object.
.. versionadded:: 2.0
.. versionchanged:: 2.4 Removed ``use_decimal``.
:param obj: a Python object
:param patch: a :doc:`JSON patch </json-patch-spec>`
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:raises AssertionError: if an assertion fails
:raises IndexError: if an index is out of range
:raises JSONSyntaxError: if a query is invalid
:raises KeyError: if a key is not found
:raises TypeError: if a value has the wrong type
:raises ValueError: if a value is invalid
:return: the patched Python object
Example:
>>> import jsonyx as json
>>> json.apply_patch([1, 2, 3], {"op": "del", "path": "$[1]"})
[1, 3]
.. tip::
- A :ref:`filter` is more robust than an index.
- Use :class:`jsonyx.Manipulator` directly for better performance.
.. seealso:: :func:`jsonyx.make_patch` for making a patch.
"""
return Manipulator(allow=allow).apply_patch(obj, patch)
[docs]
def paste_values(
current_nodes: _Node | list[_Node],
values: Any | list[Any],
operation: _Operation,
*,
allow: Container[str] = NOTHING,
) -> None:
"""Paste value to a node or values to a list of nodes.
.. versionadded:: 2.0
.. versionchanged:: 2.4 Removed ``use_decimal``.
:param current_nodes: a node or a list of nodes
:param values: a value or a list of values
:param operation: a JSON :ref:`copy` or :ref:`move` operation
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:raises IndexError: if an index is out of range
:raises JSONSyntaxError: if a query is invalid
:raises KeyError: if a key is not found
:raises TypeError: if a value has the wrong type
:raises ValueError: if a value is invalid
Example:
>>> import jsonyx as json
>>> obj = [1, 2, 3]
>>> root = [obj]
>>> node = root, 0 # pointer to obj
>>> json.paste_values(node, 4, {"mode": "append"})
>>> root[0]
[1, 2, 3, 4]
.. tip:: Use :class:`jsonyx.Manipulator` directly for better performance.
"""
Manipulator(allow=allow).paste_values(current_nodes, values, operation)
[docs]
def select_nodes(
nodes: _Node | list[_Node],
query: str,
*,
allow: Container[str] = NOTHING,
allow_slice: bool = False,
relative: bool = False,
) -> list[_Node]:
"""Select nodes from a node or a list of nodes.
.. versionadded:: 2.0
.. versionchanged:: 2.4 Removed ``use_decimal``.
:param nodes: a node or a list of nodes
:param query: an :ref:`JSON select query <query>`
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param allow_slice: allow slice
:param relative: query must start with ``"@"`` instead of ``"$"``
:raises IndexError: if an index is out of range
:raises JSONSyntaxError: if the select query is invalid
:raises KeyError: if a key is not found
:raises TypeError: if a value has the wrong type
:raises ValueError: if a number is too big
:return: the selected list of nodes
Example:
>>> import jsonyx as json
>>> obj = [1, 2, 3, 4, 5, 6]
>>> root = [obj]
>>> node = root, 0 # pointer to obj
>>> for target, key in json.select_nodes(node, "$[@ > 3]"):
... target[key] = None
...
>>> root[0]
[1, 2, 3, None, None, None]
.. tip:: Use :class:`jsonyx.Manipulator` directly for better performance.
"""
return Manipulator(allow=allow).select_nodes(
nodes, query, allow_slice=allow_slice, relative=relative,
)
[docs]
def apply_filter(
nodes: _Node | list[_Node], query: str, *, allow: Container[str] = NOTHING,
) -> list[_Node]:
"""Apply a JSON filter query to a node or a list of nodes.
.. versionadded:: 2.0
.. versionchanged:: 2.4 Removed ``use_decimal``.
:param nodes: a node or a list of nodes
:param query: a :ref:`JSON filter query <filter>`
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:raises IndexError: if an index is out of range
:raises JSONSyntaxError: if the filter query is invalid
:raises KeyError: if a key is not found
:raises TypeError: if a value has the wrong type
:raises ValueError: if a number is too big
:return: the filtered list of nodes
Example:
>>> import jsonyx as json
>>> obj = None
>>> root = [obj]
>>> node = root, 0 # pointer to obj
>>> assert json.apply_filter(node, "@ == null")
.. tip:: Use :class:`jsonyx.Manipulator` directly for better performance.
"""
return Manipulator(allow=allow).apply_filter(nodes, query)
[docs]
def load_query_value(s: str, *, allow: Container[str] = NOTHING) -> Any:
"""Deserialize a JSON query value to a Python object.
.. versionadded:: 2.0
.. versionchanged:: 2.4 Removed ``use_decimal``.
:param s: a :ref:`JSON query value <query_value>`
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:raises JSONSyntaxError: if the query value is invalid
:raises ValueError: if a number is too big
:return: a Python object
Example:
>>> import jsonyx as json
>>> json.load_query_value("'~'foo'")
"'foo"
.. tip:: Use :class:`jsonyx.Manipulator` directly for better performance.
"""
return Manipulator(allow=allow).load_query_value(s)