Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
69 changes: 62 additions & 7 deletions jsonpointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) == \
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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")
Expand All @@ -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"""
Expand Down
53 changes: 53 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading