13
13
class JSONPath (object ):
14
14
"""
15
15
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
17
17
JSONPath semantics.
18
18
"""
19
19
@@ -56,8 +56,8 @@ class DatumInContext(object):
56
56
"""
57
57
Represents a datum along a path from a context.
58
58
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
61
61
representation of the context.
62
62
63
63
For quick-and-dirty work, this proxies any non-special attributes
@@ -118,17 +118,17 @@ class AutoIdForDatum(DatumInContext):
118
118
"""
119
119
This behaves like a DatumInContext, but the value is
120
120
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
122
122
segment of the path
123
123
124
124
For example, it will make "foo.bar.id" return a datum
125
125
that behaves like DatumInContext(value="foo.bar", path="foo.bar.id").
126
126
127
127
This is disabled by default; it can be turned on by
128
128
settings the `auto_id_field` global to a value other
129
- than `None`.
129
+ than `None`.
130
130
"""
131
-
131
+
132
132
def __init__ (self , datum , id_field = None ):
133
133
"""
134
134
Invariant is that datum.path is the path from context to datum. The auto id
@@ -215,7 +215,7 @@ class Child(JSONPath):
215
215
JSONPath that first matches the left, then the right.
216
216
Concrete syntax is <left> '.' <right>
217
217
"""
218
-
218
+
219
219
def __init__ (self , left , right ):
220
220
self .left = left
221
221
self .right = right
@@ -225,7 +225,7 @@ def find(self, datum):
225
225
Extra special case: auto ids do not have children,
226
226
so cut it off right now rather than auto id the auto id
227
227
"""
228
-
228
+
229
229
return [submatch
230
230
for subdata in self .left .find (datum )
231
231
if not isinstance (subdata , AutoIdForDatum )
@@ -264,7 +264,7 @@ def __str__(self):
264
264
265
265
def __repr__ (self ):
266
266
return 'Parent()'
267
-
267
+
268
268
269
269
class Where (JSONPath ):
270
270
"""
@@ -275,7 +275,7 @@ class Where(JSONPath):
275
275
WARNING: Subject to change. May want to have "contains"
276
276
or some other better word for it.
277
277
"""
278
-
278
+
279
279
def __init__ (self , left , right ):
280
280
self .left = left
281
281
self .right = right
@@ -299,7 +299,7 @@ class Descendants(JSONPath):
299
299
JSONPath that matches first the left expression then any descendant
300
300
of it which matches the right expression.
301
301
"""
302
-
302
+
303
303
def __init__ (self , left , right ):
304
304
self .left = left
305
305
self .right = right
@@ -308,7 +308,7 @@ def find(self, datum):
308
308
# <left> .. <right> ==> <left> . (<right> | *..<right> | [*]..<right>)
309
309
#
310
310
# 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
312
312
# infinite loop. So right here we implement the coercion-free version.
313
313
314
314
# Get all left matches into a list
@@ -334,12 +334,12 @@ def match_recursively(datum):
334
334
recursive_matches = []
335
335
336
336
return right_matches + list (recursive_matches )
337
-
337
+
338
338
# TODO: repeatable iterator instead of list?
339
339
return [submatch
340
340
for left_match in left_matches
341
341
for submatch in match_recursively (left_match )]
342
-
342
+
343
343
def is_singular ():
344
344
return False
345
345
@@ -425,7 +425,7 @@ class Fields(JSONPath):
425
425
WARNING: If '*' is any of the field names, then they will
426
426
all be returned.
427
427
"""
428
-
428
+
429
429
def __init__ (self , * fields ):
430
430
self .fields = fields
431
431
@@ -451,14 +451,18 @@ def reified_fields(self, datum):
451
451
452
452
def find (self , datum ):
453
453
datum = DatumInContext .wrap (datum )
454
-
454
+
455
455
return [field_datum
456
456
for field_datum in [self .get_field_datum (datum , field ) for field in self .reified_fields (datum )]
457
457
if field_datum is not None ]
458
458
459
459
def update (self , data , val ):
460
460
for field in self .reified_fields (DatumInContext .wrap (data )):
461
461
if field in data :
462
+ if hasattr (val , '__call__' ):
463
+ val (data [field ], data , field )
464
+ else :
465
+ data [field ] = val
462
466
data [field ] = val
463
467
return data
464
468
@@ -475,7 +479,7 @@ def __eq__(self, other):
475
479
class Index (JSONPath ):
476
480
"""
477
481
JSONPath that matches indices of the current datum, or none if not large enough.
478
- Concrete syntax is brackets.
482
+ Concrete syntax is brackets.
479
483
480
484
WARNING: If the datum is None or not long enough, it will not crash but will not match anything.
481
485
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):
486
490
487
491
def find (self , datum ):
488
492
datum = DatumInContext .wrap (datum )
489
-
493
+
490
494
if datum .value and len (datum .value ) > self .index :
491
495
return [DatumInContext (datum .value [self .index ], path = self , context = datum )]
492
496
else :
493
497
return []
494
498
495
499
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 :
497
503
data [self .index ] = val
498
504
return data
499
505
@@ -505,15 +511,15 @@ def __str__(self):
505
511
506
512
class Slice (JSONPath ):
507
513
"""
508
- JSONPath matching a slice of an array.
514
+ JSONPath matching a slice of an array.
509
515
510
516
Because of a mismatch between JSON and XML when schema-unaware,
511
517
this always returns an iterable; if the incoming data
512
518
was not a list, then it returns a one element list _containing_ that
513
519
data.
514
520
515
521
Consider these two docs, and their schema-unaware translation to JSON:
516
-
522
+
517
523
<a><b>hello</b></a> ==> {"a": {"b": "hello"}}
518
524
<a><b>hello</b><b>goodbye</b></a> ==> {"a": {"b": ["hello", "goodbye"]}}
519
525
@@ -531,10 +537,10 @@ def __init__(self, start=None, end=None, step=None):
531
537
self .start = start
532
538
self .end = end
533
539
self .step = step
534
-
540
+
535
541
def find (self , datum ):
536
542
datum = DatumInContext .wrap (datum )
537
-
543
+
538
544
# Here's the hack. If it is a dictionary or some kind of constant,
539
545
# put it in a single-element list
540
546
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):
556
562
if self .start == None and self .end == None and self .step == None :
557
563
return '[*]'
558
564
else :
559
- return '[%s%s%s]' % (self .start or '' ,
565
+ return '[%s%s%s]' % (self .start or '' ,
560
566
':%d' % self .end if self .end else '' ,
561
567
':%d' % self .step if self .step else '' )
562
568
0 commit comments