Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/glom/mutation.py: 57%
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.. warning::
11 glom's deep assignment is powerful, and incorrect use can result in
12 unintended assignments to global state, including class and module
13 attributes, as well as function defaults.
15 Be careful when writing assignment specs, and especially careful when
16 any part of the spec is data-driven or provided by an end user.
18"""
19import operator
20from pprint import pprint
22from .core import Path, T, S, Spec, glom, UnregisteredTarget, GlomError, PathAccessError, UP
23from .core import TType, register_op, TargetRegistry, bbrepr, PathAssignError, arg_val, _assign_op
26try:
27 basestring
28except NameError:
29 basestring = str
32if getattr(__builtins__, '__dict__', None) is not None:
33 # pypy's __builtins__ is a module, as is CPython's REPL, but at
34 # normal execution time it's a dict?
35 __builtins__ = __builtins__.__dict__
38class PathDeleteError(PathAssignError):
39 """This :exc:`GlomError` subtype is raised when an assignment fails,
40 stemming from an :func:`~glom.delete` call or other
41 :class:`~glom.Delete` usage.
43 One example would be deleting an out-of-range position in a list::
45 >>> delete(["short", "list"], Path(5))
46 Traceback (most recent call last):
47 ...
48 PathDeleteError: could not delete 5 on object at Path(), got error: IndexError(...
50 Other assignment failures could be due to deleting a read-only
51 ``@property`` or exception being raised inside a ``__delattr__()``.
53 """
54 def get_message(self):
55 return ('could not delete %r on object at %r, got error: %r'
56 % (self.dest_name, self.path, self.exc))
59def _apply_for_each(func, path, val):
60 layers = path.path_t.__stars__()
61 if layers:
62 for i in range(layers - 1):
63 val = sum(val, []) # flatten out the extra layers
64 for inner in val:
65 func(inner)
66 else:
67 func(val)
70class Assign:
71 """*New in glom 18.3.0*
73 The ``Assign`` specifier type enables glom to modify the target,
74 performing a "deep-set" to mirror glom's original deep-get use
75 case.
77 ``Assign`` can be used to perform spot modifications of large data
78 structures when making a copy is not desired::
80 # deep assignment into a nested dictionary
81 >>> target = {'a': {}}
82 >>> spec = Assign('a.b', 'value')
83 >>> _ = glom(target, spec)
84 >>> pprint(target)
85 {'a': {'b': 'value'}}
87 The value to be assigned can also be a :class:`~glom.Spec`, which
88 is useful for copying values around within the data structure::
90 # copying one nested value to another
91 >>> _ = glom(target, Assign('a.c', Spec('a.b')))
92 >>> pprint(target)
93 {'a': {'b': 'value', 'c': 'value'}}
95 Another handy use of Assign is to deep-apply a function::
97 # sort a deep nested list
98 >>> target={'a':{'b':[3,1,2]}}
99 >>> _ = glom(target, Assign('a.b', Spec(('a.b',sorted))))
100 >>> pprint(target)
101 {'a': {'b': [1, 2, 3]}}
103 Like many other specifier types, ``Assign``'s destination path can be
104 a :data:`~glom.T` expression, for maximum control::
106 # changing the error message of an exception in an error list
107 >>> err = ValueError('initial message')
108 >>> target = {'errors': [err]}
109 >>> _ = glom(target, Assign(T['errors'][0].args, ('new message',)))
110 >>> str(err)
111 'new message'
113 ``Assign`` has built-in support for assigning to attributes of
114 objects, keys of mappings (like dicts), and indexes of sequences
115 (like lists). Additional types can be registered through
116 :func:`~glom.register()` using the ``"assign"`` operation name.
118 Attempting to assign to an immutable structure, like a
119 :class:`tuple`, will result in a
120 :class:`~glom.PathAssignError`. Attempting to assign to a path
121 that doesn't exist will raise a :class:`~PathAccessError`.
123 To automatically backfill missing structures, you can pass a
124 callable to the *missing* argument. This callable will be called
125 for each path segment along the assignment which is not
126 present.
128 >>> target = {}
129 >>> assign(target, 'a.b.c', 'hi', missing=dict)
130 {'a': {'b': {'c': 'hi'}}}
132 """
133 def __init__(self, path, val, missing=None):
134 # TODO: an option like require_preexisting or something to
135 # ensure that a value is mutated, not just added. Current
136 # workaround is to do a Check().
137 if isinstance(path, basestring):
138 path = Path.from_text(path)
139 elif type(path) is TType:
140 path = Path(path)
141 elif not isinstance(path, Path):
142 raise TypeError('path argument must be a .-delimited string, Path, T, or S')
144 try:
145 self.op, self.arg = path.items()[-1]
146 except IndexError:
147 raise ValueError('path must have at least one element')
148 self._orig_path = path
149 self.path = path[:-1]
151 if self.op not in '[.P':
152 # maybe if we add null-coalescing this should do something?
153 raise ValueError('last part of path must be setattr or setitem')
154 self.val = val
156 if missing is not None:
157 if not callable(missing):
158 raise TypeError(f'expected missing to be callable, not {missing!r}')
159 self.missing = missing
161 def glomit(self, target, scope):
162 val = arg_val(target, self.val, scope)
164 op, arg, path = self.op, self.arg, self.path
165 if self.path.startswith(S):
166 dest_target = scope[UP]
167 dest_path = self.path.from_t()
168 else:
169 dest_target = target
170 dest_path = self.path
171 try:
172 dest = scope[glom](dest_target, dest_path, scope)
173 except PathAccessError as pae:
174 if not self.missing:
175 raise
177 remaining_path = self._orig_path[pae.part_idx + 1:]
178 val = scope[glom](self.missing(), Assign(remaining_path, val, missing=self.missing), scope)
180 op, arg = self._orig_path.items()[pae.part_idx]
181 path = self._orig_path[:pae.part_idx]
182 dest = scope[glom](dest_target, path, scope)
184 # TODO: forward-detect immutable dest?
185 _apply = lambda dest: _assign_op(
186 dest=dest, op=op, arg=arg, val=val, path=path, scope=scope)
187 _apply_for_each(_apply, path, dest)
189 return target
191 def __repr__(self):
192 cn = self.__class__.__name__
193 if self.missing is None:
194 return f'{cn}({self._orig_path!r}, {self.val!r})'
195 return f'{cn}({self._orig_path!r}, {self.val!r}, missing={bbrepr(self.missing)})'
198def assign(obj, path, val, missing=None):
199 """*New in glom 18.3.0*
201 The ``assign()`` function provides convenient "deep set"
202 functionality, modifying nested data structures in-place::
204 >>> target = {'a': [{'b': 'c'}, {'d': None}]}
205 >>> _ = assign(target, 'a.1.d', 'e') # let's give 'd' a value of 'e'
206 >>> pprint(target)
207 {'a': [{'b': 'c'}, {'d': 'e'}]}
209 Missing structures can also be automatically created with the
210 *missing* parameter. For more information and examples, see the
211 :class:`~glom.Assign` specifier type, which this function wraps.
212 """
213 return glom(obj, Assign(path, val, missing=missing))
216_ALL_BUILTIN_TYPES = [v for v in __builtins__.values() if isinstance(v, type)]
217_BUILTIN_BASE_TYPES = [v for v in _ALL_BUILTIN_TYPES
218 if not issubclass(v, tuple([t for t in _ALL_BUILTIN_TYPES
219 if t not in (v, type, object)]))]
220_UNASSIGNABLE_BASE_TYPES = tuple(set(_BUILTIN_BASE_TYPES)
221 - {dict, list, BaseException, object, type})
224def _set_sequence_item(target, idx, val):
225 target[int(idx)] = val
228def _assign_autodiscover(type_obj):
229 # TODO: issubclass or "in"?
230 if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES):
231 return False
233 if callable(getattr(type_obj, '__setitem__', None)):
234 if callable(getattr(type_obj, 'index', None)):
235 return _set_sequence_item
236 return operator.setitem
238 return setattr
241register_op('assign', auto_func=_assign_autodiscover, exact=False)
244class Delete:
245 """
246 In addition to glom's core "deep-get" and ``Assign``'s "deep-set",
247 the ``Delete`` specifier type performs a "deep-del", which can
248 remove items from larger data structures by key, attribute, and
249 index.
251 >>> target = {'dict': {'x': [5, 6, 7]}}
252 >>> glom(target, Delete('dict.x.1'))
253 {'dict': {'x': [5, 7]}}
254 >>> glom(target, Delete('dict.x'))
255 {'dict': {}}
257 If a target path is missing, a :exc:`PathDeleteError` will be
258 raised. To ignore missing targets, use the ``ignore_missing``
259 flag:
261 >>> glom(target, Delete('does_not_exist', ignore_missing=True))
262 {'dict': {}}
264 ``Delete`` has built-in support for deleting attributes of
265 objects, keys of dicts, and indexes of sequences
266 (like lists). Additional types can be registered through
267 :func:`~glom.register()` using the ``"delete"`` operation name.
269 .. versionadded:: 20.5.0
270 """
271 def __init__(self, path, ignore_missing=False):
272 if isinstance(path, basestring):
273 path = Path.from_text(path)
274 elif type(path) is TType:
275 path = Path(path)
276 elif not isinstance(path, Path):
277 raise TypeError('path argument must be a .-delimited string, Path, T, or S')
279 try:
280 self.op, self.arg = path.items()[-1]
281 except IndexError:
282 raise ValueError('path must have at least one element')
283 self._orig_path = path
284 self.path = path[:-1]
286 if self.op not in '[.P':
287 raise ValueError('last part of path must be an attribute or index')
289 self.ignore_missing = ignore_missing
291 def _del_one(self, dest, op, arg, scope):
292 if op == '[':
293 try:
294 del dest[arg]
295 except IndexError as e:
296 if not self.ignore_missing:
297 raise PathDeleteError(e, self.path, arg)
298 elif op == '.':
299 try:
300 delattr(dest, arg)
301 except AttributeError as e:
302 if not self.ignore_missing:
303 raise PathDeleteError(e, self.path, arg)
304 elif op == 'P':
305 _delete = scope[TargetRegistry].get_handler('delete', dest)
306 try:
307 _delete(dest, arg)
308 except Exception as e:
309 if not self.ignore_missing:
310 raise PathDeleteError(e, self.path, arg)
312 def glomit(self, target, scope):
313 op, arg, path = self.op, self.arg, self.path
314 if self.path.startswith(S):
315 dest_target = scope[UP]
316 dest_path = self.path.from_t()
317 else:
318 dest_target = target
319 dest_path = self.path
320 try:
321 dest = scope[glom](dest_target, dest_path, scope)
322 except PathAccessError as pae:
323 if not self.ignore_missing:
324 raise
325 else:
326 _apply_for_each(lambda dest: self._del_one(dest, op, arg, scope), path, dest)
328 return target
330 def __repr__(self):
331 cn = self.__class__.__name__
332 return f'{cn}({self._orig_path!r})'
335def delete(obj, path, ignore_missing=False):
336 """
337 The ``delete()`` function provides "deep del" functionality,
338 modifying nested data structures in-place::
340 >>> target = {'a': [{'b': 'c'}, {'d': None}]}
341 >>> delete(target, 'a.0.b')
342 {'a': [{}, {'d': None}]}
344 Attempting to delete missing keys, attributes, and indexes will
345 raise a :exc:`PathDeleteError`. To ignore these errors, use the
346 *ignore_missing* argument::
348 >>> delete(target, 'does_not_exist', ignore_missing=True)
349 {'a': [{}, {'d': None}]}
351 For more information and examples, see the :class:`~glom.Delete`
352 specifier type, which this convenience function wraps.
354 .. versionadded:: 20.5.0
355 """
356 return glom(obj, Delete(path, ignore_missing=ignore_missing))
359def _del_sequence_item(target, idx):
360 del target[int(idx)]
363def _delete_autodiscover(type_obj):
364 if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES):
365 return False
367 if callable(getattr(type_obj, '__delitem__', None)):
368 if callable(getattr(type_obj, 'index', None)):
369 return _del_sequence_item
370 return operator.delitem
371 return delattr
374register_op('delete', auto_func=_delete_autodiscover, exact=False)