Skip to content

Commit

Permalink
Rename types to hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
nineteendo committed Feb 5, 2025
1 parent ccd6fc9 commit 60b5865
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 139 deletions.
2 changes: 1 addition & 1 deletion docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jsonyx 2.0.0 (unreleased)

- Added support for Python 3.8 and Python 3.9
- Added the ``jsonyx`` application
- Added ``types`` to :class:`jsonyx.Decoder`, :func:`jsonyx.load`,
- Added ``hooks`` to :class:`jsonyx.Decoder`, :func:`jsonyx.load`,
:func:`jsonyx.loads` and :func:`jsonyx.read`
- Added ``commas``, ``indent_leaves``, ``max_indent_level`` and ``quoted_keys``
and ``types`` to :class:`jsonyx.Encoder`, :func:`jsonyx.dump`,
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@

autodoc_preserve_defaults: bool = True
autodoc_type_aliases: dict[str, str] = {
name: name for name in ["_Node", "_Operation", "_StrPath"]
name: name for name in ["_Hook", "_Node", "_Operation", "_StrPath"]
}
autodoc_typehints: str = "none"

Expand Down
67 changes: 49 additions & 18 deletions docs/source/how-to.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ How-to Guide
Better error messages for other JSON libraries
----------------------------------------------

>>> import jsonyx
>>> from json import JSONDecodeError, loads
>>> import json, jsonyx
>>> try:
... loads("[,]")
... except JSONDecodeError as exc:
... json.loads("[,]")
... except json.JSONDecodeError as exc:
... raise jsonyx.JSONSyntaxError(exc.msg, "<string>", exc.doc, exc.pos) from None
...
Traceback (most recent call last):
Expand All @@ -20,8 +19,23 @@ jsonyx.JSONSyntaxError: Expecting value

.. seealso:: :func:`jsonyx.format_syntax_error` for formatting the exception.

Encoding :mod:`numpy` objects
-----------------------------
.. _protocol_types:

Encoding protocol-based objects
-------------------------------

.. versionadded:: 2.0

Required methods:

- ``"bool"``: :meth:`~object.__bool__` or :meth:`~object.__len__`
- ``"float"``: :meth:`~object.__float__`
- ``"int"``: :meth:`~object.__int__`
- ``"mapping"``: :meth:`~object.__len__`, :meth:`!keys`, :meth:`!values` and :meth:`!items`
- ``"sequence"``: :meth:`~object.__len__`, and :meth:`~object.__iter__`
- ``"str"``: :meth:`~object.__str__`

Example with :mod:`numpy`:

>>> import jsonyx as json
>>> import numpy as np
Expand All @@ -39,27 +53,42 @@ Encoding :mod:`numpy` objects
>>> json.dump(obj, types=types)
[false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0]

.. tip:: If needed, you can also specify ``"mapping"`` or ``"str"``.
.. note:: Custom types must be registered manually, :mod:`jsonyx` does not
infer serializability based on method presence.
.. warning:: Avoid specifying ABCs for ``types``, that is very slow.

.. _using_hooks:

Decoding :mod:`numpy` objects
-----------------------------
Decoding objects using hooks
----------------------------

.. versionadded:: 2.0

Called with:

- ``"bool"``: :class:`bool`
- ``"float"``: :class:`float`
- ``"int"``: :class:`int`
- ``"mapping"``: ``list[tuple[Any, Any]]``
- ``"sequence"``: :class:`list`
- ``"str"``: :class:`str`

Example with :mod:`numpy`:

>>> import jsonyx as json
>>> from functools import partial
>>> import numpy as np
>>> types = {
>>> hooks = {
... "bool": np.bool_,
... "float": np.float64,
... "int": np.int64,
... "sequence": partial(np.array, dtype="O")
... }
>>> json.loads("[false, 0.0, 0]", types=types)
>>> json.loads("[false, 0.0, 0]", hooks=hooks)
array([np.False_, np.float64(0.0), np.int64(0)], dtype=object)

.. tip:: If needed, you can also specify ``"mapping"`` or ``"str"``.

Specializing JSON object encoding
---------------------------------
Encoding arbitrary objects
--------------------------

>>> import jsonyx as json
>>> def to_json(obj):
Expand All @@ -74,10 +103,11 @@ Specializing JSON object encoding
>>> json.dump(to_json(1 + 2j))
{"__complex__": true, "real": 1.0, "imag": 2.0}

.. todo:: Mention alternatives.
.. tip:: You can use :func:`functools.singledispatch` to make this extensible.

Specializing JSON object decoding
---------------------------------
Decoding arbitrary objects
--------------------------

>>> import jsonyx as json
>>> def from_json(obj):
Expand All @@ -92,7 +122,8 @@ Specializing JSON object decoding
>>> from_json(json.loads('{"__complex__": true, "real": 1.0, "imag": 2.0}'))
(1+2j)

.. note:: ``mapping_type`` is not intended for this purpose.
.. todo:: Mention alternatives.
.. note:: The ``"mapping"`` hook is not intended for this purpose.

Disabling the integer string conversion length limit
----------------------------------------------------
Expand Down
32 changes: 16 additions & 16 deletions src/jsonyx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def write(self, s: _T_contra, /) -> object:
_Node = tuple[dict[Any, Any] | list[Any], int | slice | str]
_Operation = dict[str, Any]
_StrPath = PathLike[str] | str
_Type = Callable[[Any], Any]
_Hook = Callable[[Any], Any]


def format_syntax_error(exc: JSONSyntaxError) -> list[str]:
Expand Down Expand Up @@ -103,16 +103,16 @@ def read(
filename: _StrPath,
*,
allow: Container[str] = NOTHING,
types: dict[str, _Type] | None = None,
hooks: dict[str, _Hook] | None = None,
use_decimal: bool = False,
) -> Any:
"""Deserialize a JSON file to a Python object.
.. versionchanged:: 2.0 Added ``types``.
.. versionchanged:: 2.0 Added ``hooks``.
:param filename: the path to the JSON file
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param types: the types
:param hooks: the :ref:`hooks <using_hooks>` used for transforming data
:param use_decimal: use :class:`decimal.Decimal` instead of :class:`float`
:raises JSONSyntaxError: if the JSON file is invalid
:raises RecursionError: if the JSON file is too deeply nested
Expand All @@ -131,7 +131,7 @@ def read(
['filesystem API']
"""
return Decoder(allow=allow, types=types, use_decimal=use_decimal).read(
return Decoder(allow=allow, hooks=hooks, use_decimal=use_decimal).read(
filename,
)

Expand All @@ -140,17 +140,17 @@ def load(
fp: _SupportsRead[bytes | str],
*,
allow: Container[str] = NOTHING,
types: dict[str, _Type] | None = None,
hooks: dict[str, _Hook] | None = None,
root: _StrPath = ".",
use_decimal: bool = False,
) -> Any:
"""Deserialize an open JSON file to a Python object.
.. versionchanged:: 2.0 Added ``types``.
.. versionchanged:: 2.0 Added ``hooks``.
:param fp: an open JSON file
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param types: the types
:param hooks: the :ref:`hooks <using_hooks>` used for transforming data
:param root: the path to the archive containing this JSON file
:param use_decimal: use :class:`decimal.Decimal` instead of :class:`float`
:raises JSONSyntaxError: if the JSON file is invalid
Expand All @@ -166,7 +166,7 @@ def load(
['streaming API']
"""
return Decoder(allow=allow, types=types, use_decimal=use_decimal).load(
return Decoder(allow=allow, hooks=hooks, use_decimal=use_decimal).load(
fp, root=root,
)

Expand All @@ -176,17 +176,17 @@ def loads(
*,
allow: Container[str] = NOTHING,
filename: _StrPath = "<string>",
types: dict[str, _Type] | None = None,
hooks: dict[str, _Hook] | None = None,
use_decimal: bool = False,
) -> Any:
"""Deserialize a JSON string to a Python object.
.. versionchanged:: 2.0 Added ``types``.
.. versionchanged:: 2.0 Added ``hooks``.
:param s: a JSON string
:param allow: the JSON deviations from :mod:`jsonyx.allow`
:param filename: the path to the JSON file
:param types: the types
:param hooks: the :ref:`hooks <using_hooks>` used for transforming data
:param use_decimal: use :class:`decimal.Decimal` instead of :class:`float`
:raises JSONSyntaxError: if the JSON string is invalid
:raises RecursionError: if the JSON string is too deeply nested
Expand All @@ -201,7 +201,7 @@ def loads(
.. tip:: Specify ``filename`` to display the filename in error messages.
"""
return Decoder(allow=allow, types=types, use_decimal=use_decimal).loads(
return Decoder(allow=allow, hooks=hooks, use_decimal=use_decimal).loads(
s, filename=filename,
)

Expand Down Expand Up @@ -245,7 +245,7 @@ def write(
:param separators: the item and key separator
:param sort_keys: sort the keys of objects
:param trailing_comma: add a trailing comma when indented
:param types: a dictionary of additional types
:param types: a dictionary of additional :ref:`types <protocol_types>`
:raises RecursionError: if the object is too deeply nested
:raises TypeError: for unserializable values
:raises UnicodeEncodeError: when failing to encode the file
Expand Down Expand Up @@ -322,7 +322,7 @@ def dump(
:param separators: the item and key separator
:param sort_keys: sort the keys of objects
:param trailing_comma: add a trailing comma when indented
:param types: a dictionary of additional types
:param types: a dictionary of additional :ref:`types <protocol_types>`
:raises RecursionError: if the object is too deeply nested
:raises TypeError: for unserializable values
:raises ValueError: for invalid values
Expand Down Expand Up @@ -395,7 +395,7 @@ def dumps(
:param separators: the item and key separator
:param sort_keys: sort the keys of objects
:param trailing_comma: add a trailing comma when indented
:param types: a dictionary of additional types
:param types: a dictionary of additional :ref:`types <protocol_types>`
:raises RecursionError: if the object is too deeply nested
:raises TypeError: for unserializable values
:raises ValueError: for invalid values
Expand Down
Loading

0 comments on commit 60b5865

Please sign in to comment.