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

152 statements  

1"""By default, glom aims to safely return a transformed copy of your 

2data. But sometimes you really need to transform an existing object. 

3 

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. 

8 

9.. warning:: 

10 

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.  

14  

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. 

17 

18""" 

19import operator 

20from pprint import pprint 

21 

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 

24 

25 

26try: 

27 basestring 

28except NameError: 

29 basestring = str 

30 

31 

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__ 

36 

37 

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. 

42 

43 One example would be deleting an out-of-range position in a list:: 

44 

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(... 

49 

50 Other assignment failures could be due to deleting a read-only 

51 ``@property`` or exception being raised inside a ``__delattr__()``. 

52 

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)) 

57 

58 

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) 

68 

69 

70class Assign: 

71 """*New in glom 18.3.0* 

72 

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. 

76 

77 ``Assign`` can be used to perform spot modifications of large data 

78 structures when making a copy is not desired:: 

79 

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'}} 

86 

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:: 

89 

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'}} 

94 

95 Another handy use of Assign is to deep-apply a function:: 

96 

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]}} 

102 

103 Like many other specifier types, ``Assign``'s destination path can be 

104 a :data:`~glom.T` expression, for maximum control:: 

105 

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' 

112 

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. 

117 

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`. 

122 

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. 

127 

128 >>> target = {} 

129 >>> assign(target, 'a.b.c', 'hi', missing=dict) 

130 {'a': {'b': {'c': 'hi'}}} 

131 

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') 

143 

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] 

150 

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 

155 

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 

160 

161 def glomit(self, target, scope): 

162 val = arg_val(target, self.val, scope) 

163 

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 

176 

177 remaining_path = self._orig_path[pae.part_idx + 1:] 

178 val = scope[glom](self.missing(), Assign(remaining_path, val, missing=self.missing), scope) 

179 

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) 

183 

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) 

188 

189 return target 

190 

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)})' 

196 

197 

198def assign(obj, path, val, missing=None): 

199 """*New in glom 18.3.0* 

200 

201 The ``assign()`` function provides convenient "deep set" 

202 functionality, modifying nested data structures in-place:: 

203 

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'}]} 

208 

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)) 

214 

215 

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}) 

222 

223 

224def _set_sequence_item(target, idx, val): 

225 target[int(idx)] = val 

226 

227 

228def _assign_autodiscover(type_obj): 

229 # TODO: issubclass or "in"? 

230 if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES): 

231 return False 

232 

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 

237 

238 return setattr 

239 

240 

241register_op('assign', auto_func=_assign_autodiscover, exact=False) 

242 

243 

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. 

250 

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': {}} 

256 

257 If a target path is missing, a :exc:`PathDeleteError` will be 

258 raised. To ignore missing targets, use the ``ignore_missing`` 

259 flag: 

260 

261 >>> glom(target, Delete('does_not_exist', ignore_missing=True)) 

262 {'dict': {}} 

263 

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. 

268 

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') 

278 

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] 

285 

286 if self.op not in '[.P': 

287 raise ValueError('last part of path must be an attribute or index') 

288 

289 self.ignore_missing = ignore_missing 

290 

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) 

311 

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) 

327 

328 return target 

329 

330 def __repr__(self): 

331 cn = self.__class__.__name__ 

332 return f'{cn}({self._orig_path!r})' 

333 

334 

335def delete(obj, path, ignore_missing=False): 

336 """ 

337 The ``delete()`` function provides "deep del" functionality, 

338 modifying nested data structures in-place:: 

339 

340 >>> target = {'a': [{'b': 'c'}, {'d': None}]} 

341 >>> delete(target, 'a.0.b') 

342 {'a': [{}, {'d': None}]} 

343 

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:: 

347 

348 >>> delete(target, 'does_not_exist', ignore_missing=True) 

349 {'a': [{}, {'d': None}]} 

350 

351 For more information and examples, see the :class:`~glom.Delete` 

352 specifier type, which this convenience function wraps. 

353 

354 .. versionadded:: 20.5.0 

355 """ 

356 return glom(obj, Delete(path, ignore_missing=ignore_missing)) 

357 

358 

359def _del_sequence_item(target, idx): 

360 del target[int(idx)] 

361 

362 

363def _delete_autodiscover(type_obj): 

364 if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES): 

365 return False 

366 

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 

372 

373 

374register_op('delete', auto_func=_delete_autodiscover, exact=False)