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
|
"""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])
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
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)}!")
|