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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonpointer
3
- Version: 3.2.1
3
+ Version: 3.4
4
4
  Summary: Identify specific nodes in a JSON document (RFC 6901)
5
5
  Home-page: https://github.com/stefankoegl/python-json-pointer
6
6
  Author: Stefan Kögl
@@ -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
- file. """
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
- doc = json.load(f)
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonpointer
3
- Version: 3.2.1
3
+ Version: 3.4
4
4
  Summary: Identify specific nodes in a JSON document (RFC 6901)
5
5
  Home-page: https://github.com/stefankoegl/python-json-pointer
6
6
  Author: Stefan Kögl
@@ -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.2.1'
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
- (parent, part) = self.to_last(doc)
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
- parent.append(value)
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: # noqa E722
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