Skip to content

Commit 2e9030f

Browse files
committed
feat(jsonpath): allow callable object in update
1 parent f2d1ce1 commit 2e9030f

File tree

1 file changed

+30
-24
lines changed

1 file changed

+30
-24
lines changed

jsonpath_rw/jsonpath.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
class JSONPath(object):
1414
"""
1515
The base class for JSONPath abstract syntax; those
16-
methods stubbed here are the interface to supported
16+
methods stubbed here are the interface to supported
1717
JSONPath semantics.
1818
"""
1919

@@ -56,8 +56,8 @@ class DatumInContext(object):
5656
"""
5757
Represents a datum along a path from a context.
5858
59-
Essentially a zipper but with a structure represented by JsonPath,
60-
and where the context is more of a parent pointer than a proper
59+
Essentially a zipper but with a structure represented by JsonPath,
60+
and where the context is more of a parent pointer than a proper
6161
representation of the context.
6262
6363
For quick-and-dirty work, this proxies any non-special attributes
@@ -118,17 +118,17 @@ class AutoIdForDatum(DatumInContext):
118118
"""
119119
This behaves like a DatumInContext, but the value is
120120
always the path leading up to it, not including the "id",
121-
and with any "id" fields along the way replacing the prior
121+
and with any "id" fields along the way replacing the prior
122122
segment of the path
123123
124124
For example, it will make "foo.bar.id" return a datum
125125
that behaves like DatumInContext(value="foo.bar", path="foo.bar.id").
126126
127127
This is disabled by default; it can be turned on by
128128
settings the `auto_id_field` global to a value other
129-
than `None`.
129+
than `None`.
130130
"""
131-
131+
132132
def __init__(self, datum, id_field=None):
133133
"""
134134
Invariant is that datum.path is the path from context to datum. The auto id
@@ -215,7 +215,7 @@ class Child(JSONPath):
215215
JSONPath that first matches the left, then the right.
216216
Concrete syntax is <left> '.' <right>
217217
"""
218-
218+
219219
def __init__(self, left, right):
220220
self.left = left
221221
self.right = right
@@ -225,7 +225,7 @@ def find(self, datum):
225225
Extra special case: auto ids do not have children,
226226
so cut it off right now rather than auto id the auto id
227227
"""
228-
228+
229229
return [submatch
230230
for subdata in self.left.find(datum)
231231
if not isinstance(subdata, AutoIdForDatum)
@@ -264,7 +264,7 @@ def __str__(self):
264264

265265
def __repr__(self):
266266
return 'Parent()'
267-
267+
268268

269269
class Where(JSONPath):
270270
"""
@@ -275,7 +275,7 @@ class Where(JSONPath):
275275
WARNING: Subject to change. May want to have "contains"
276276
or some other better word for it.
277277
"""
278-
278+
279279
def __init__(self, left, right):
280280
self.left = left
281281
self.right = right
@@ -299,7 +299,7 @@ class Descendants(JSONPath):
299299
JSONPath that matches first the left expression then any descendant
300300
of it which matches the right expression.
301301
"""
302-
302+
303303
def __init__(self, left, right):
304304
self.left = left
305305
self.right = right
@@ -308,7 +308,7 @@ def find(self, datum):
308308
# <left> .. <right> ==> <left> . (<right> | *..<right> | [*]..<right>)
309309
#
310310
# With with a wonky caveat that since Slice() has funky coercions
311-
# we cannot just delegate to that equivalence or we'll hit an
311+
# we cannot just delegate to that equivalence or we'll hit an
312312
# infinite loop. So right here we implement the coercion-free version.
313313

314314
# Get all left matches into a list
@@ -334,12 +334,12 @@ def match_recursively(datum):
334334
recursive_matches = []
335335

336336
return right_matches + list(recursive_matches)
337-
337+
338338
# TODO: repeatable iterator instead of list?
339339
return [submatch
340340
for left_match in left_matches
341341
for submatch in match_recursively(left_match)]
342-
342+
343343
def is_singular():
344344
return False
345345

@@ -425,7 +425,7 @@ class Fields(JSONPath):
425425
WARNING: If '*' is any of the field names, then they will
426426
all be returned.
427427
"""
428-
428+
429429
def __init__(self, *fields):
430430
self.fields = fields
431431

@@ -451,14 +451,18 @@ def reified_fields(self, datum):
451451

452452
def find(self, datum):
453453
datum = DatumInContext.wrap(datum)
454-
454+
455455
return [field_datum
456456
for field_datum in [self.get_field_datum(datum, field) for field in self.reified_fields(datum)]
457457
if field_datum is not None]
458458

459459
def update(self, data, val):
460460
for field in self.reified_fields(DatumInContext.wrap(data)):
461461
if field in data:
462+
if hasattr(val, '__call__'):
463+
val(data[field], data, field)
464+
else:
465+
data[field] = val
462466
data[field] = val
463467
return data
464468

@@ -475,7 +479,7 @@ def __eq__(self, other):
475479
class Index(JSONPath):
476480
"""
477481
JSONPath that matches indices of the current datum, or none if not large enough.
478-
Concrete syntax is brackets.
482+
Concrete syntax is brackets.
479483
480484
WARNING: If the datum is None or not long enough, it will not crash but will not match anything.
481485
NOTE: For the concrete syntax of `[*]`, the abstract syntax is a Slice() with no parameters (equiv to `[:]`
@@ -486,14 +490,16 @@ def __init__(self, index):
486490

487491
def find(self, datum):
488492
datum = DatumInContext.wrap(datum)
489-
493+
490494
if datum.value and len(datum.value) > self.index:
491495
return [DatumInContext(datum.value[self.index], path=self, context=datum)]
492496
else:
493497
return []
494498

495499
def update(self, data, val):
496-
if len(data) > self.index:
500+
if hasattr(val, '__call__'):
501+
val.__call__(data[self.index], data, self.index)
502+
elif len(data) > self.index:
497503
data[self.index] = val
498504
return data
499505

@@ -505,15 +511,15 @@ def __str__(self):
505511

506512
class Slice(JSONPath):
507513
"""
508-
JSONPath matching a slice of an array.
514+
JSONPath matching a slice of an array.
509515
510516
Because of a mismatch between JSON and XML when schema-unaware,
511517
this always returns an iterable; if the incoming data
512518
was not a list, then it returns a one element list _containing_ that
513519
data.
514520
515521
Consider these two docs, and their schema-unaware translation to JSON:
516-
522+
517523
<a><b>hello</b></a> ==> {"a": {"b": "hello"}}
518524
<a><b>hello</b><b>goodbye</b></a> ==> {"a": {"b": ["hello", "goodbye"]}}
519525
@@ -531,10 +537,10 @@ def __init__(self, start=None, end=None, step=None):
531537
self.start = start
532538
self.end = end
533539
self.step = step
534-
540+
535541
def find(self, datum):
536542
datum = DatumInContext.wrap(datum)
537-
543+
538544
# Here's the hack. If it is a dictionary or some kind of constant,
539545
# put it in a single-element list
540546
if (isinstance(datum.value, dict) or isinstance(datum.value, six.integer_types) or isinstance(datum.value, six.string_types)):
@@ -556,7 +562,7 @@ def __str__(self):
556562
if self.start == None and self.end == None and self.step == None:
557563
return '[*]'
558564
else:
559-
return '[%s%s%s]' % (self.start or '',
565+
return '[%s%s%s]' % (self.start or '',
560566
':%d'%self.end if self.end else '',
561567
':%d'%self.step if self.step else '')
562568

0 commit comments

Comments
 (0)