"""This module contains code for accessing values in nested data structures."""
__author__ = "Jens Finkhaeuser"
__copyright__ = "Copyright (c) 2018 Jens Finkhaeuser"
__license__ = "MIT"
__all__ = ()
def _json_ref_escape(path):
"""JSON-reference escape object path."""
path = str(path) # Could be an int, etc.
path = path.replace("~", "~0")
path = path.replace("/", "~1")
return path
def _str_path(path):
"""Stringify object path."""
return "/" + "/".join([_json_ref_escape(p) for p in path])
[docs]def path_get(obj, path, defaultvalue=None, path_of_obj=()):
"""
Retrieve the value from obj indicated by path.
Like dict.get(), except:
- Any Mapping or Sequence is supported.
- Path is itself a Sequence; the first part is applied to the passed
object, the second part to the value returned from this operation, and
so forth recursively.
:param mixed obj: The Sequence or Mapping from which to retrieve values.
:param Sequence path: A Sequence of zero or more key/index elements.
:param mixed defaultvalue: If the value at the path does not exist and this
parameter is not None, it is returned. Otherwise an error is raised.
"""
from collections.abc import Mapping, Sequence
# For error reporting.
path_of_obj_str = _str_path(path_of_obj)
if path is not None and not isinstance(path, Sequence):
raise TypeError(f"Path is a {type(path)}, but must be None or a Collection!")
if isinstance(obj, Mapping):
if path is None or len(path) < 1:
return obj or defaultvalue
if path[0] not in obj:
raise KeyError(
'Object at "{}" does not contain key: {}'.format(
path_of_obj_str, path[0]
)
)
return path_get(
obj[path[0]], path[1:], defaultvalue, path_of_obj=path_of_obj + (path[0],)
)
elif isinstance(obj, Sequence):
if path is None or len(path) < 1:
return obj or defaultvalue
try:
idx = int(path[0])
except ValueError:
raise KeyError(
'Sequence at "%s" needs integer indices only, but got: '
"%s"
% (
path_of_obj_str,
path[0],
)
)
if idx < 0 or idx >= len(obj):
raise IndexError(
'Index out of bounds for sequence at "%s": %d' % (path_of_obj_str, idx)
)
return path_get(
obj[idx], path[1:], defaultvalue, path_of_obj=path_of_obj + (path[0],)
)
else:
# Path must be empty.
if path is not None and len(path) > 0:
raise TypeError(f"Cannot get anything from type {type(obj)}!")
return obj or defaultvalue
[docs]def path_set(obj, path, value, **options):
"""
Set the value in obj indicated by path.
Setter anologous to path_get() above.
As setting values is a write operation, this function optionally creates
intermediate objects to ensure all elements of path can be dereferenced.
:param mixed obj: The Sequence or Mapping from which to retrieve values.
:param Sequence path: A Sequence of zero or more key/index elements.
:param mixed value: The value to set.
:param bool create: [optional] Flag indicating whether to create
intermediate values or not. Defaults to False.
"""
# Retrieve options
create = options.get("create", False)
def fill_sequence(seq, index, value_index_type):
"""
Fill the sequence seq with elements until index can be accessed.
Fills with None except for the indexed element. That is either a dict or
a list, depending on the value_index_type. If the latter is an int, a
list is added. If the latter is None (unknown), None is added. Otherwise
a dict is added.
"""
if len(seq) > index:
return
while len(seq) < index:
seq.append(None)
if value_index_type == int:
seq.append([])
elif value_index_type is None:
seq.append(None)
else:
seq.append({})
def safe_idx(seq, index):
"""
Safely index a sequence.
Much like dict.get with default value, except returns None instead of
raising IndexError.
"""
try:
return type(seq[index])
except IndexError:
return None
# print('obj', obj, type(obj))
# print('path', path)
# print('value', value)
from collections.abc import Sequence, MutableSequence, Mapping, MutableMapping
if path is not None and not isinstance(path, Sequence):
raise TypeError(f"Path is a {type(path)}, but must be None or a Collection!")
if len(path) < 1:
raise KeyError("Cannot set with an empty path!")
if isinstance(obj, Mapping):
# If we don't have a mutable mapping, we should raise a TypeError
if not isinstance(obj, MutableMapping): # pragma: nocover
raise TypeError(f"Mapping is not mutable: {type(obj)}")
# If the path has only one element, we just overwrite the element at the
# given key. Otherwise we recurse.
if len(path) == 1:
if not create and path[0] not in obj:
# dicts would normally silently create, but we have to make it
# explicit to fulfil our contract.
raise KeyError(f'Key "{path[0]}" not in Mapping!')
obj[path[0]] = value
else:
if create and path[0] not in obj:
if type(path[1]) == int:
obj[path[0]] = []
else:
obj[path[0]] = {}
path_set(obj[path[0]], path[1:], value, create=create)
return obj
elif isinstance(obj, Sequence):
idx = path[0]
# If we don't have a mutable sequence, we should raise a TypeError
if not isinstance(obj, MutableSequence):
raise TypeError(f"Sequence is not mutable: {type(obj)}")
# Ensure integer indices
try:
idx = int(idx)
except ValueError:
raise KeyError("Sequences need integer indices only.")
# If we're supposed to create and the index at path[0] doesn't exist,
# then we need to push some dummy objects.
if create:
fill_sequence(obj, idx, safe_idx(path, 1))
# If the path has only one element, we just overwrite the element at the
# given index. Otherwise we recurse.
# print('pl', len(path))
if len(path) == 1:
obj[idx] = value
else:
path_set(obj[idx], path[1:], value, create=create)
return obj
else:
raise TypeError(f"Cannot set anything on type {type(obj)}!")