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

151 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""" 

10import operator 

11from pprint import pprint 

12 

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 

15 

16 

17try: 

18 basestring 

19except NameError: 

20 basestring = str 

21 

22 

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__ 

27 

28 

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. 

33 

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

35 

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

40 

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

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

43 

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

48 

49 

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) 

59 

60 

61class Assign(object): 

62 """*New in glom 18.3.0* 

63 

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. 

67 

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

69 structures when making a copy is not desired:: 

70 

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

77 

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

80 

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

85 

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

87 

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

93 

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

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

96 

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' 

103 

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. 

108 

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

113 

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. 

118 

119 >>> target = {} 

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

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

122 

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

134 

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] 

141 

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 

146 

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 

151 

152 def glomit(self, target, scope): 

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

154 

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 

167 

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

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

170 

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) 

174 

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) 

179 

180 return target 

181 

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

187 

188 

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

190 """*New in glom 18.3.0* 

191 

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

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

194 

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

199 

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

205 

206 

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

213 

214 

215def _set_sequence_item(target, idx, val): 

216 target[int(idx)] = val 

217 

218 

219def _assign_autodiscover(type_obj): 

220 # TODO: issubclass or "in"? 

221 if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES): 

222 return False 

223 

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 

228 

229 return setattr 

230 

231 

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

233 

234 

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. 

241 

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

247 

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

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

250 flag: 

251 

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

253 {'dict': {}} 

254 

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. 

259 

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

269 

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] 

276 

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

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

279 

280 self.ignore_missing = ignore_missing 

281 

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) 

302 

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) 

318 

319 return target 

320 

321 def __repr__(self): 

322 cn = self.__class__.__name__ 

323 return '%s(%r)' % (cn, self._orig_path) 

324 

325 

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

327 """ 

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

329 modifying nested data structures in-place:: 

330 

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

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

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

334 

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

338 

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

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

341 

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

343 specifier type, which this convenience function wraps. 

344 

345 .. versionadded:: 20.5.0 

346 """ 

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

348 

349 

350def _del_sequence_item(target, idx): 

351 del target[int(idx)] 

352 

353 

354def _delete_autodiscover(type_obj): 

355 if issubclass(type_obj, _UNASSIGNABLE_BASE_TYPES): 

356 return False 

357 

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 

363 

364 

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