Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0794cf1

Browse files
committed
Make auto ids work take present ids into account in useful ways
1 parent a83f79d commit 0794cf1

2 files changed

Lines changed: 262 additions & 75 deletions

File tree

jsonpath_rw/jsonpath.py

Lines changed: 166 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -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

99158
class 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+
116185
class 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+
130208
class 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+
151238
class 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

312406
class 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

Comments
 (0)