"""GJSON module."""
import json
import os
import re
from importlib.metadata import PackageNotFoundError, version
from typing import Any
from gjson._gjson import MODIFIER_NAME_RESERVED_CHARS, GJSONObj
from gjson._protocols import ModifierProtocol
from gjson.exceptions import GJSONError, GJSONParseError
# Explicit export of modules for the import * syntax, custom order to force the documentation order
__all__ = ['GJSON', 'GJSONError', 'GJSONObj', 'GJSONParseError', 'ModifierProtocol', '__version__', 'get']
# TODO: use a proper type hint for obj once https://github.com/python/typing/issues/182 will be fixed
[docs]
def get(obj: Any, query: str, *, as_str: bool = False, quiet: bool = False) -> Any:
"""Quick accessor to GJSON functionalities exposed for simplicity of use.
Examples:
Import and directly use this quick helper for the simpler usage::
>>> import gjson
>>> data = {'items': [{'name': 'a', 'size': 1}, {'name': 'b', 'size': 2}]}
>>> gjson.get(data, 'items.#.size')
[1, 2]
Arguments:
obj: the object to query. It must be accessible in JSON-like fashion so it must be an object that can be
converted to JSON.
query: the query string to evaluate to extract the data from the object.
as_str: if set to :py:data:`True` returns a JSON-encoded string, a Python object otherwise.
quiet: on error, if set to :py:data:`True`, will raises an GJSONError exception. Otherwise returns
:py:data:`None` on error.
Return:
the resulting object.
"""
gjson_obj = GJSON(obj)
if as_str:
return gjson_obj.getj(query, quiet=quiet)
return gjson_obj.get(query, quiet=quiet)
[docs]
class GJSON:
"""The GJSON class to operate on JSON-like objects."""
def __init__(self, obj: Any):
"""Initialize the instance with the given object.
Examples:
Use the :py:class:`gjson.GJSON` class for more complex usage or to perform multiple queries on the same
object::
>>> import gjson
>>> data = {'items': [{'name': 'a', 'size': 1}, {'name': 'b', 'size': 2}]}
>>> gjson_obj = gjson.GJSON(data)
Arguments:
obj: the object to query.
"""
self._obj = obj
self._custom_modifiers: dict[str, ModifierProtocol] = {}
[docs]
def __str__(self) -> str:
"""Return the current object as a JSON-encoded string.
Examples:
Converting to string a :py:class:`gjson.GJSON` object returns it as a JSON-encoded string::
>>> str(gjson_obj)
'{"items": [{"name": "a", "size": 1}, {"name": "b", "size": 2}]}'
Returns:
the JSON-encoded string representing the instantiated object.
"""
return json.dumps(self._obj, ensure_ascii=False)
[docs]
def get(self, query: str, *, quiet: bool = False) -> Any:
"""Perform a query on the instantiated object and return the resulting object.
Examples:
Perform a query and get the resulting object::
>>> gjson_obj.get('items.#.size')
[1, 2]
Arguments:
query: the GJSON query to apply to the object.
quiet: wheter to raise a :py:class:`gjson.GJSONError` exception on error or just return :py:data:`None` in
case of error.
Raises:
gjson.GJSONError: on error if the quiet parameter is not :py:data:`True`.
Returns:
the resulting object or :py:data:`None` if the ``quiet`` parameter is :py:data:`True` and there was an
error.
"""
try:
return GJSONObj(self._obj, query, custom_modifiers=self._custom_modifiers).get()
except GJSONError:
if quiet:
return None
raise
[docs]
def getj(self, query: str, *, quiet: bool = False) -> str:
"""Perform a query on the instantiated object and return the resulting object as JSON-encoded string.
Examples:
Perform a query and get the resulting object as a JSON-encoded string::
>>> gjson_obj.getj('items.#.size')
'[1, 2]'
Arguments:
query: the GJSON query to apply to the object.
quiet: wheter to raise a :py:class:`gjson.GJSONError` exception on error or just return :py:data:`None` in
case of error.
Raises:
gjson.GJSONError: on error if the quiet parameter is not :py:data:`True`.
Returns:
the JSON-encoded string representing the resulting object or :py:data:`None` if the ``quiet`` parameter is
:py:data:`True` and there was an error.
"""
try:
return str(GJSONObj(self._obj, query, custom_modifiers=self._custom_modifiers))
except GJSONError:
if quiet:
return ''
raise
[docs]
def get_gjson(self, query: str, *, quiet: bool = False) -> 'GJSON':
"""Perform a query on the instantiated object and return the resulting object as a GJSON instance.
Examples:
Perform a query and get the resulting object already encapsulated into a :py:class:`gjson.GJSON` object::
>>> sizes = gjson_obj.get_gjson('items.#.size')
>>> str(sizes)
'[1, 2]'
>>> sizes.get('0')
1
Arguments:
query: the GJSON query to apply to the object.
quiet: wheter to raise a :py:class:`gjson.GJSONError` exception on error or just return :py:data:`None` in
case of error.
Raises:
gjson.GJSONError: on error if the quiet parameter is not :py:data:`True`.
Returns:
the resulting object encapsulated as a :py:class:`gjson.GJSON` object or :py:data:`None` if the ``quiet``
parameter is :py:data:`True` and there was an error.
"""
return GJSON(self.get(query, quiet=quiet))
[docs]
def register_modifier(self, name: str, func: ModifierProtocol) -> None:
"""Register a custom modifier.
Examples:
Register a custom modifier that sums all the numbers in a list:
>>> def custom_sum(options, obj, *, last):
... # insert sanity checks code here
... return sum(obj)
...
>>> gjson_obj.register_modifier('sum', custom_sum)
>>> gjson_obj.get('items.#.size.@sum')
3
Arguments:
name: the modifier name. It will be called where ``@name`` is used in the query. If two custom modifiers
are registered with the same name the last one will be used.
func: the modifier code in the form of a callable object that adhere to the
:py:class:`gjson.ModifierProtocol`.
Raises:
gjson.GJSONError: if the provided callable doesn't adhere to the :py:class:`gjson.ModifierProtocol`.
"""
# Escape the ] as they are inside a [...] block
not_allowed_regex = ''.join(MODIFIER_NAME_RESERVED_CHARS).replace(']', r'\]')
if re.search(fr'[{not_allowed_regex}]', name):
not_allowed_string = ', '.join(f'`{i}`' for i in MODIFIER_NAME_RESERVED_CHARS)
raise GJSONError(f'Unable to register modifier `{name}`, contains at least one not allowed character: '
f'{not_allowed_string}')
if name in GJSONObj.builtin_modifiers():
raise GJSONError(f'Unable to register a modifier with the same name of the built-in modifier: @{name}.')
if not isinstance(func, ModifierProtocol):
raise GJSONError(f'The given func "{func}" for the custom modifier @{name} does not adhere '
'to the gjson.ModifierProtocol.')
self._custom_modifiers[name] = func
try:
__version__: str = version('gjson')
"""str: the version of the current gjson module."""
except PackageNotFoundError: # pragma: no cover - this should never happen during tests
# Read the override from the environment, if present (like inside Debian build system)
if 'SETUPTOOLS_SCM_PRETEND_VERSION' in os.environ:
__version__ = os.environ['SETUPTOOLS_SCM_PRETEND_VERSION']