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 @@
 
 [![PyPI 
version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/)
 [![Supported Python 
versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/)
-[![Coverage 
Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.svg?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master)
+[![Tests](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml)
+[![Coverage 
Status](https://coveralls.io/repos/github/stefankoegl/python-json-pointer/badge.svg?branch=master)](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 @@
 
 [![PyPI 
version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/)
 [![Supported Python 
versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/)
-[![Coverage 
Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.svg?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master)
+[![Tests](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml)
+[![Coverage 
Status](https://coveralls.io/repos/github/stefankoegl/python-json-pointer/badge.svg?branch=master)](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 @@
 
 [![PyPI 
version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/)
 [![Supported Python 
versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/)
-[![Coverage 
Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.svg?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master)
+[![Tests](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/stefankoegl/python-json-pointer/actions/workflows/test.yaml)
+[![Coverage 
Status](https://coveralls.io/repos/github/stefankoegl/python-json-pointer/badge.svg?branch=master)](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

Reply via email to