@@ -17,7 +17,7 @@ class JSONPath(object):
1717
1818 def find (self , data ):
1919 """
20- All `JSONPath` types support `find()`, which returns an iterable of `DatumAtPath `s.
20+ All `JSONPath` types support `find()`, which returns an iterable of `DatumInContext `s.
2121 They keep track of the path followed to the current location, so if the calling code
2222 has some opinion about that, it can be passed in here as a starting point.
2323 """
@@ -38,95 +38,173 @@ def child(self, child):
3838 else :
3939 return Child (self , child )
4040
41- class DatumAtPath (object ):
41+ def make_datum (self , value ):
42+ if isinstance (value , DatumInContext ):
43+ return value
44+ else :
45+ return DatumInContext (value , path = Root (), context = None )
46+
47+ class DatumInContext (object ):
4248 """
43- Represents a single datum along with the path followed to locate it.
49+ Represents a datum along a path from a context.
50+
51+ Essentially a zipper but with a structure represented by JsonPath,
52+ and where the context is more of a parent pointer than a proper
53+ representation of the context.
4454
4555 For quick-and-dirty work, this proxies any non-special attributes
4656 to the underlying datum, but the actual datum can (and usually should)
4757 be retrieved via the `value` attribute.
4858
49- To place `datum` within a path, use `datum.in_context(path)`, which prepends
50- `path` to that already stored.
59+ To place `datum` within another, use `datum.in_context(context=..., path=...)`
60+ which extends the path. If the datum already has a context, it places the entire
61+ context within that passed in, so an object can be built from the inside
62+ out.
5163 """
52- def __init__ (self , value , path ):
64+ @classmethod
65+ def wrap (cls , data ):
66+ if isinstance (data , cls ):
67+ return data
68+ else :
69+ return cls (data )
70+
71+ def __init__ (self , value , path = None , context = None ):
5372 self .value = value
54- self .path = path
73+ self .path = path or This ()
74+ self .context = None if context is None else DatumInContext .wrap (context )
75+
76+ def in_context (self , context , path ):
77+ context = DatumInContext .wrap (context )
5578
56- def __getattr__ (self , attr ):
57- if attr == 'id' and not hasattr (self .value , 'id' ):
58- return str (self .path )
79+ if self .context :
80+ return DatumInContext (value = self .value , path = self .path , context = context .in_context (path = path , context = context ))
5981 else :
60- return getattr ( self .value , attr )
82+ return DatumInContext ( value = self .value , path = path , context = context )
6183
62- def __str__ (self ):
63- return str (self .value )
84+ @property
85+ def full_path (self ):
86+ return self .path if self .context is None else self .context .full_path .child (self .path )
6487
65- def in_context (self , context_path ):
66- return DatumAtPath (self .value , path = context_path .child (self .path ))
88+ @property
89+ def id_pseudopath (self ):
90+ """
91+ Looks like a path, but with ids stuck in when available
92+ """
93+ try :
94+ pseudopath = Fields (str (self .value [auto_id_field ]))
95+ except (TypeError , AttributeError , KeyError ): # This may not be all the interesting exceptions
96+ pseudopath = self .path
6797
68- class AutoIdDatum (DatumAtPath ):
98+ if self .context :
99+ return self .context .id_pseudopath .child (pseudopath )
100+ else :
101+ return pseudopath
102+
103+ def __repr__ (self ):
104+ return '%s(value=%r, path=%r, context=%r)' % (self .__class__ .__name__ , self .value , self .path , self .context )
105+
106+ def __eq__ (self , other ):
107+ return isinstance (other , DatumInContext ) and other .value == self .value and other .path == self .path and self .context == other .context
108+
109+ class AutoIdForDatum (DatumInContext ):
69110 """
70- This behaves like a DatumAtPath, but the value is
71- always the path leading up to it (not including the "id").
111+ This behaves like a DatumInContext, but the value is
112+ always the path leading up to it, not including the "id",
113+ and with any "id" fields along the way replacing the prior
114+ segment of the path
72115
73116 For example, it will make "foo.bar.id" return a datum
74- that behaves like DatumAtPath (value="foo.bar", path="foo.bar.id").
117+ that behaves like DatumInContext (value="foo.bar", path="foo.bar.id").
75118
76119 This is disabled by default; it can be turned on by
77120 settings the `auto_id_field` global to a value other
78121 than `None`.
79122 """
80123
81- def __init__ (self , auto_id ):
82- self .auto_id = auto_id
124+ def __init__ (self , datum , id_field = None ):
125+ """
126+ Invariant is that datum.path is the path from context to datum. The auto id
127+ will either be the id in the datum (if present) or the id of the context
128+ followed by the path to the datum.
129+
130+ The path to this datum is always the path to the context, the path to the
131+ datum, and then the auto id field.
132+ """
133+ self .datum = datum
134+ self .id_field = id_field or auto_id_field
83135
84136 @property
85137 def value (self ):
86- return str (self .auto_id )
138+ return str (self .datum . id_pseudopath )
87139
88140 @property
89141 def path (self ):
90- return self .auto_id . child ( Fields ( auto_id_field ))
142+ return self .id_field
91143
92- def __str__ (self ):
93- return str (self .path )
144+ @property
145+ def context (self ):
146+ return self .datum
147+
148+ def __repr__ (self ):
149+ return '%s(%r)' % (self .__class__ .__name__ , self .datum )
150+
151+ def in_context (self , context , path ):
152+ return AutoIdForDatum (self .datum .in_context (context = context , path = path ))
94153
95- def in_context (self , context_path ):
96- return AutoIdDatum ( context_path . child ( self .auto_id ))
154+ def __eq__ (self , other ):
155+ return isinstance ( other , AutoIdForDatum ) and other . datum == self .datum and self . id_field == other . id_field
97156
98157
99158class Root (JSONPath ):
100159 """
101- The JSONPath referring to the root object. Concrete syntax is '$'.
102-
103- WARNING! Currently synonymous with '@' because this library does not
104- keep track of parent pointers or any such thing.
160+ The JSONPath referring to the "root" object. Concrete syntax is '$'.
161+ The root is the topmost datum without any context attached.
105162 """
106163
107164 def find (self , data ):
108- return [DatumAtPath (data , path = Root ())]
165+ if not isinstance (data , DatumInContext ):
166+ return [DatumInContext (data , path = This (), context = None )]
167+ else :
168+ if data .context is None :
169+ return data
170+ else :
171+ return Root ().find (data .context )
109172
110173 def update (self , data , val ):
111174 return val
112175
113176 def __str__ (self ):
114177 return '$'
115178
179+ def __repr__ (self ):
180+ return 'Root()'
181+
182+ def __eq__ (self , other ):
183+ return isinstance (other , Root )
184+
116185class This (JSONPath ):
117186 """
118187 The JSONPath referring to the current datum. Concrete syntax is '@'.
119188 """
120189
121190 def find (self , data ):
122- return [DatumAtPath (data , path = This ())]
191+ if isinstance (data , DatumInContext ):
192+ return [data ]
193+ else :
194+ return [DatumInContext (data , path = This (), context = None )]
123195
124196 def update (self , data , val ):
125197 return val
126198
127199 def __str__ (self ):
128200 return '@'
129201
202+ def __repr__ (self ):
203+ return 'This()'
204+
205+ def __eq__ (self , other ):
206+ return isinstance (other , This )
207+
130208class Child (JSONPath ):
131209 """
132210 JSONPath that first matches the left, then the right.
@@ -137,17 +215,26 @@ def __init__(self, left, right):
137215 self .left = left
138216 self .right = right
139217
140- def find (self , data ):
141- return [submatch .in_context (subdata .path )
142- for subdata in self .left .find (data )
143- for submatch in self .right .find (subdata .value )]
218+ def find (self , datum ):
219+ """
220+ Extra special case: auto ids do not have children,
221+ so cut it off right now rather than auto id the auto id
222+ """
223+
224+ return [submatch
225+ for subdata in self .left .find (datum )
226+ if not isinstance (subdata , AutoIdForDatum )
227+ for submatch in self .right .find (subdata )]
144228
145229 def __eq__ (self , other ):
146230 return isinstance (other , Child ) and self .left == other .left and self .right == other .right
147231
148232 def __str__ (self ):
149233 return '%s.%s' % (self .left , self .right )
150234
235+ def __repr__ (self ):
236+ return '%s(%r, %r)' % (self .__class__ .__name__ , self .left , self .right )
237+
151238class Where (JSONPath ):
152239 """
153240 JSONPath that first matches the left, and then
@@ -181,41 +268,41 @@ def __init__(self, left, right):
181268 self .left = left
182269 self .right = right
183270
184- def find (self , data ):
271+ def find (self , datum ):
185272 # <left> .. <right> ==> <left> . (<right> | *..<right> | [*]..<right>)
186273 #
187274 # With with a wonky caveat that since Slice() has funky coercions
188275 # we cannot just delegate to that equivalence or we'll hit an
189276 # infinite loop. So right here we implement the coercion-free version.
190277
191278 # Get all left matches into a list
192- left_matches = self .left .find (data )
279+ left_matches = self .left .find (datum )
193280 if not isinstance (left_matches , list ):
194281 left_matches = [left_matches ]
195282
196- def match_recursively (data ):
197- right_matches = self .right .find (data )
283+ def match_recursively (datum ):
284+ right_matches = self .right .find (datum )
198285
199286 # Manually do the * or [*] to avoid coercion and recurse just the right-hand pattern
200- if isinstance (data , list ):
201- recursive_matches = [submatch . in_context ( Index ( i ))
202- for submatch in match_recursively (data [i ])
203- for i in xrange (0 , len (data ))]
287+ if isinstance (datum . value , list ):
288+ recursive_matches = [submatch
289+ for submatch in match_recursively (DatumInContext ( datum . value [i ], context = datum , path = Index ( i )) )
290+ for i in xrange (0 , len (datum . value ))]
204291
205- elif isinstance (data , dict ):
206- recursive_matches = [submatch . in_context ( Fields ( field ))
207- for field in data .keys ()
208- for submatch in match_recursively (data [field ])]
292+ elif isinstance (datum . value , dict ):
293+ recursive_matches = [submatch
294+ for field in datum . value .keys ()
295+ for submatch in match_recursively (DatumInContext ( datum . value [field ], context = datum , path = Fields ( field )) )]
209296
210297 else :
211298 recursive_matches = []
212299
213300 return right_matches + list (recursive_matches )
214301
215302 # TODO: repeatable iterator instead of list?
216- return [submatch . in_context ( left_match . path )
303+ return [submatch
217304 for left_match in left_matches
218- for submatch in match_recursively (left_match . value )]
305+ for submatch in match_recursively (left_match )]
219306
220307 def is_singular ():
221308 return False
@@ -279,34 +366,41 @@ class Fields(JSONPath):
279366 def __init__ (self , * fields ):
280367 self .fields = fields
281368
282- def get_datum (self , val , field ):
283- try :
284- field_value = val [ field ] # Do NOT use `val.get(field)` since that confuses None as a value and None due to `get`
285- return DatumAtPath ( value = field_value , path = Fields ( field ))
286- except ( TypeError , AttributeError , KeyError ): # This may not be all the interesting exceptions
287- if field == auto_id_field :
288- return AutoIdDatum ( auto_id = This () )
289- else :
369+ def get_field_datum (self , datum , field ):
370+ if field == auto_id_field :
371+ return AutoIdForDatum ( datum )
372+ else :
373+ try :
374+ field_value = datum . value [ field ] # Do NOT use `val.get(field)` since that confuses None as a value and None due to `get`
375+ return DatumInContext ( value = field_value , path = Fields ( field ), context = datum )
376+ except ( TypeError , KeyError , AttributeError ) :
290377 return None
291378
292- def find (self , data ):
293- if '*' in self .fields :
379+ def reified_fields (self , datum ):
380+ if '*' not in self .fields :
381+ return self .fields
382+ else :
294383 try :
295- return [DatumAtPath (data [field ], path = Fields (field )) for field in data .keys ()]
384+ fields = tuple (datum .value .keys ())
385+ return fields if auto_id_field is None else fields + (auto_id_field ,)
296386 except AttributeError :
297- return []
298- else :
299- result = [datum
300- for datum in [self .get_datum (data , field ) for field in self .fields ]
301- if datum is not None ]
387+ return ()
302388
303- return result
389+ def find (self , datum ):
390+ datum = DatumInContext .wrap (datum )
391+
392+ return [field_datum
393+ for field_datum in [self .get_field_datum (datum , field ) for field in self .reified_fields (datum )]
394+ if field_datum is not None ]
304395
305396 def __str__ (self ):
306397 return ',' .join (self .fields )
307398
399+ def __repr__ (self ):
400+ return '%s(%s)' % (self .__class__ .__name__ , ',' .join (map (repr , self .fields )))
401+
308402 def __eq__ (self , other ):
309- return isinstance (other , Fields ) and self .fields == other .fields
403+ return isinstance (other , Fields ) and tuple ( self .fields ) == tuple ( other .fields )
310404
311405
312406class Index (JSONPath ):
@@ -323,7 +417,7 @@ def __init__(self, index):
323417
324418 def find (self , data ):
325419 if len (data ) > self .index :
326- return [DatumAtPath (data [self .index ], path = self )]
420+ return [DatumInContext (data [self .index ], path = self )]
327421 else :
328422 return []
329423
@@ -373,9 +467,9 @@ def find(self, data):
373467 # Some iterators do not support slicing but we can still
374468 # at least work for '*'
375469 if self .start == None and self .end == None and self .step == None :
376- return [DatumAtPath (data [i ], Index (i )) for i in xrange (0 , len (data ))]
470+ return [DatumInContext (data [i ], Index (i )) for i in xrange (0 , len (data ))]
377471 else :
378- return [DatumAtPath (data [i ], Index (i )) for i in range (0 , len (data ))[self .start :self .end :self .step ]]
472+ return [DatumInContext (data [i ], Index (i )) for i in range (0 , len (data ))[self .start :self .end :self .step ]]
379473
380474 def __str__ (self ):
381475 if self .start == None and self .end == None and self .step == None :
0 commit comments