From ead884aeae7c721904797f8402dba263762ac869 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 12:52:33 +0000 Subject: [PATCH] Add create option to set_pointer to create missing members (#41) With create=True, set_pointer and JsonPointer.set create missing intermediate members as dicts instead of raising JsonPointerException. Missing list elements can be created by referencing the end of a list with "-" or an index equal to its length. The missing structure is built detached and attached in one step, so doc is left unchanged if the pointer can't be applied. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGUKSut5p4NSKEm3mD5Ft3 --- doc/tutorial.rst | 16 +++++++++++ jsonpointer.py | 69 +++++++++++++++++++++++++++++++++++++++++++----- tests.py | 53 +++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 7 deletions(-) diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 9d7b21d..5a58c4f 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -55,6 +55,22 @@ to create a copy and modify the copy instead: >>> obj {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 44}]}} +By default, setting a value below a member that does not exist raises a +``JsonPointerException``. Pass ``create=True`` to create missing members as +dicts instead. Missing list elements can be created by referencing the end of +the list, either with ``-`` or with an index equal to the length of the list: + + >>> from jsonpointer import set_pointer + >>> obj = {} + + >>> set_pointer(obj, '/cat/name', 'whiskers', create=True) + {'cat': {'name': 'whiskers'}} + + >>> obj = {'cats': []} + + >>> set_pointer(obj, '/cats/-/name', 'whiskers', create=True) + {'cats': [{'name': 'whiskers'}]} + The ``JsonPointer`` class wraps a (string) path and can be used to access the same path on several objects. diff --git a/jsonpointer.py b/jsonpointer.py index 40271fb..2ab30b9 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -48,12 +48,17 @@ 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) == \ @@ -68,10 +73,14 @@ def set_pointer(doc, pointer, value, inplace=True): >>> 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): @@ -200,8 +209,11 @@ def resolve(self, doc, default=_nothing): 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: @@ -211,7 +223,52 @@ def set(self, doc, value, inplace=True): if not inplace: doc = copy.deepcopy(doc) - (parent, part) = self.to_last(doc) + 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) = 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") @@ -223,8 +280,6 @@ def set(self, doc, value, inplace=True): except (TypeError, IndexError) as e: raise JsonPointerException("Invalid assignment target: %s" % (e,)) - return doc - @classmethod def get_part(cls, doc, part): """Returns the next step in the correct type""" diff --git a/tests.py b/tests.py index a330ab4..220dfec 100755 --- a/tests.py +++ b/tests.py @@ -373,6 +373,59 @@ def test_set(self): 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_off_by_default(self): + doc = {} + self.assertRaises(JsonPointerException, set_pointer, doc, + "/cat/name", "whiskers") + self.assertEqual(doc, {}) + class AltTypesTests(unittest.TestCase): class Node(object):