jsonpointer 3.2.1__tar.gz → 3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jsonpointer-3.2.1/jsonpointer.egg-info → jsonpointer-3.4}/PKG-INFO +1 -1
- {jsonpointer-3.2.1 → jsonpointer-3.4}/bin/jsonpointer +14 -3
- {jsonpointer-3.2.1 → jsonpointer-3.4/jsonpointer.egg-info}/PKG-INFO +1 -1
- {jsonpointer-3.2.1 → jsonpointer-3.4}/jsonpointer.py +69 -10
- {jsonpointer-3.2.1 → jsonpointer-3.4}/tests.py +163 -1
- {jsonpointer-3.2.1 → jsonpointer-3.4}/AUTHORS +0 -0
- {jsonpointer-3.2.1 → jsonpointer-3.4}/LICENSE.txt +0 -0
- {jsonpointer-3.2.1 → jsonpointer-3.4}/MANIFEST.in +0 -0
- {jsonpointer-3.2.1 → jsonpointer-3.4}/README.md +0 -0
- {jsonpointer-3.2.1 → jsonpointer-3.4}/jsonpointer.egg-info/SOURCES.txt +0 -0
- {jsonpointer-3.2.1 → jsonpointer-3.4}/jsonpointer.egg-info/dependency_links.txt +0 -0
- {jsonpointer-3.2.1 → jsonpointer-3.4}/jsonpointer.egg-info/top_level.txt +0 -0
- {jsonpointer-3.2.1 → jsonpointer-3.4}/setup.cfg +0 -0
- {jsonpointer-3.2.1 → jsonpointer-3.4}/setup.py +0 -0
|
@@ -27,6 +27,8 @@ parser.add_argument('FILE', type=str, nargs='+',
|
|
|
27
27
|
help='Files for which the pointer should be resolved')
|
|
28
28
|
parser.add_argument('--indent', type=int, default=None,
|
|
29
29
|
help='Indent output by n spaces')
|
|
30
|
+
parser.add_argument('--continue-on-error', action='store_true',
|
|
31
|
+
help='Continue with remaining files after JSON or text decoding errors')
|
|
30
32
|
parser.add_argument('-v', '--version', action='version',
|
|
31
33
|
version='%(prog)s ' + jsonpointer.__version__)
|
|
32
34
|
|
|
@@ -77,8 +79,8 @@ def parse_pointer(args):
|
|
|
77
79
|
def resolve_files():
|
|
78
80
|
""" Resolve a JSON pointer on JSON files
|
|
79
81
|
|
|
80
|
-
Returns False if the pointer could not be resolved on at least one
|
|
81
|
-
|
|
82
|
+
Returns False if the pointer could not be resolved on at least one file,
|
|
83
|
+
or if --continue-on-error skipped a JSON or text decoding error. """
|
|
82
84
|
args = parser.parse_args()
|
|
83
85
|
|
|
84
86
|
ptr, files = parse_pointer(args)
|
|
@@ -90,7 +92,16 @@ def resolve_files():
|
|
|
90
92
|
except argparse.ArgumentTypeError as e:
|
|
91
93
|
parser.error(str(e))
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
try:
|
|
96
|
+
doc = json.load(f)
|
|
97
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
98
|
+
if not args.continue_on_error:
|
|
99
|
+
raise
|
|
100
|
+
print('Could not parse JSON in %r: %s' % (name, e),
|
|
101
|
+
file=sys.stderr)
|
|
102
|
+
ok = False
|
|
103
|
+
continue
|
|
104
|
+
|
|
94
105
|
try:
|
|
95
106
|
result = jsonpointer.resolve_pointer(doc, ptr)
|
|
96
107
|
print(json.dumps(result, indent=args.indent))
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
# Will be parsed by setup.py to determine package metadata
|
|
34
34
|
__author__ = 'Stefan Kögl <stefan@skoegl.net>'
|
|
35
|
-
__version__ = '3.
|
|
35
|
+
__version__ = '3.4'
|
|
36
36
|
__website__ = 'https://github.com/stefankoegl/python-json-pointer'
|
|
37
37
|
__license__ = 'Modified BSD License'
|
|
38
38
|
|
|
@@ -48,12 +48,17 @@ _nothing = object()
|
|
|
48
48
|
VERBOSE_EXCEPTIONS = True
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def set_pointer(doc, pointer, value, inplace=True):
|
|
51
|
+
def set_pointer(doc, pointer, value, inplace=True, create=False):
|
|
52
52
|
"""Resolves a pointer against doc and sets the value of the target within doc.
|
|
53
53
|
|
|
54
54
|
With inplace set to true, doc is modified as long as pointer is not the
|
|
55
55
|
root.
|
|
56
56
|
|
|
57
|
+
With create set to true, missing intermediate members are created as
|
|
58
|
+
dicts instead of raising a JsonPointerException. A missing intermediate
|
|
59
|
+
array element can be created by referencing the end of the array, either
|
|
60
|
+
with "-" or with an index equal to the length of the array.
|
|
61
|
+
|
|
57
62
|
>>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}}
|
|
58
63
|
|
|
59
64
|
>>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \
|
|
@@ -68,10 +73,14 @@ def set_pointer(doc, pointer, value, inplace=True):
|
|
|
68
73
|
>>> set_pointer(obj, '/foo/a%20b', 'x') == \
|
|
69
74
|
{'foo': {'a%20b': 'x' }}
|
|
70
75
|
True
|
|
76
|
+
|
|
77
|
+
>>> set_pointer({}, '/cat/name', 'whiskers', create=True) == \
|
|
78
|
+
{'cat': {'name': 'whiskers'}}
|
|
79
|
+
True
|
|
71
80
|
"""
|
|
72
81
|
|
|
73
82
|
pointer = JsonPointer(pointer)
|
|
74
|
-
return pointer.set(doc, value, inplace)
|
|
83
|
+
return pointer.set(doc, value, inplace, create)
|
|
75
84
|
|
|
76
85
|
|
|
77
86
|
def resolve_pointer(doc, pointer, default=_nothing):
|
|
@@ -200,8 +209,11 @@ class JsonPointer:
|
|
|
200
209
|
|
|
201
210
|
get = resolve
|
|
202
211
|
|
|
203
|
-
def set(self, doc, value, inplace=True):
|
|
204
|
-
"""Resolve the pointer against the doc and replace the target with value.
|
|
212
|
+
def set(self, doc, value, inplace=True, create=False):
|
|
213
|
+
"""Resolve the pointer against the doc and replace the target with value.
|
|
214
|
+
|
|
215
|
+
With create set to true, missing intermediate members are created as
|
|
216
|
+
dicts (see set_pointer)."""
|
|
205
217
|
|
|
206
218
|
if len(self.parts) == 0:
|
|
207
219
|
if inplace:
|
|
@@ -211,20 +223,67 @@ class JsonPointer:
|
|
|
211
223
|
if not inplace:
|
|
212
224
|
doc = copy.deepcopy(doc)
|
|
213
225
|
|
|
214
|
-
|
|
226
|
+
if create:
|
|
227
|
+
parent = doc
|
|
228
|
+
for i, part in enumerate(self.parts[:-1]):
|
|
229
|
+
part = self._get_create_part(parent, part)
|
|
230
|
+
if self._is_missing(parent, part):
|
|
231
|
+
# Everything below this point is new, so build the
|
|
232
|
+
# missing structure detached and attach it in one step.
|
|
233
|
+
# This leaves doc unchanged if anything goes wrong.
|
|
234
|
+
child = value
|
|
235
|
+
for missing in reversed(self.parts[i + 1:]):
|
|
236
|
+
child = {missing: child}
|
|
237
|
+
self._set_child(parent, part, child)
|
|
238
|
+
return doc
|
|
239
|
+
parent = self.walk(parent, part)
|
|
240
|
+
part = self._get_create_part(parent, self.parts[-1])
|
|
241
|
+
else:
|
|
242
|
+
(parent, part) = self.to_last(doc)
|
|
243
|
+
|
|
244
|
+
self._set_child(parent, part, value)
|
|
245
|
+
return doc
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def _get_create_part(cls, doc, part):
|
|
249
|
+
"""Like get_part, but maps an index just past the end of a sequence
|
|
250
|
+
to "-" so that it is appended"""
|
|
251
|
+
|
|
252
|
+
part = cls.get_part(doc, part)
|
|
253
|
+
if isinstance(doc, Sequence) and part == len(doc):
|
|
254
|
+
return '-'
|
|
255
|
+
return part
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _is_missing(doc, part):
|
|
259
|
+
"""Returns True if part does not exist in doc but could be added"""
|
|
260
|
+
|
|
261
|
+
if isinstance(doc, Mapping):
|
|
262
|
+
return part not in doc
|
|
263
|
+
|
|
264
|
+
if isinstance(doc, Sequence) and not isinstance(doc, str):
|
|
265
|
+
return part == '-'
|
|
266
|
+
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def _set_child(parent, part, value):
|
|
271
|
+
"""Sets parent[part] to value, appending for the end of a sequence"""
|
|
215
272
|
|
|
216
273
|
if isinstance(parent, str):
|
|
217
274
|
raise JsonPointerException("Cannot set value in a string")
|
|
218
275
|
elif isinstance(parent, Sequence) and part == '-':
|
|
219
|
-
|
|
276
|
+
try:
|
|
277
|
+
parent.append(value)
|
|
278
|
+
except AttributeError as e:
|
|
279
|
+
# immutable sequences, such as tuples, can't be appended to
|
|
280
|
+
raise JsonPointerException("Invalid assignment target: %s" % (e,))
|
|
220
281
|
else:
|
|
221
282
|
try:
|
|
222
283
|
parent[part] = value
|
|
223
284
|
except (TypeError, IndexError) as e:
|
|
224
285
|
raise JsonPointerException("Invalid assignment target: %s" % (e,))
|
|
225
286
|
|
|
226
|
-
return doc
|
|
227
|
-
|
|
228
287
|
@classmethod
|
|
229
288
|
def get_part(cls, doc, part):
|
|
230
289
|
"""Returns the next step in the correct type"""
|
|
@@ -307,7 +366,7 @@ class JsonPointer:
|
|
|
307
366
|
suffix_parts = suffix
|
|
308
367
|
try:
|
|
309
368
|
return self.__class__.from_parts(chain(self.parts, suffix_parts))
|
|
310
|
-
except:
|
|
369
|
+
except Exception:
|
|
311
370
|
raise JsonPointerException("Invalid suffix")
|
|
312
371
|
|
|
313
372
|
def __truediv__(self, suffix):
|
|
@@ -196,6 +196,19 @@ class ComparisonTests(unittest.TestCase):
|
|
|
196
196
|
ptr12e = self.ptr1 / ["a", "b"]
|
|
197
197
|
self.assertEqual(ptr12e.path, "/a/b/c/a/b")
|
|
198
198
|
|
|
199
|
+
def test_join_preserves_control_flow_exceptions(self):
|
|
200
|
+
for exception_type in (KeyboardInterrupt, SystemExit, GeneratorExit):
|
|
201
|
+
with self.subTest(exception_type=exception_type):
|
|
202
|
+
error = exception_type()
|
|
203
|
+
|
|
204
|
+
def parts():
|
|
205
|
+
yield 'valid'
|
|
206
|
+
raise error
|
|
207
|
+
|
|
208
|
+
with self.assertRaises(exception_type) as caught:
|
|
209
|
+
self.ptr1.join(parts())
|
|
210
|
+
self.assertIs(caught.exception, error)
|
|
211
|
+
|
|
199
212
|
def test_join_subclass(self):
|
|
200
213
|
class Pointer(JsonPointer):
|
|
201
214
|
pass
|
|
@@ -373,6 +386,69 @@ class SetTests(unittest.TestCase):
|
|
|
373
386
|
|
|
374
387
|
self.assertRaises(JsonPointerException, set_pointer, doc, "", 9)
|
|
375
388
|
|
|
389
|
+
def test_set_create(self):
|
|
390
|
+
doc = {}
|
|
391
|
+
set_pointer(doc, "/cat/name", "whiskers", create=True)
|
|
392
|
+
self.assertEqual(doc, {"cat": {"name": "whiskers"}})
|
|
393
|
+
|
|
394
|
+
doc = {"cities": {"NewYork": "0"}}
|
|
395
|
+
set_pointer(doc, "/cities/LosAngeles/population", "4mln", create=True)
|
|
396
|
+
self.assertEqual(doc, {"cities": {
|
|
397
|
+
"NewYork": "0",
|
|
398
|
+
"LosAngeles": {"population": "4mln"},
|
|
399
|
+
}})
|
|
400
|
+
|
|
401
|
+
# existing members are kept
|
|
402
|
+
set_pointer(doc, "/cities/LosAngeles/area", 1302, create=True)
|
|
403
|
+
self.assertEqual(doc["cities"]["LosAngeles"],
|
|
404
|
+
{"population": "4mln", "area": 1302})
|
|
405
|
+
|
|
406
|
+
def test_set_create_not_inplace(self):
|
|
407
|
+
doc = {"a": {}}
|
|
408
|
+
newdoc = set_pointer(doc, "/a/b/c", 1, inplace=False, create=True)
|
|
409
|
+
self.assertEqual(newdoc, {"a": {"b": {"c": 1}}})
|
|
410
|
+
self.assertEqual(doc, {"a": {}})
|
|
411
|
+
|
|
412
|
+
def test_set_create_list(self):
|
|
413
|
+
doc = {"foo": []}
|
|
414
|
+
set_pointer(doc, "/foo/-/bar", 1, create=True)
|
|
415
|
+
self.assertEqual(doc, {"foo": [{"bar": 1}]})
|
|
416
|
+
|
|
417
|
+
set_pointer(doc, "/foo/1/bar", 2, create=True)
|
|
418
|
+
self.assertEqual(doc, {"foo": [{"bar": 1}, {"bar": 2}]})
|
|
419
|
+
|
|
420
|
+
set_pointer(doc, "/foo/0/baz", 3, create=True)
|
|
421
|
+
self.assertEqual(doc, {"foo": [{"bar": 1, "baz": 3}, {"bar": 2}]})
|
|
422
|
+
|
|
423
|
+
set_pointer(doc, "/foo/2", 4, create=True)
|
|
424
|
+
self.assertEqual(doc, {"foo": [{"bar": 1, "baz": 3}, {"bar": 2}, 4]})
|
|
425
|
+
|
|
426
|
+
def test_set_create_invalid(self):
|
|
427
|
+
doc = {"foo": [1], "s": "str"}
|
|
428
|
+
# indices past the end of a list can't be created
|
|
429
|
+
self.assertRaises(JsonPointerException, set_pointer, doc,
|
|
430
|
+
"/foo/5/bar", 1, create=True)
|
|
431
|
+
# nothing can be created inside a string
|
|
432
|
+
self.assertRaises(JsonPointerException, set_pointer, doc,
|
|
433
|
+
"/s/bar/baz", 1, create=True)
|
|
434
|
+
self.assertEqual(doc, {"foo": [1], "s": "str"})
|
|
435
|
+
|
|
436
|
+
def test_set_create_immutable_sequence(self):
|
|
437
|
+
doc = {"items": ()}
|
|
438
|
+
self.assertRaises(JsonPointerException, set_pointer, doc,
|
|
439
|
+
"/items/0/leaf", 7, create=True)
|
|
440
|
+
self.assertRaises(JsonPointerException, set_pointer, doc,
|
|
441
|
+
"/items/0", 7, create=True)
|
|
442
|
+
self.assertRaises(JsonPointerException, set_pointer, doc,
|
|
443
|
+
"/items/-", 7)
|
|
444
|
+
self.assertEqual(doc, {"items": ()})
|
|
445
|
+
|
|
446
|
+
def test_set_create_off_by_default(self):
|
|
447
|
+
doc = {}
|
|
448
|
+
self.assertRaises(JsonPointerException, set_pointer, doc,
|
|
449
|
+
"/cat/name", "whiskers")
|
|
450
|
+
self.assertEqual(doc, {})
|
|
451
|
+
|
|
376
452
|
|
|
377
453
|
class AltTypesTests(unittest.TestCase):
|
|
378
454
|
class Node(object):
|
|
@@ -494,7 +570,7 @@ class CommandLineTests(unittest.TestCase):
|
|
|
494
570
|
f.write(content)
|
|
495
571
|
|
|
496
572
|
def _run(self, *args):
|
|
497
|
-
env = dict(os.environ, PYTHONPATH=self.ROOT)
|
|
573
|
+
env = dict(os.environ, PYTHONPATH=self.ROOT, PYTHONUTF8='1')
|
|
498
574
|
return subprocess.run([sys.executable, self.SCRIPT] + list(args),
|
|
499
575
|
cwd=self.dir, env=env, capture_output=True,
|
|
500
576
|
text=True)
|
|
@@ -571,6 +647,92 @@ class CommandLineTests(unittest.TestCase):
|
|
|
571
647
|
self.assertIn('Could not resolve pointer', proc.stderr)
|
|
572
648
|
self.assertEqual(proc.stdout, '1\n')
|
|
573
649
|
|
|
650
|
+
def test_invalid_json(self):
|
|
651
|
+
for content in ('', '{"a": }', '{"a": 1}\n{"a": 2}'):
|
|
652
|
+
with self.subTest(content=content):
|
|
653
|
+
self._write('invalid.json', content)
|
|
654
|
+
proc = self._run('--continue-on-error', '/a', 'invalid.json')
|
|
655
|
+
self.assertEqual(proc.returncode, 1)
|
|
656
|
+
self.assertEqual(proc.stdout, '')
|
|
657
|
+
self.assertIn("Could not parse JSON in 'invalid.json':",
|
|
658
|
+
proc.stderr)
|
|
659
|
+
self.assertIn('line ', proc.stderr)
|
|
660
|
+
self.assertNotIn('Traceback', proc.stderr)
|
|
661
|
+
|
|
662
|
+
def test_invalid_json_in_one_file(self):
|
|
663
|
+
self._write('invalid.json', '{"a": }')
|
|
664
|
+
for index in range(3):
|
|
665
|
+
with self.subTest(index=index):
|
|
666
|
+
files = ['a.json', 'b.json']
|
|
667
|
+
files.insert(index, 'invalid.json')
|
|
668
|
+
proc = self._run('--continue-on-error', '/a', *files)
|
|
669
|
+
self.assertEqual(proc.returncode, 1)
|
|
670
|
+
self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
|
|
671
|
+
self.assertIn("Could not parse JSON in 'invalid.json':",
|
|
672
|
+
proc.stderr)
|
|
673
|
+
self.assertNotIn('Traceback', proc.stderr)
|
|
674
|
+
|
|
675
|
+
def test_invalid_json_and_unresolvable_pointer(self):
|
|
676
|
+
self._write('invalid.json', '{"a": }')
|
|
677
|
+
proc = self._run('--continue-on-error', '/b',
|
|
678
|
+
'invalid.json', 'a.json', 'b.json')
|
|
679
|
+
self.assertEqual(proc.returncode, 1)
|
|
680
|
+
self.assertEqual(proc.stdout, '1\n')
|
|
681
|
+
self.assertIn("Could not parse JSON in 'invalid.json':", proc.stderr)
|
|
682
|
+
self.assertIn('Could not resolve pointer:', proc.stderr)
|
|
683
|
+
self.assertNotIn('Traceback', proc.stderr)
|
|
684
|
+
|
|
685
|
+
def test_invalid_json_stops_without_continue_on_error(self):
|
|
686
|
+
self._write('invalid.json', '{"a": }')
|
|
687
|
+
for index in range(3):
|
|
688
|
+
with self.subTest(index=index):
|
|
689
|
+
files = ['a.json', 'b.json']
|
|
690
|
+
files.insert(index, 'invalid.json')
|
|
691
|
+
proc = self._run('/a', *files)
|
|
692
|
+
self.assertEqual(proc.returncode, 1)
|
|
693
|
+
self.assertEqual(proc.stdout,
|
|
694
|
+
''.join([self.A_OUT, self.B_OUT][:index]))
|
|
695
|
+
self.assertIn('JSONDecodeError', proc.stderr)
|
|
696
|
+
|
|
697
|
+
def test_invalid_encoding_in_one_file(self):
|
|
698
|
+
with open(os.path.join(self.dir, 'invalid.json'), 'wb') as f:
|
|
699
|
+
f.write(b'{"a":"\xff"}')
|
|
700
|
+
for index in range(3):
|
|
701
|
+
with self.subTest(index=index):
|
|
702
|
+
files = ['a.json', 'b.json']
|
|
703
|
+
files.insert(index, 'invalid.json')
|
|
704
|
+
proc = self._run('--continue-on-error', '/a', *files)
|
|
705
|
+
self.assertEqual(proc.returncode, 1)
|
|
706
|
+
self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
|
|
707
|
+
self.assertIn("Could not parse JSON in 'invalid.json':",
|
|
708
|
+
proc.stderr)
|
|
709
|
+
self.assertIn('decode', proc.stderr)
|
|
710
|
+
self.assertNotIn('Traceback', proc.stderr)
|
|
711
|
+
|
|
712
|
+
def test_invalid_encoding_stops_without_continue_on_error(self):
|
|
713
|
+
with open(os.path.join(self.dir, 'invalid.json'), 'wb') as f:
|
|
714
|
+
f.write(b'{"a":"\xff"}')
|
|
715
|
+
proc = self._run('/a', 'a.json', 'invalid.json', 'b.json')
|
|
716
|
+
self.assertEqual(proc.returncode, 1)
|
|
717
|
+
self.assertEqual(proc.stdout, self.A_OUT)
|
|
718
|
+
self.assertIn('UnicodeDecodeError', proc.stderr)
|
|
719
|
+
|
|
720
|
+
def test_continue_on_error_with_valid_files(self):
|
|
721
|
+
for pointer in (['/a'], ['-p', '/a'], ['-f', 'ptr.txt']):
|
|
722
|
+
with self.subTest(pointer=pointer):
|
|
723
|
+
proc = self._run('--continue-on-error', *pointer,
|
|
724
|
+
'a.json', 'b.json')
|
|
725
|
+
self.assertEqual(proc.returncode, 0, proc.stderr)
|
|
726
|
+
self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
|
|
727
|
+
self.assertEqual(proc.stderr, '')
|
|
728
|
+
|
|
729
|
+
def test_continue_on_error_does_not_skip_missing_files(self):
|
|
730
|
+
proc = self._run('--continue-on-error', '/a',
|
|
731
|
+
'a.json', 'missing.json', 'b.json')
|
|
732
|
+
self.assertEqual(proc.returncode, 2)
|
|
733
|
+
self.assertEqual(proc.stdout, self.A_OUT)
|
|
734
|
+
self.assertIn('missing.json', proc.stderr)
|
|
735
|
+
|
|
574
736
|
def test_pointer_file_requires_value(self):
|
|
575
737
|
proc = self._run('-f', '-p', '/a', 'a.json')
|
|
576
738
|
self.assertEqual(proc.returncode, 2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|