Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-jsonpointer for
openSUSE:Factory checked in at 2026-10-27 18:19:07
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jsonpointer (Old)
and /work/SRC/openSUSE:Factory/.python-jsonpointer.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-jsonpointer"
Tue Oct 27 18:19:07 2026 rev:24 rq:1392649 version:3.5
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-jsonpointer/python-jsonpointer.changes
2026-03-30 18:29:59.507809734 +0200
+++
/work/SRC/openSUSE:Factory/.python-jsonpointer.new.2252680/python-jsonpointer.changes
2026-10-27 18:20:18.129961843 +0100
@@ -1,0 +2,28 @@
+Mon Oct 26 08:08:01 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.5:
+ * Handle array-end pointers as CLI resolution errors
+ * Read CLI JSON and pointer files as UTF-8
+- update to 3.4:
+ * Add `create` parameter to `set_pointer` for auto-creating
+ missing paths
+- update to 3.3:
+ * Preserve control-flow exceptions when joining pointer parts
+ * Continue CLI batches after malformed JSON input
+- update to 3.2.1:
+ * Report test coverage to Coveralls from GitHub Actions
+ * Replace Travis CI link with GitHub Actions badge in README
+ * Preserve trailing member-name spaces in pointer files
+- update to 3.2.0:
+ * Run unittest discovery in the coverage target
+ * [BUGFIX] JsonPointer should not index into strings
+ * Preserve JsonPointer subclasses when joining
+ * fix: validate assignment target in JsonPointer.set()
+ * Normalize oversized array index conversion errors
+ * Add VERBOSE_EXCEPTIONS flag to suppress doc content in
+ JsonPointerException messages by @stefankoegl with @Copilot
+ in https://github.com/stefankoegl/python-json-pointer/pull/72
+ * Improve jsonpointer CLI: support positional pointer argument
+ * Add GitHub Actions workflow for PyPI publishing
+
+-------------------------------------------------------------------
Old:
----
jsonpointer-3.1.1.tar.gz
New:
----
jsonpointer-3.5.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-jsonpointer.spec ++++++
--- /var/tmp/diff_new_pack.7QrSNL/_old 2026-10-27 18:20:20.504061279 +0100
+++ /var/tmp/diff_new_pack.7QrSNL/_new 2026-10-27 18:20:20.505061321 +0100
@@ -24,7 +24,7 @@
%{?sle15_python_module_pythons}
Name: python-jsonpointer
-Version: 3.1.1
+Version: 3.5
Release: 0
Summary: Module to identify specific nodes in a JSON document
License: BSD-3-Clause
++++++ jsonpointer-3.1.1.tar.gz -> jsonpointer-3.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jsonpointer-3.1.1/PKG-INFO
new/jsonpointer-3.5/PKG-INFO
--- old/jsonpointer-3.1.1/PKG-INFO 2026-03-23 23:26:18.652129400 +0100
+++ new/jsonpointer-3.5/PKG-INFO 2026-10-24 22:41:46.350471700 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: jsonpointer
-Version: 3.1.1
+Version: 3.5
Summary: Identify specific nodes in a JSON document (RFC 6901)
Home-page: https://github.com/stefankoegl/python-json-pointer
Author: Stefan Kögl
@@ -42,7 +42,8 @@
[](https://pypi.python.org/pypi/jsonpointer/)
[](https://pypi.python.org/pypi/jsonpointer/)
-[](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master)
+[](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml)
+[](https://coveralls.io/github/stefankoegl/python-json-pointer?branch=master)
Resolve JSON Pointers in Python
@@ -56,5 +57,5 @@
* Repository: https://github.com/stefankoegl/python-json-pointer.git
* Documentation: https://python-json-pointer.readthedocs.org/
* PyPI: https://pypi.python.org/pypi/jsonpointer
-* Travis CI: https://travis-ci.org/stefankoegl/python-json-pointer
-* Coveralls: https://coveralls.io/r/stefankoegl/python-json-pointer
+* GitHub Actions: https://github.com/stefankoegl/python-json-pointer/actions
+* Coveralls: https://coveralls.io/github/stefankoegl/python-json-pointer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jsonpointer-3.1.1/README.md
new/jsonpointer-3.5/README.md
--- old/jsonpointer-3.1.1/README.md 2026-03-20 22:08:32.000000000 +0100
+++ new/jsonpointer-3.5/README.md 2026-10-24 22:41:28.025525600 +0200
@@ -3,7 +3,8 @@
[](https://pypi.python.org/pypi/jsonpointer/)
[](https://pypi.python.org/pypi/jsonpointer/)
-[](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master)
+[](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml)
+[](https://coveralls.io/github/stefankoegl/python-json-pointer?branch=master)
Resolve JSON Pointers in Python
@@ -17,5 +18,5 @@
* Repository: https://github.com/stefankoegl/python-json-pointer.git
* Documentation: https://python-json-pointer.readthedocs.org/
* PyPI: https://pypi.python.org/pypi/jsonpointer
-* Travis CI: https://travis-ci.org/stefankoegl/python-json-pointer
-* Coveralls: https://coveralls.io/r/stefankoegl/python-json-pointer
+* GitHub Actions: https://github.com/stefankoegl/python-json-pointer/actions
+* Coveralls: https://coveralls.io/github/stefankoegl/python-json-pointer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jsonpointer-3.1.1/bin/jsonpointer
new/jsonpointer-3.5/bin/jsonpointer
--- old/jsonpointer-3.1.1/bin/jsonpointer 2026-03-23 23:11:48.000000000
+0100
+++ new/jsonpointer-3.5/bin/jsonpointer 2026-10-24 22:41:28.025525600 +0200
@@ -10,56 +10,109 @@
parser = argparse.ArgumentParser(
description='Resolve a JSON pointer on JSON files')
-# Accept pointer as argument or as file
-ptr_group = parser.add_mutually_exclusive_group(required=True)
+# Accept pointer as option, as file or as first positional argument
+ptr_group = parser.add_mutually_exclusive_group()
-ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r'),
- nargs='?',
+ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r',
encoding='utf-8'),
help='File containing a JSON pointer expression')
-ptr_group.add_argument('POINTER', type=str, nargs='?',
+ptr_group.add_argument('-p', '--pointer', type=str,
help='A JSON pointer expression')
-parser.add_argument('FILE', type=argparse.FileType('r'), nargs='+',
+parser.add_argument('POINTER', type=str, nargs='?',
+ help='A JSON pointer expression (if neither -f nor -p '
+ 'is given)')
+
+parser.add_argument('FILE', type=str, nargs='+',
help='Files for which the pointer should be resolved')
parser.add_argument('--indent', type=int, default=None,
help='Indent output by n spaces')
+parser.add_argument('--continue-on-error', action='store_true',
+ help='Continue with remaining files after JSON or text
decoding errors')
parser.add_argument('-v', '--version', action='version',
version='%(prog)s ' + jsonpointer.__version__)
def main():
try:
- resolve_files()
+ ok = resolve_files()
except KeyboardInterrupt:
sys.exit(1)
+ if not ok:
+ sys.exit(1)
+
def parse_pointer(args):
- if args.POINTER:
+ """ Return the pointer and the list of files to resolve it on
+
+ If the pointer is given via -f or -p, a POINTER positional argument is
+ actually the first file. """
+ files = args.FILE
+
+ if args.pointer is not None or args.pointer_file is not None:
+ if args.POINTER is not None:
+ files = [args.POINTER] + files
+
+ if args.pointer is not None:
+ ptr = args.pointer
+ else:
+ # 2026-10-11: Trim file line endings without changing member names.
+ ptr = args.pointer_file.read().rstrip("\r\n")
+
+ elif args.POINTER is not None:
ptr = args.POINTER
- elif args.pointer_file:
- ptr = args.pointer_file.read().strip()
+
else:
- parser.print_usage()
- sys.exit(1)
+ parser.error('a JSON pointer is required (POINTER, -p or -f)')
- return ptr
+ # Catches e.g. `jsonpointer a.json b.json`, where the pointer was omitted
+ # and the first file would otherwise be used as the pointer
+ try:
+ jsonpointer.JsonPointer(ptr)
+ except jsonpointer.JsonPointerException as e:
+ parser.error('invalid JSON pointer %r: %s' % (ptr, e))
+
+ return ptr, files
def resolve_files():
- """ Resolve a JSON pointer on JSON files """
+ """ Resolve a JSON pointer on JSON files
+
+ Returns False if the pointer could not be resolved on at least one file,
+ or if --continue-on-error skipped a JSON or text decoding error. """
args = parser.parse_args()
- ptr = parse_pointer(args)
+ ptr, files = parse_pointer(args)
+ ok = True
+
+ for name in files:
+ try:
+ f = argparse.FileType('r', encoding='utf-8')(name)
+ except argparse.ArgumentTypeError as e:
+ parser.error(str(e))
+
+ try:
+ doc = json.load(f)
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
+ if not args.continue_on_error:
+ raise
+ print('Could not parse JSON in %r: %s' % (name, e),
+ file=sys.stderr)
+ ok = False
+ continue
- for f in args.FILE:
- doc = json.load(f)
try:
result = jsonpointer.resolve_pointer(doc, ptr)
+ if isinstance(result, jsonpointer.EndOfList):
+ raise jsonpointer.JsonPointerException(
+ "'-' does not reference an existing array element")
print(json.dumps(result, indent=args.indent))
except jsonpointer.JsonPointerException as e:
print('Could not resolve pointer: %s' % str(e), file=sys.stderr)
+ ok = False
+
+ return ok
if __name__ == "__main__":
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jsonpointer-3.1.1/jsonpointer.egg-info/PKG-INFO
new/jsonpointer-3.5/jsonpointer.egg-info/PKG-INFO
--- old/jsonpointer-3.1.1/jsonpointer.egg-info/PKG-INFO 2026-03-23
23:26:18.000000000 +0100
+++ new/jsonpointer-3.5/jsonpointer.egg-info/PKG-INFO 2026-10-24
22:41:46.333826500 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: jsonpointer
-Version: 3.1.1
+Version: 3.5
Summary: Identify specific nodes in a JSON document (RFC 6901)
Home-page: https://github.com/stefankoegl/python-json-pointer
Author: Stefan Kögl
@@ -42,7 +42,8 @@
[](https://pypi.python.org/pypi/jsonpointer/)
[](https://pypi.python.org/pypi/jsonpointer/)
-[](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master)
+[](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml)
+[](https://coveralls.io/github/stefankoegl/python-json-pointer?branch=master)
Resolve JSON Pointers in Python
@@ -56,5 +57,5 @@
* Repository: https://github.com/stefankoegl/python-json-pointer.git
* Documentation: https://python-json-pointer.readthedocs.org/
* PyPI: https://pypi.python.org/pypi/jsonpointer
-* Travis CI: https://travis-ci.org/stefankoegl/python-json-pointer
-* Coveralls: https://coveralls.io/r/stefankoegl/python-json-pointer
+* GitHub Actions: https://github.com/stefankoegl/python-json-pointer/actions
+* Coveralls: https://coveralls.io/github/stefankoegl/python-json-pointer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jsonpointer-3.1.1/jsonpointer.py
new/jsonpointer-3.5/jsonpointer.py
--- old/jsonpointer-3.1.1/jsonpointer.py 2026-03-23 23:24:11.000000000
+0100
+++ new/jsonpointer-3.5/jsonpointer.py 2026-10-24 22:41:28.026069000 +0200
@@ -32,7 +32,7 @@
# Will be parsed by setup.py to determine package metadata
__author__ = 'Stefan Kögl <[email protected]>'
-__version__ = '3.1.1'
+__version__ = '3.5'
__website__ = 'https://github.com/stefankoegl/python-json-pointer'
__license__ = 'Modified BSD License'
@@ -43,13 +43,22 @@
_nothing = object()
+# If True, exceptions include the doc content in the error message.
+# Set to False to suppress potentially large document content from exceptions.
+VERBOSE_EXCEPTIONS = True
-def set_pointer(doc, pointer, value, inplace=True):
+
+def set_pointer(doc, pointer, value, inplace=True, create=False):
"""Resolves a pointer against doc and sets the value of the target within
doc.
With inplace set to true, doc is modified as long as pointer is not the
root.
+ With create set to true, missing intermediate members are created as
+ dicts instead of raising a JsonPointerException. A missing intermediate
+ array element can be created by referencing the end of the array, either
+ with "-" or with an index equal to the length of the array.
+
>>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A
string' }}}
>>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \
@@ -64,10 +73,14 @@
>>> set_pointer(obj, '/foo/a%20b', 'x') == \
{'foo': {'a%20b': 'x' }}
True
+
+ >>> set_pointer({}, '/cat/name', 'whiskers', create=True) == \
+ {'cat': {'name': 'whiskers'}}
+ True
"""
pointer = JsonPointer(pointer)
- return pointer.set(doc, value, inplace)
+ return pointer.set(doc, value, inplace, create)
def resolve_pointer(doc, pointer, default=_nothing):
@@ -196,8 +209,11 @@
get = resolve
- def set(self, doc, value, inplace=True):
- """Resolve the pointer against the doc and replace the target with
value."""
+ def set(self, doc, value, inplace=True, create=False):
+ """Resolve the pointer against the doc and replace the target with
value.
+
+ With create set to true, missing intermediate members are created as
+ dicts (see set_pointer)."""
if len(self.parts) == 0:
if inplace:
@@ -207,22 +223,77 @@
if not inplace:
doc = copy.deepcopy(doc)
- (parent, part) = self.to_last(doc)
-
- if isinstance(parent, Sequence) and part == '-':
- parent.append(value)
+ if create:
+ parent = doc
+ for i, part in enumerate(self.parts[:-1]):
+ part = self._get_create_part(parent, part)
+ if self._is_missing(parent, part):
+ # Everything below this point is new, so build the
+ # missing structure detached and attach it in one step.
+ # This leaves doc unchanged if anything goes wrong.
+ child = value
+ for missing in reversed(self.parts[i + 1:]):
+ child = {missing: child}
+ self._set_child(parent, part, child)
+ return doc
+ parent = self.walk(parent, part)
+ part = self._get_create_part(parent, self.parts[-1])
else:
- parent[part] = value
+ (parent, part) = self.to_last(doc)
+ self._set_child(parent, part, value)
return doc
@classmethod
+ def _get_create_part(cls, doc, part):
+ """Like get_part, but maps an index just past the end of a sequence
+ to "-" so that it is appended"""
+
+ part = cls.get_part(doc, part)
+ if isinstance(doc, Sequence) and part == len(doc):
+ return '-'
+ return part
+
+ @staticmethod
+ def _is_missing(doc, part):
+ """Returns True if part does not exist in doc but could be added"""
+
+ if isinstance(doc, Mapping):
+ return part not in doc
+
+ if isinstance(doc, Sequence) and not isinstance(doc, str):
+ return part == '-'
+
+ return False
+
+ @staticmethod
+ def _set_child(parent, part, value):
+ """Sets parent[part] to value, appending for the end of a sequence"""
+
+ if isinstance(parent, str):
+ raise JsonPointerException("Cannot set value in a string")
+ elif isinstance(parent, Sequence) and part == '-':
+ try:
+ parent.append(value)
+ except AttributeError as e:
+ # immutable sequences, such as tuples, can't be appended to
+ raise JsonPointerException("Invalid assignment target: %s" %
(e,))
+ else:
+ try:
+ parent[part] = value
+ except (TypeError, IndexError) as e:
+ raise JsonPointerException("Invalid assignment target: %s" %
(e,))
+
+ @classmethod
def get_part(cls, doc, part):
"""Returns the next step in the correct type"""
if isinstance(doc, Mapping):
return part
+ elif isinstance(doc, str):
+ raise JsonPointerException("Cannot apply token '%s' to
non-container type %s" % (part, type(doc)))
+
elif isinstance(doc, Sequence):
if part == '-':
@@ -231,7 +302,10 @@
if not JsonPointer._RE_ARRAY_INDEX.fullmatch(str(part)):
raise JsonPointerException("'%s' is not a valid sequence
index" % part)
- return int(part)
+ try:
+ return int(part)
+ except ValueError as e:
+ raise JsonPointerException("Invalid sequence index: %s" % (e,))
elif hasattr(doc, '__getitem__'):
# Allow indexing via ducktyping
@@ -269,7 +343,10 @@
return doc[part]
except KeyError:
- raise JsonPointerException("member '%s' not found in %s" % (part,
doc))
+ if VERBOSE_EXCEPTIONS:
+ raise JsonPointerException("member '%s' not found in %s" %
(part, doc))
+ else:
+ raise JsonPointerException("member '%s' not found" % (part,))
def contains(self, ptr):
""" Returns True if self contains the given ptr """
@@ -288,8 +365,8 @@
else:
suffix_parts = suffix
try:
- return JsonPointer.from_parts(chain(self.parts, suffix_parts))
- except: # noqa E722
+ return self.__class__.from_parts(chain(self.parts, suffix_parts))
+ except Exception:
raise JsonPointerException("Invalid suffix")
def __truediv__(self, suffix):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jsonpointer-3.1.1/tests.py
new/jsonpointer-3.5/tests.py
--- old/jsonpointer-3.1.1/tests.py 2026-03-23 23:14:19.000000000 +0100
+++ new/jsonpointer-3.5/tests.py 2026-10-24 22:41:28.026069000 +0200
@@ -2,6 +2,11 @@
import copy
import doctest
+import json
+import os
+import subprocess
+import sys
+import tempfile
import unittest
import jsonpointer
@@ -192,6 +197,44 @@
ptr12e = self.ptr1 / ["a", "b"]
self.assertEqual(ptr12e.path, "/a/b/c/a/b")
+ def test_join_preserves_control_flow_exceptions(self):
+ for exception_type in (KeyboardInterrupt, SystemExit, GeneratorExit):
+ with self.subTest(exception_type=exception_type):
+ error = exception_type()
+
+ def parts():
+ yield 'valid'
+ raise error
+
+ with self.assertRaises(exception_type) as caught:
+ self.ptr1.join(parts())
+ self.assertIs(caught.exception, error)
+
+ def test_join_subclass(self):
+ class Pointer(JsonPointer):
+ pass
+
+ class OtherPointer(JsonPointer):
+ pass
+
+ ptr = Pointer("/a~1b")
+ suffixes = [JsonPointer("/m~0n"), Pointer("/m~0n"),
+ OtherPointer("/m~0n"), "/m~0n", ["m~n"]]
+ for suffix in suffixes:
+ with self.subTest(suffix=suffix):
+ joined = ptr.join(suffix)
+ self.assertIs(type(joined), Pointer)
+ self.assertEqual(joined.path, "/a~1b/m~0n")
+ self.assertEqual(ptr.path, "/a~1b")
+
+ def test_join_magic_subclass(self):
+ class Pointer(JsonPointer):
+ pass
+
+ ptr = Pointer("/a") / JsonPointer("/b") / ["c"]
+ self.assertIs(type(ptr), Pointer)
+ self.assertEqual(ptr.path, "/a/b/c")
+
class WrongInputTests(unittest.TestCase):
@@ -219,6 +262,54 @@
doc = [0, 1, 2]
self.assertRaises(JsonPointerException, resolve_pointer, doc, '/01')
+ def test_string_not_indexable(self):
+ doc = {"foo": "should-not-be-indexable"}
+ self.assertRaises(JsonPointerException, resolve_pointer, doc, "/foo/0")
+
+ ptr = JsonPointer("/foo/0")
+ self.assertRaises(JsonPointerException, ptr.resolve, doc)
+
+
[email protected](hasattr(sys, 'get_int_max_str_digits'),
+ 'Integer string conversion limits are not available')
+class LargeIndexTests(unittest.TestCase):
+
+ def setUp(self):
+ limit = sys.get_int_max_str_digits()
+ if not limit:
+ self.skipTest('Integer string conversion limit is disabled')
+ self.token = '1' * (limit + 1)
+ self.pointer = '/' + self.token
+
+ def test_resolve_large_index(self):
+ self.assertRaises(JsonPointerException, resolve_pointer,
+ [0], self.pointer)
+
+ def test_resolve_large_index_default(self):
+ default = object()
+ self.assertIs(resolve_pointer([0], self.pointer, default), default)
+ self.assertIs(resolve_pointer({'items': [0]},
+ '/items' + self.pointer, default),
+ default)
+
+ def test_to_last_large_index(self):
+ self.assertRaises(JsonPointerException,
+ JsonPointer(self.pointer).to_last, [0])
+
+ def test_set_large_index(self):
+ for inplace in (True, False):
+ with self.subTest(inplace=inplace):
+ doc = [0]
+ self.assertRaises(JsonPointerException, set_pointer,
+ doc, self.pointer, 1, inplace=inplace)
+ self.assertEqual(doc, [0])
+
+ def test_large_numeric_object_member(self):
+ doc = {self.token: 'old'}
+ self.assertEqual(resolve_pointer(doc, self.pointer), 'old')
+ set_pointer(doc, self.pointer, 'new')
+ self.assertEqual(doc, {self.token: 'new'})
+
class ToLastTests(unittest.TestCase):
@@ -296,6 +387,69 @@
self.assertRaises(JsonPointerException, set_pointer, doc, "", 9)
+ def test_set_create(self):
+ doc = {}
+ set_pointer(doc, "/cat/name", "whiskers", create=True)
+ self.assertEqual(doc, {"cat": {"name": "whiskers"}})
+
+ doc = {"cities": {"NewYork": "0"}}
+ set_pointer(doc, "/cities/LosAngeles/population", "4mln", create=True)
+ self.assertEqual(doc, {"cities": {
+ "NewYork": "0",
+ "LosAngeles": {"population": "4mln"},
+ }})
+
+ # existing members are kept
+ set_pointer(doc, "/cities/LosAngeles/area", 1302, create=True)
+ self.assertEqual(doc["cities"]["LosAngeles"],
+ {"population": "4mln", "area": 1302})
+
+ def test_set_create_not_inplace(self):
+ doc = {"a": {}}
+ newdoc = set_pointer(doc, "/a/b/c", 1, inplace=False, create=True)
+ self.assertEqual(newdoc, {"a": {"b": {"c": 1}}})
+ self.assertEqual(doc, {"a": {}})
+
+ def test_set_create_list(self):
+ doc = {"foo": []}
+ set_pointer(doc, "/foo/-/bar", 1, create=True)
+ self.assertEqual(doc, {"foo": [{"bar": 1}]})
+
+ set_pointer(doc, "/foo/1/bar", 2, create=True)
+ self.assertEqual(doc, {"foo": [{"bar": 1}, {"bar": 2}]})
+
+ set_pointer(doc, "/foo/0/baz", 3, create=True)
+ self.assertEqual(doc, {"foo": [{"bar": 1, "baz": 3}, {"bar": 2}]})
+
+ set_pointer(doc, "/foo/2", 4, create=True)
+ self.assertEqual(doc, {"foo": [{"bar": 1, "baz": 3}, {"bar": 2}, 4]})
+
+ def test_set_create_invalid(self):
+ doc = {"foo": [1], "s": "str"}
+ # indices past the end of a list can't be created
+ self.assertRaises(JsonPointerException, set_pointer, doc,
+ "/foo/5/bar", 1, create=True)
+ # nothing can be created inside a string
+ self.assertRaises(JsonPointerException, set_pointer, doc,
+ "/s/bar/baz", 1, create=True)
+ self.assertEqual(doc, {"foo": [1], "s": "str"})
+
+ def test_set_create_immutable_sequence(self):
+ doc = {"items": ()}
+ self.assertRaises(JsonPointerException, set_pointer, doc,
+ "/items/0/leaf", 7, create=True)
+ self.assertRaises(JsonPointerException, set_pointer, doc,
+ "/items/0", 7, create=True)
+ self.assertRaises(JsonPointerException, set_pointer, doc,
+ "/items/-", 7)
+ self.assertEqual(doc, {"items": ()})
+
+ def test_set_create_off_by_default(self):
+ doc = {}
+ self.assertRaises(JsonPointerException, set_pointer, doc,
+ "/cat/name", "whiskers")
+ self.assertEqual(doc, {})
+
class AltTypesTests(unittest.TestCase):
class Node(object):
@@ -394,6 +548,281 @@
self.assertRaises(JsonPointerException, resolve_pointer, doc,
'/root/1/2/3/4')
+class CommandLineTests(unittest.TestCase):
+ """ Tests the jsonpointer command line utility """
+
+ ROOT = os.path.dirname(os.path.abspath(__file__))
+ SCRIPT = os.path.join(ROOT, 'bin', 'jsonpointer')
+
+ A_OUT = '[1, 2, 3]\n'
+ B_OUT = '{"b": [1, 3, 4]}\n'
+
+ def setUp(self):
+ tmpdir = tempfile.TemporaryDirectory()
+ self.addCleanup(tmpdir.cleanup)
+ self.dir = tmpdir.name
+
+ self._write('a.json', '{ "a": [1, 2, 3] }')
+ self._write('b.json', '{ "a": {"b": [1, 3, 4]}, "b": 1 }')
+ self._write('ptr.txt', '/a\n')
+
+ def _write(self, name, content):
+ with open(os.path.join(self.dir, name), 'w', encoding='utf-8') as f:
+ f.write(content)
+
+ def _run(self, *args, utf8=True):
+ env = dict(os.environ, PYTHONPATH=self.ROOT, PYTHONUTF8='1')
+ if not utf8:
+ # Keep pipes in UTF-8 while exercising locale-dependent file I/O.
+ env.update(PYTHONUTF8='0', PYTHONIOENCODING='utf-8',
+ LC_ALL='C', PYTHONCOERCECLOCALE='0')
+ return subprocess.run([sys.executable, self.SCRIPT] + list(args),
+ cwd=self.dir, env=env, capture_output=True,
+ encoding='utf-8')
+
+ def test_utf8_json_file_without_utf8_mode(self):
+ self._write('unicode.json', '{"value": "🚀"}')
+ proc = self._run('/value', 'unicode.json', utf8=False)
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(json.loads(proc.stdout), '🚀')
+ self.assertEqual(proc.stderr, '')
+
+ def test_utf8_pointer_file_without_utf8_mode(self):
+ self._write('unicode.json', json.dumps({'ä': 'expected'}))
+ self._write('unicode.txt', '/ä\n')
+ proc = self._run('-f', 'unicode.txt', 'unicode.json', utf8=False)
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(json.loads(proc.stdout), 'expected')
+ self.assertEqual(proc.stderr, '')
+
+ def test_positional_pointer(self):
+ proc = self._run('/a', 'a.json', 'b.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
+
+ def test_positional_pointer_single_file(self):
+ proc = self._run('/a', 'a.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, self.A_OUT)
+
+ def test_pointer_file(self):
+ proc = self._run('-f', 'ptr.txt', 'a.json', 'b.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
+
+ # 2026-10-11: A pointer file's trailing spaces are part of its member name.
+ def test_pointer_file_preserves_member_whitespace(self):
+ self._write('space.json', '{"a ": "expected", "a": "wrong"}')
+ for newline in ('', '\n', '\r\n'):
+ with self.subTest(newline=newline):
+ with open(os.path.join(self.dir, 'space.txt'), 'w',
newline='') as f:
+ f.write('/a ' + newline)
+ proc = self._run('-f', 'space.txt', 'space.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, '"expected"\n')
+
+ def test_pointer_file_single_file(self):
+ proc = self._run('-f', 'ptr.txt', 'a.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, self.A_OUT)
+
+ def test_pointer_option(self):
+ proc = self._run('-p', '/a', 'a.json', 'b.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
+
+ def test_no_pointer(self):
+ proc = self._run('a.json')
+ self.assertEqual(proc.returncode, 2)
+ self.assertIn('a JSON pointer is required', proc.stderr)
+
+ def test_missing_pointer_multiple_files(self):
+ proc = self._run('a.json', 'b.json')
+ self.assertEqual(proc.returncode, 2)
+ self.assertIn("invalid JSON pointer 'a.json'", proc.stderr)
+ self.assertEqual(proc.stdout, '')
+
+ def test_invalid_pointer_file(self):
+ self._write('bad.txt', 'a\n')
+ proc = self._run('-f', 'bad.txt', 'a.json')
+ self.assertEqual(proc.returncode, 2)
+ self.assertIn("invalid JSON pointer 'a'", proc.stderr)
+
+ def test_empty_pointer(self):
+ proc = self._run('', 'a.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, '{"a": [1, 2, 3]}\n')
+
+ def test_unresolvable_pointer(self):
+ proc = self._run('/x', 'a.json')
+ self.assertEqual(proc.returncode, 1)
+ self.assertIn('Could not resolve pointer', proc.stderr)
+ self.assertEqual(proc.stdout, '')
+
+ def test_end_of_list_pointer(self):
+ for content, pointer in (('[]', '/-'), ('[1, 2]', '/-'),
+ ('{"a": []}', '/a/-')):
+ with self.subTest(content=content, pointer=pointer):
+ self._write('array.json', content)
+ proc = self._run(pointer, 'array.json')
+ self.assertEqual(proc.returncode, 1)
+ self.assertEqual(proc.stdout, '')
+ self.assertIn('Could not resolve pointer:', proc.stderr)
+ self.assertNotIn('Traceback', proc.stderr)
+
+ def test_end_of_list_pointer_in_one_file(self):
+ self._write('array.json', '{"a": []}')
+ self._write('member.json', '{"a": {"-": "value"}}')
+ for files in (('array.json', 'member.json'),
+ ('member.json', 'array.json')):
+ with self.subTest(files=files):
+ proc = self._run('/a/-', *files)
+ self.assertEqual(proc.returncode, 1)
+ self.assertEqual(proc.stdout, '"value"\n')
+ self.assertIn('Could not resolve pointer:', proc.stderr)
+ self.assertNotIn('Traceback', proc.stderr)
+
+ def test_hyphen_object_member(self):
+ self._write('member.json', '{"-": [1, 2]}')
+ proc = self._run('/-', 'member.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, '[1, 2]\n')
+ self.assertEqual(proc.stderr, '')
+
+ def test_unresolvable_pointer_in_one_file(self):
+ # /b resolves in b.json only; a.json is still reported but b.json
+ # is processed
+ proc = self._run('/b', 'a.json', 'b.json')
+ self.assertEqual(proc.returncode, 1)
+ self.assertIn('Could not resolve pointer', proc.stderr)
+ self.assertEqual(proc.stdout, '1\n')
+
+ def test_invalid_json(self):
+ for content in ('', '{"a": }', '{"a": 1}\n{"a": 2}'):
+ with self.subTest(content=content):
+ self._write('invalid.json', content)
+ proc = self._run('--continue-on-error', '/a', 'invalid.json')
+ self.assertEqual(proc.returncode, 1)
+ self.assertEqual(proc.stdout, '')
+ self.assertIn("Could not parse JSON in 'invalid.json':",
+ proc.stderr)
+ self.assertIn('line ', proc.stderr)
+ self.assertNotIn('Traceback', proc.stderr)
+
+ def test_invalid_json_in_one_file(self):
+ self._write('invalid.json', '{"a": }')
+ for index in range(3):
+ with self.subTest(index=index):
+ files = ['a.json', 'b.json']
+ files.insert(index, 'invalid.json')
+ proc = self._run('--continue-on-error', '/a', *files)
+ self.assertEqual(proc.returncode, 1)
+ self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
+ self.assertIn("Could not parse JSON in 'invalid.json':",
+ proc.stderr)
+ self.assertNotIn('Traceback', proc.stderr)
+
+ def test_invalid_json_and_unresolvable_pointer(self):
+ self._write('invalid.json', '{"a": }')
+ proc = self._run('--continue-on-error', '/b',
+ 'invalid.json', 'a.json', 'b.json')
+ self.assertEqual(proc.returncode, 1)
+ self.assertEqual(proc.stdout, '1\n')
+ self.assertIn("Could not parse JSON in 'invalid.json':", proc.stderr)
+ self.assertIn('Could not resolve pointer:', proc.stderr)
+ self.assertNotIn('Traceback', proc.stderr)
+
+ def test_invalid_json_stops_without_continue_on_error(self):
+ self._write('invalid.json', '{"a": }')
+ for index in range(3):
+ with self.subTest(index=index):
+ files = ['a.json', 'b.json']
+ files.insert(index, 'invalid.json')
+ proc = self._run('/a', *files)
+ self.assertEqual(proc.returncode, 1)
+ self.assertEqual(proc.stdout,
+ ''.join([self.A_OUT, self.B_OUT][:index]))
+ self.assertIn('JSONDecodeError', proc.stderr)
+
+ def test_invalid_encoding_in_one_file(self):
+ with open(os.path.join(self.dir, 'invalid.json'), 'wb') as f:
+ f.write(b'{"a":"\xff"}')
+ for index in range(3):
+ with self.subTest(index=index):
+ files = ['a.json', 'b.json']
+ files.insert(index, 'invalid.json')
+ proc = self._run('--continue-on-error', '/a', *files)
+ self.assertEqual(proc.returncode, 1)
+ self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
+ self.assertIn("Could not parse JSON in 'invalid.json':",
+ proc.stderr)
+ self.assertIn('decode', proc.stderr)
+ self.assertNotIn('Traceback', proc.stderr)
+
+ def test_invalid_encoding_stops_without_continue_on_error(self):
+ with open(os.path.join(self.dir, 'invalid.json'), 'wb') as f:
+ f.write(b'{"a":"\xff"}')
+ proc = self._run('/a', 'a.json', 'invalid.json', 'b.json')
+ self.assertEqual(proc.returncode, 1)
+ self.assertEqual(proc.stdout, self.A_OUT)
+ self.assertIn('UnicodeDecodeError', proc.stderr)
+
+ def test_continue_on_error_with_valid_files(self):
+ for pointer in (['/a'], ['-p', '/a'], ['-f', 'ptr.txt']):
+ with self.subTest(pointer=pointer):
+ proc = self._run('--continue-on-error', *pointer,
+ 'a.json', 'b.json')
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)
+ self.assertEqual(proc.stderr, '')
+
+ def test_continue_on_error_does_not_skip_missing_files(self):
+ proc = self._run('--continue-on-error', '/a',
+ 'a.json', 'missing.json', 'b.json')
+ self.assertEqual(proc.returncode, 2)
+ self.assertEqual(proc.stdout, self.A_OUT)
+ self.assertIn('missing.json', proc.stderr)
+
+ def test_pointer_file_requires_value(self):
+ proc = self._run('-f', '-p', '/a', 'a.json')
+ self.assertEqual(proc.returncode, 2)
+ self.assertIn('expected one argument', proc.stderr)
+
+ def test_pointer_and_pointer_file_exclusive(self):
+ proc = self._run('-p', '/a', '-f', 'ptr.txt', 'a.json')
+ self.assertEqual(proc.returncode, 2)
+ self.assertIn('not allowed with', proc.stderr)
+
+
+class VerboseExceptionsTests(unittest.TestCase):
+
+ def setUp(self):
+ # Save original value and ensure verbose mode is on for each test
+ self._original = jsonpointer.VERBOSE_EXCEPTIONS
+ jsonpointer.VERBOSE_EXCEPTIONS = True
+
+ def tearDown(self):
+ jsonpointer.VERBOSE_EXCEPTIONS = self._original
+
+ def test_verbose_exception_includes_doc(self):
+ doc = {'foo': 1}
+ try:
+ resolve_pointer(doc, '/bar')
+ self.fail('Expected JsonPointerException')
+ except JsonPointerException as e:
+ self.assertIn(repr(doc), str(e))
+
+ def test_non_verbose_exception_excludes_doc(self):
+ doc = {'foo': 1}
+ jsonpointer.VERBOSE_EXCEPTIONS = False
+ try:
+ resolve_pointer(doc, '/bar')
+ self.fail('Expected JsonPointerException')
+ except JsonPointerException as e:
+ self.assertNotIn(repr(doc), str(e))
+ self.assertIn('bar', str(e))
+
+
def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite(jsonpointer))
return tests