1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
|
"""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
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)
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] = {}
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)
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
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
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))
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']
|