-
Notifications
You must be signed in to change notification settings - Fork 65
/
Copy pathmutation.py
365 lines (281 loc) · 12.3 KB
/
mutation.py
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
"""By default, glom aims to safely return a transformed copy of your
data. But sometimes you really need to transform an existing object.
When you already have a large or complex bit of nested data that you
are sure you want to modify in-place, glom has you covered, with the
:func:`~glom.assign` function, and the :func:`~glom.Assign` specifier
type.
"""
import operator
from pprint import pprint
from .core import Path, T, S, Spec, glom, UnregisteredTarget, GlomError, PathAccessError, UP
from .core import TType, register_op, TargetRegistry, bbrepr, PathAssignError, arg_val, _assign_op
try:
basestring
except NameError:
basestring = str
if getattr(__builtins__, '__dict__', None) is not None:
# pypy's __builtins__ is a module, as is CPython's REPL, but at
# normal execution time it's a dict?
__builtins__ = __builtins__.__dict__
class PathDeleteError(PathAssignError):
"""This :exc:`GlomError` subtype is raised when an assignment fails,
stemming from an :func:`~glom.delete` call or other
:class:`~glom.Delete` usage.
One example would be deleting an out-of-range position in a list::
>>> delete(["short", "list"], Path(5))
Traceback (most recent call last):
...
PathDeleteError: could not delete 5 on object at Path(), got error: IndexError(...
Other assignment failures could be due to deleting a read-only
``@property`` or exception being raised inside a ``__delattr__()``.
"""
def get_message(self):
return ('could not delete %r on object at %r, got error: %r'
% (self.dest_name, self.path, self.exc))
def _apply_for_each(func, path, val):
layers = path.path_t.__stars__()
if layers:
for i in range(layers - 1):
val = sum(val, []) # flatten out the extra layers
for inner in val:
func(inner)
else:
func(val)
class Assign(object):
"""*New in glom 18.3.0*
The ``Assign`` specifier type enables glom to modify the target,
performing a "deep-set" to mirror glom's original deep-get use
case.
``Assign`` can be used to perform spot modifications of large data
structures when making a copy is not desired::
# deep assignment into a nested dictionary
>>> target = {'a': {}}
>>> spec = Assign('a.b', 'value')
>>> _ = glom(target, spec)
>>> pprint(target)
{'a': {'b': 'value'}}
The value to be assigned can also be a :class:`~glom.Spec`, which
is useful for copying values around within the data structure::
# copying one nested value to another
>>> _ = glom(target, Assign('a.c', Spec('a.b')))
>>> pprint(target)
{'a': {'b': 'value', 'c': 'value'}}
Another handy use of Assign is to deep-apply a function::
# sort a deep nested list
>>> target={'a':{'b':[3,1,2]}}
>>> _ = glom(target, Assign('a.b', Spec(('a.b',sorted))))
>>> pprint(target)
{'a': {'b': [1, 2, 3]}}
Like many other specifier types, ``Assign``'s destination path can be
a :data:`~glom.T` expression, for maximum control::
# changing the error message of an exception in an error list
>>> err = ValueError('initial message')
>>> target = {'errors': [err]}
>>> _ = glom(target, Assign(T['errors'][0].args, ('new message',)))
>>> str(err)
'new message'
``Assign`` has built-in support for assigning to attributes of
objects, keys of mappings (like dicts), and indexes of sequences
(like lists). Additional types can be registered through
:func:`~glom.register()` using the ``"assign"`` operation name.
Attempting to assign to an immutable structure, like a
:class:`tuple`, will result in a
:class:`~glom.PathAssignError`. Attempting to assign to a path
that doesn't exist will raise a :class:`~PathAccessError`.
To automatically backfill missing structures, you can pass a
callable to the *missing* argument. This callable will be called
for each path segment along the assignment which is not
present.
>>> target = {}
>>> assign(target, 'a.b.c', 'hi', missing=dict)
{'a': {'b': {'c': 'hi'}}}
"""
def __init__(self, path, val, missing=None):
# TODO: an option like require_preexisting or something to
# ensure that a value is mutated, not just added. Current
# workaround is to do a Check().
if isinstance(path, basestring):
path = Path.from_text(path)
elif type(path) is TType:
path = Path(path)
elif not isinstance(path, Path):
raise TypeError('path argument must be a .-delimited string, Path, T, or S')
try:
self.op, self.arg = path.items()[-1]
except IndexError:
raise ValueError('path must have at least one element')
self._orig_path = path
self.path = path[:-1]
if self.op not in '[.P':
# maybe if we add null-coalescing this should do something?
raise ValueError('last part of path must be setattr or setitem')
self.val = val
if missing is not None:
if not callable(missing):
raise TypeError('expected missing to be callable, not %r' % (missing,))
self.missing = missing
def glomit(self, target, scope):
val = arg_val(target, self.val, scope)
op, arg, path = self.op, self.arg, self.path
if self.path.startswith(S):
dest_target = scope[UP]
dest_path = self.path.from_t()
else:
dest_target = target
dest_path = self.path
try:
dest = scope[glom](dest_target, dest_path, scope)
except PathAccessError as pae:
if not self.missing:
raise
remaining_path = self._orig_path[pae.part_idx + 1:]
val = scope[glom](self.missing(), Assign(remaining_path, val, missing=self.missing), scope)
op, arg = self._orig_path.items()[pae.part_idx]
path = self._orig_path[:pae.part_idx]
dest = scope[glom](dest_target, path, scope)
# TODO: forward-detect immutable dest?
_apply = lambda dest: _assign_op(
dest=dest, op=op, arg=arg, val=val, path=path, scope=scope)
_apply_for_each(_apply, path, dest)
return target
def __repr__(self):
cn = self.__class__.__name__
if self.missing is None:
return '%s(%r, %r)' % (cn, self._orig_path, self.val)
return '%s(%r, %r, missing=%s)' % (cn, self._orig_path, self.val, bbrepr(self.missing))
def assign(obj, path, val, missing=None):
"""*New in glom 18.3.0*
The ``assign()`` function provides convenient "deep set"
functionality, modifying nested data structures in-place::
>>> target = {'a': [{'b': 'c'}, {'d': None}]}
>>> _ = assign(target, 'a.1.d', 'e') # let's give 'd' a value of 'e'
>>> pprint(target)
{'a': [{'b': 'c'}, {'d': 'e'}]}
Missing structures can also be automatically created with the
*missing* parameter. For more information and examples, see the
:class:`~glom.Assign` specifier type, which this function wraps.
"""
return glom(obj, Assign(path, val, missing=missing))
_ALL_BUILTIN_TYPES = [v for v in __builtins__.values() if isinstance(v, type)]
_BUILTIN_BASE_TYPES = [v for v in _ALL_BUILTIN_TYPES
if not issubclass(v, tuple([t for t in _ALL_BUILTIN_TYPES
if t not in (v, type, object)]))]
_UNASSIGNABLE_BASE_TYPES = tuple(set(_BUILTIN_BASE_TYPES)
- set([dict, list, BaseException, object, type]))
def _set_sequence_item(target, idx, val):
target[int(idx)] = val
def _assign_autodiscover(type_obj):
# TODO: issubclass or "in"?
if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES):
return False
if callable(getattr(type_obj, '__setitem__', None)):
if callable(getattr(type_obj, 'index', None)):
return _set_sequence_item
return operator.setitem
return setattr
register_op('assign', auto_func=_assign_autodiscover, exact=False)
class Delete(object):
"""
In addition to glom's core "deep-get" and ``Assign``'s "deep-set",
the ``Delete`` specifier type performs a "deep-del", which can
remove items from larger data structures by key, attribute, and
index.
>>> target = {'dict': {'x': [5, 6, 7]}}
>>> glom(target, Delete('dict.x.1'))
{'dict': {'x': [5, 7]}}
>>> glom(target, Delete('dict.x'))
{'dict': {}}
If a target path is missing, a :exc:`PathDeleteError` will be
raised. To ignore missing targets, use the ``ignore_missing``
flag:
>>> glom(target, Delete('does_not_exist', ignore_missing=True))
{'dict': {}}
``Delete`` has built-in support for deleting attributes of
objects, keys of dicts, and indexes of sequences
(like lists). Additional types can be registered through
:func:`~glom.register()` using the ``"delete"`` operation name.
.. versionadded:: 20.5.0
"""
def __init__(self, path, ignore_missing=False):
if isinstance(path, basestring):
path = Path.from_text(path)
elif type(path) is TType:
path = Path(path)
elif not isinstance(path, Path):
raise TypeError('path argument must be a .-delimited string, Path, T, or S')
try:
self.op, self.arg = path.items()[-1]
except IndexError:
raise ValueError('path must have at least one element')
self._orig_path = path
self.path = path[:-1]
if self.op not in '[.P':
raise ValueError('last part of path must be an attribute or index')
self.ignore_missing = ignore_missing
def _del_one(self, dest, op, arg, scope):
if op == '[':
try:
del dest[arg]
except IndexError as e:
if not self.ignore_missing:
raise PathDeleteError(e, self.path, arg)
elif op == '.':
try:
delattr(dest, arg)
except AttributeError as e:
if not self.ignore_missing:
raise PathDeleteError(e, self.path, arg)
elif op == 'P':
_delete = scope[TargetRegistry].get_handler('delete', dest)
try:
_delete(dest, arg)
except Exception as e:
if not self.ignore_missing:
raise PathDeleteError(e, self.path, arg)
def glomit(self, target, scope):
op, arg, path = self.op, self.arg, self.path
if self.path.startswith(S):
dest_target = scope[UP]
dest_path = self.path.from_t()
else:
dest_target = target
dest_path = self.path
try:
dest = scope[glom](dest_target, dest_path, scope)
except PathAccessError as pae:
if not self.ignore_missing:
raise
else:
_apply_for_each(lambda dest: self._del_one(dest, op, arg, scope), path, dest)
return target
def __repr__(self):
cn = self.__class__.__name__
return '%s(%r)' % (cn, self._orig_path)
def delete(obj, path, ignore_missing=False):
"""
The ``delete()`` function provides "deep del" functionality,
modifying nested data structures in-place::
>>> target = {'a': [{'b': 'c'}, {'d': None}]}
>>> delete(target, 'a.0.b')
{'a': [{}, {'d': None}]}
Attempting to delete missing keys, attributes, and indexes will
raise a :exc:`PathDeleteError`. To ignore these errors, use the
*ignore_missing* argument::
>>> delete(target, 'does_not_exist', ignore_missing=True)
{'a': [{}, {'d': None}]}
For more information and examples, see the :class:`~glom.Delete`
specifier type, which this convenience function wraps.
.. versionadded:: 20.5.0
"""
return glom(obj, Delete(path, ignore_missing=ignore_missing))
def _del_sequence_item(target, idx):
del target[int(idx)]
def _delete_autodiscover(type_obj):
if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES):
return False
if callable(getattr(type_obj, '__delitem__', None)):
if callable(getattr(type_obj, 'index', None)):
return _del_sequence_item
return operator.delitem
return delattr
register_op('delete', auto_func=_delete_autodiscover, exact=False)