Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/glom/mutation.py: 56%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""By default, glom aims to safely return a transformed copy of your
2data. But sometimes you really need to transform an existing object.
4When you already have a large or complex bit of nested data that you
5are sure you want to modify in-place, glom has you covered, with the
6:func:`~glom.assign` function, and the :func:`~glom.Assign` specifier
7type.
9"""
10import operator
11from pprint import pprint
13from .core import Path, T, S, Spec, glom, UnregisteredTarget, GlomError, PathAccessError, UP
14from .core import TType, register_op, TargetRegistry, bbrepr, PathAssignError, arg_val, _assign_op
17try:
18 basestring
19except NameError:
20 basestring = str
23if getattr(__builtins__, '__dict__', None) is not None:
24 # pypy's __builtins__ is a module, as is CPython's REPL, but at
25 # normal execution time it's a dict?
26 __builtins__ = __builtins__.__dict__
29class PathDeleteError(PathAssignError):
30 """This :exc:`GlomError` subtype is raised when an assignment fails,
31 stemming from an :func:`~glom.delete` call or other
32 :class:`~glom.Delete` usage.
34 One example would be deleting an out-of-range position in a list::
36 >>> delete(["short", "list"], Path(5))
37 Traceback (most recent call last):
38 ...
39 PathDeleteError: could not delete 5 on object at Path(), got error: IndexError(...
41 Other assignment failures could be due to deleting a read-only
42 ``@property`` or exception being raised inside a ``__delattr__()``.
44 """
45 def get_message(self):
46 return ('could not delete %r on object at %r, got error: %r'
47 % (self.dest_name, self.path, self.exc))
50def _apply_for_each(func, path, val):
51 layers = path.path_t.__stars__()
52 if layers:
53 for i in range(layers - 1):
54 val = sum(val, []) # flatten out the extra layers
55 for inner in val:
56 func(inner)
57 else:
58 func(val)
61class Assign(object):
62 """*New in glom 18.3.0*
64 The ``Assign`` specifier type enables glom to modify the target,
65 performing a "deep-set" to mirror glom's original deep-get use
66 case.
68 ``Assign`` can be used to perform spot modifications of large data
69 structures when making a copy is not desired::
71 # deep assignment into a nested dictionary
72 >>> target = {'a': {}}
73 >>> spec = Assign('a.b', 'value')
74 >>> _ = glom(target, spec)
75 >>> pprint(target)
76 {'a': {'b': 'value'}}
78 The value to be assigned can also be a :class:`~glom.Spec`, which
79 is useful for copying values around within the data structure::
81 # copying one nested value to another
82 >>> _ = glom(target, Assign('a.c', Spec('a.b')))
83 >>> pprint(target)
84 {'a': {'b': 'value', 'c': 'value'}}
86 Another handy use of Assign is to deep-apply a function::
88 # sort a deep nested list
89 >>> target={'a':{'b':[3,1,2]}}
90 >>> _ = glom(target, Assign('a.b', Spec(('a.b',sorted))))
91 >>> pprint(target)
92 {'a': {'b': [1, 2, 3]}}
94 Like many other specifier types, ``Assign``'s destination path can be
95 a :data:`~glom.T` expression, for maximum control::
97 # changing the error message of an exception in an error list
98 >>> err = ValueError('initial message')
99 >>> target = {'errors': [err]}
100 >>> _ = glom(target, Assign(T['errors'][0].args, ('new message',)))
101 >>> str(err)
102 'new message'
104 ``Assign`` has built-in support for assigning to attributes of
105 objects, keys of mappings (like dicts), and indexes of sequences
106 (like lists). Additional types can be registered through
107 :func:`~glom.register()` using the ``"assign"`` operation name.
109 Attempting to assign to an immutable structure, like a
110 :class:`tuple`, will result in a
111 :class:`~glom.PathAssignError`. Attempting to assign to a path
112 that doesn't exist will raise a :class:`~PathAccessError`.
114 To automatically backfill missing structures, you can pass a
115 callable to the *missing* argument. This callable will be called
116 for each path segment along the assignment which is not
117 present.
119 >>> target = {}
120 >>> assign(target, 'a.b.c', 'hi', missing=dict)
121 {'a': {'b': {'c': 'hi'}}}
123 """
124 def __init__(self, path, val, missing=None):
125 # TODO: an option like require_preexisting or something to
126 # ensure that a value is mutated, not just added. Current
127 # workaround is to do a Check().
128 if isinstance(path, basestring):
129 path = Path.from_text(path)
130 elif type(path) is TType:
131 path = Path(path)
132 elif not isinstance(path, Path):
133 raise TypeError('path argument must be a .-delimited string, Path, T, or S')
135 try:
136 self.op, self.arg = path.items()[-1]
137 except IndexError:
138 raise ValueError('path must have at least one element')
139 self._orig_path = path
140 self.path = path[:-1]
142 if self.op not in '[.P':
143 # maybe if we add null-coalescing this should do something?
144 raise ValueError('last part of path must be setattr or setitem')
145 self.val = val
147 if missing is not None:
148 if not callable(missing):
149 raise TypeError('expected missing to be callable, not %r' % (missing,))
150 self.missing = missing
152 def glomit(self, target, scope):
153 val = arg_val(target, self.val, scope)
155 op, arg, path = self.op, self.arg, self.path
156 if self.path.startswith(S):
157 dest_target = scope[UP]
158 dest_path = self.path.from_t()
159 else:
160 dest_target = target
161 dest_path = self.path
162 try:
163 dest = scope[glom](dest_target, dest_path, scope)
164 except PathAccessError as pae:
165 if not self.missing:
166 raise
168 remaining_path = self._orig_path[pae.part_idx + 1:]
169 val = scope[glom](self.missing(), Assign(remaining_path, val, missing=self.missing), scope)
171 op, arg = self._orig_path.items()[pae.part_idx]
172 path = self._orig_path[:pae.part_idx]
173 dest = scope[glom](dest_target, path, scope)
175 # TODO: forward-detect immutable dest?
176 _apply = lambda dest: _assign_op(
177 dest=dest, op=op, arg=arg, val=val, path=path, scope=scope)
178 _apply_for_each(_apply, path, dest)
180 return target
182 def __repr__(self):
183 cn = self.__class__.__name__
184 if self.missing is None:
185 return '%s(%r, %r)' % (cn, self._orig_path, self.val)
186 return '%s(%r, %r, missing=%s)' % (cn, self._orig_path, self.val, bbrepr(self.missing))
189def assign(obj, path, val, missing=None):
190 """*New in glom 18.3.0*
192 The ``assign()`` function provides convenient "deep set"
193 functionality, modifying nested data structures in-place::
195 >>> target = {'a': [{'b': 'c'}, {'d': None}]}
196 >>> _ = assign(target, 'a.1.d', 'e') # let's give 'd' a value of 'e'
197 >>> pprint(target)
198 {'a': [{'b': 'c'}, {'d': 'e'}]}
200 Missing structures can also be automatically created with the
201 *missing* parameter. For more information and examples, see the
202 :class:`~glom.Assign` specifier type, which this function wraps.
203 """
204 return glom(obj, Assign(path, val, missing=missing))
207_ALL_BUILTIN_TYPES = [v for v in __builtins__.values() if isinstance(v, type)]
208_BUILTIN_BASE_TYPES = [v for v in _ALL_BUILTIN_TYPES
209 if not issubclass(v, tuple([t for t in _ALL_BUILTIN_TYPES
210 if t not in (v, type, object)]))]
211_UNASSIGNABLE_BASE_TYPES = tuple(set(_BUILTIN_BASE_TYPES)
212 - set([dict, list, BaseException, object, type]))
215def _set_sequence_item(target, idx, val):
216 target[int(idx)] = val
219def _assign_autodiscover(type_obj):
220 # TODO: issubclass or "in"?
221 if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES):
222 return False
224 if callable(getattr(type_obj, '__setitem__', None)):
225 if callable(getattr(type_obj, 'index', None)):
226 return _set_sequence_item
227 return operator.setitem
229 return setattr
232register_op('assign', auto_func=_assign_autodiscover, exact=False)
235class Delete(object):
236 """
237 In addition to glom's core "deep-get" and ``Assign``'s "deep-set",
238 the ``Delete`` specifier type performs a "deep-del", which can
239 remove items from larger data structures by key, attribute, and
240 index.
242 >>> target = {'dict': {'x': [5, 6, 7]}}
243 >>> glom(target, Delete('dict.x.1'))
244 {'dict': {'x': [5, 7]}}
245 >>> glom(target, Delete('dict.x'))
246 {'dict': {}}
248 If a target path is missing, a :exc:`PathDeleteError` will be
249 raised. To ignore missing targets, use the ``ignore_missing``
250 flag:
252 >>> glom(target, Delete('does_not_exist', ignore_missing=True))
253 {'dict': {}}
255 ``Delete`` has built-in support for deleting attributes of
256 objects, keys of dicts, and indexes of sequences
257 (like lists). Additional types can be registered through
258 :func:`~glom.register()` using the ``"delete"`` operation name.
260 .. versionadded:: 20.5.0
261 """
262 def __init__(self, path, ignore_missing=False):
263 if isinstance(path, basestring):
264 path = Path.from_text(path)
265 elif type(path) is TType:
266 path = Path(path)
267 elif not isinstance(path, Path):
268 raise TypeError('path argument must be a .-delimited string, Path, T, or S')
270 try:
271 self.op, self.arg = path.items()[-1]
272 except IndexError:
273 raise ValueError('path must have at least one element')
274 self._orig_path = path
275 self.path = path[:-1]
277 if self.op not in '[.P':
278 raise ValueError('last part of path must be an attribute or index')
280 self.ignore_missing = ignore_missing
282 def _del_one(self, dest, op, arg, scope):
283 if op == '[':
284 try:
285 del dest[arg]
286 except IndexError as e:
287 if not self.ignore_missing:
288 raise PathDeleteError(e, self.path, arg)
289 elif op == '.':
290 try:
291 delattr(dest, arg)
292 except AttributeError as e:
293 if not self.ignore_missing:
294 raise PathDeleteError(e, self.path, arg)
295 elif op == 'P':
296 _delete = scope[TargetRegistry].get_handler('delete', dest)
297 try:
298 _delete(dest, arg)
299 except Exception as e:
300 if not self.ignore_missing:
301 raise PathDeleteError(e, self.path, arg)
303 def glomit(self, target, scope):
304 op, arg, path = self.op, self.arg, self.path
305 if self.path.startswith(S):
306 dest_target = scope[UP]
307 dest_path = self.path.from_t()
308 else:
309 dest_target = target
310 dest_path = self.path
311 try:
312 dest = scope[glom](dest_target, dest_path, scope)
313 except PathAccessError as pae:
314 if not self.ignore_missing:
315 raise
316 else:
317 _apply_for_each(lambda dest: self._del_one(dest, op, arg, scope), path, dest)
319 return target
321 def __repr__(self):
322 cn = self.__class__.__name__
323 return '%s(%r)' % (cn, self._orig_path)
326def delete(obj, path, ignore_missing=False):
327 """
328 The ``delete()`` function provides "deep del" functionality,
329 modifying nested data structures in-place::
331 >>> target = {'a': [{'b': 'c'}, {'d': None}]}
332 >>> delete(target, 'a.0.b')
333 {'a': [{}, {'d': None}]}
335 Attempting to delete missing keys, attributes, and indexes will
336 raise a :exc:`PathDeleteError`. To ignore these errors, use the
337 *ignore_missing* argument::
339 >>> delete(target, 'does_not_exist', ignore_missing=True)
340 {'a': [{}, {'d': None}]}
342 For more information and examples, see the :class:`~glom.Delete`
343 specifier type, which this convenience function wraps.
345 .. versionadded:: 20.5.0
346 """
347 return glom(obj, Delete(path, ignore_missing=ignore_missing))
350def _del_sequence_item(target, idx):
351 del target[int(idx)]
354def _delete_autodiscover(type_obj):
355 if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES):
356 return False
358 if callable(getattr(type_obj, '__delitem__', None)):
359 if callable(getattr(type_obj, 'index', None)):
360 return _del_sequence_item
361 return operator.delitem
362 return delattr
365register_op('delete', auto_func=_delete_autodiscover, exact=False)