Support for set_pointer and indexing arbitrary objects via __getitem__/__setitem__
Christopher J. White authored 10 years ago
Stefan Kögl committed 10 years ago
6 | 6 | |
7 | 7 | .. code-block:: python |
8 | 8 | |
9 | >>> import jsonpointer | |
9 | >>> from jsonpointer import resolve_pointer | |
10 | 10 | >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} |
11 | 11 | |
12 | 12 | >>> resolve_pointer(obj, '') == obj |
28 | 28 | True |
29 | 29 | |
30 | 30 | |
31 | The ``set_pointer`` method allows modifying a portion of an object using | |
32 | JSON pointer notation: | |
33 | ||
34 | .. code-block:: python | |
35 | ||
36 | >>> from jsonpointer import set_pointer | |
37 | >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} | |
38 | ||
39 | >>> set_pointer(obj, '/foo/anArray/0/prop', 55) | |
40 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} | |
41 | ||
42 | >>> obj | |
43 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} | |
44 | ||
45 | By default ``set_pointer`` modifies the original object. Pass ``inplace=False`` | |
46 | to create a copy and modify the copy instead: | |
47 | ||
48 | >>> from jsonpointer import set_pointer | |
49 | >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} | |
50 | ||
51 | >>> set_pointer(obj, '/foo/anArray/0/prop', 55, inplace=False) | |
52 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} | |
53 | ||
54 | >>> obj | |
55 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 44}]}} | |
56 | ||
31 | 57 | The ``JsonPointer`` class wraps a (string) path and can be used to access the |
32 | 58 | same path on several objects. |
33 | 59 |
49 | 49 | |
50 | 50 | from itertools import tee |
51 | 51 | import re |
52 | import copy | |
52 | 53 | |
53 | 54 | |
54 | 55 | # array indices must not contain leading zeros, signs, spaces, decimals, etc |
102 | 103 | |
103 | 104 | pointer = JsonPointer(pointer) |
104 | 105 | return pointer.resolve(doc, default) |
106 | ||
107 | def set_pointer(doc, pointer, value, inplace=True): | |
108 | """ | |
109 | Resolves pointer against doc and sets the value of the target within doc. | |
110 | ||
111 | With inplace set to true, doc is modified as long as pointer is not the | |
112 | root. | |
113 | ||
114 | >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} | |
115 | ||
116 | >>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \ | |
117 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} | |
118 | True | |
119 | ||
120 | >>> set_pointer(obj, '/foo/yet%20another%20prop', 'added prop') == \ | |
121 | {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}} | |
122 | True | |
123 | ||
124 | """ | |
125 | ||
126 | pointer = JsonPointer(pointer) | |
127 | return pointer.set(doc, value, inplace) | |
105 | 128 | |
106 | 129 | |
107 | 130 | class JsonPointer(object): |
148 | 171 | |
149 | 172 | get = resolve |
150 | 173 | |
174 | def set(self, doc, value, inplace=True): | |
175 | """ Resolve the pointer against the doc and replace the target with value. """ | |
176 | ||
177 | if len(self.parts) == 0: | |
178 | if inplace: | |
179 | raise JsonPointerException('cannot set root in place') | |
180 | return value | |
181 | ||
182 | if not inplace: | |
183 | doc = copy.deepcopy(doc) | |
184 | ||
185 | (parent, part) = self.to_last(doc) | |
186 | ||
187 | parent[part] = value | |
188 | return doc | |
151 | 189 | |
152 | 190 | def get_part(self, doc, part): |
153 | 191 | """ Returns the next step in the correct type """ |
165 | 203 | |
166 | 204 | return int(part) |
167 | 205 | |
206 | elif hasattr(doc, '__getitem__'): | |
207 | # Allow indexing via ducktyping if the target has defined __getitem__ | |
208 | return part | |
209 | ||
168 | 210 | else: |
169 | raise JsonPointerException("Unknown document type '%s'" % (doc.__class__,)) | |
211 | raise JsonPointerException("Document '%s' does not support indexing, " | |
212 | "must be dict/list or support __getitem__" % type(doc)) | |
170 | 213 | |
171 | 214 | |
172 | 215 | def walk(self, doc, part): |
174 | 217 | |
175 | 218 | part = self.get_part(doc, part) |
176 | 219 | |
177 | # type is already checked in get_part, so we assert here | |
178 | # for consistency | |
179 | assert type(doc) in (dict, list), "invalid document type %s" (type(doc),) | |
220 | assert (type(doc) in (dict, list) or hasattr(doc, '__getitem__')), "invalid document type %s" (type(doc)) | |
180 | 221 | |
181 | 222 | if isinstance(doc, dict): |
182 | 223 | try: |
196 | 237 | except IndexError: |
197 | 238 | raise JsonPointerException("index '%s' is out of bounds" % (part, )) |
198 | 239 | |
240 | else: | |
241 | # Object supports __getitem__, assume custom indexing | |
242 | return doc[part] | |
199 | 243 | |
200 | 244 | def contains(self, ptr): |
201 | """" Returns True if self contains the given ptr """ | |
245 | """ Returns True if self contains the given ptr """ | |
202 | 246 | return len(self.parts) > len(ptr.parts) and \ |
203 | 247 | self.parts[:len(ptr.parts)] == ptr.parts |
204 | 248 |
3 | 3 | import doctest |
4 | 4 | import unittest |
5 | 5 | import sys |
6 | import copy | |
6 | 7 | from jsonpointer import resolve_pointer, EndOfList, JsonPointerException, \ |
7 | JsonPointer | |
8 | JsonPointer, set_pointer | |
8 | 9 | |
9 | 10 | class SpecificationTests(unittest.TestCase): |
10 | 11 | """ Tests all examples from the JSON Pointer specification """ |
109 | 110 | self.assertEqual(nxt, 'b') |
110 | 111 | |
111 | 112 | |
113 | class SetTests(unittest.TestCase): | |
114 | ||
115 | def test_set(self): | |
116 | doc = { | |
117 | "foo": ["bar", "baz"], | |
118 | "": 0, | |
119 | "a/b": 1, | |
120 | "c%d": 2, | |
121 | "e^f": 3, | |
122 | "g|h": 4, | |
123 | "i\\j": 5, | |
124 | "k\"l": 6, | |
125 | " ": 7, | |
126 | "m~n": 8 | |
127 | } | |
128 | origdoc = copy.deepcopy(doc) | |
129 | ||
130 | # inplace=False | |
131 | newdoc = set_pointer(doc, "/foo/1", "cod", inplace=False) | |
132 | self.assertEqual(resolve_pointer(newdoc, "/foo/1"), "cod") | |
133 | ||
134 | newdoc = set_pointer(doc, "/", 9, inplace=False) | |
135 | self.assertEqual(resolve_pointer(newdoc, "/"), 9) | |
136 | ||
137 | newdoc = set_pointer(doc, "/fud", {}, inplace=False) | |
138 | newdoc = set_pointer(newdoc, "/fud/gaw", [1, 2, 3], inplace=False) | |
139 | self.assertEqual(resolve_pointer(newdoc, "/fud"), {'gaw' : [1, 2, 3]}) | |
140 | ||
141 | newdoc = set_pointer(doc, "", 9, inplace=False) | |
142 | self.assertEqual(newdoc, 9) | |
143 | ||
144 | self.assertEqual(doc, origdoc) | |
145 | ||
146 | # inplace=True | |
147 | set_pointer(doc, "/foo/1", "cod") | |
148 | self.assertEqual(resolve_pointer(doc, "/foo/1"), "cod") | |
149 | ||
150 | set_pointer(doc, "/", 9) | |
151 | self.assertEqual(resolve_pointer(doc, "/"), 9) | |
152 | ||
153 | self.assertRaises(JsonPointerException, set_pointer, doc, "/fud/gaw", 9) | |
154 | ||
155 | set_pointer(doc, "/fud", {}) | |
156 | set_pointer(doc, "/fud/gaw", [1, 2, 3] ) | |
157 | self.assertEqual(resolve_pointer(doc, "/fud"), {'gaw' : [1, 2, 3]}) | |
158 | ||
159 | self.assertRaises(JsonPointerException, set_pointer, doc, "", 9) | |
160 | ||
161 | class AltTypesTests(unittest.TestCase): | |
162 | ||
163 | def test_alttypes(self): | |
164 | JsonPointer.alttypes = True | |
165 | ||
166 | class Node(object): | |
167 | def __init__(self, name, parent=None): | |
168 | self.name = name | |
169 | self.parent = parent | |
170 | self.left = None | |
171 | self.right = None | |
172 | ||
173 | def set_left(self, node): | |
174 | node.parent = self | |
175 | self.left = node | |
176 | ||
177 | def set_right(self, node): | |
178 | node.parent = self | |
179 | self.right = node | |
180 | ||
181 | def __getitem__(self, key): | |
182 | if key == 'left': | |
183 | return self.left | |
184 | if key == 'right': | |
185 | return self.right | |
186 | ||
187 | raise KeyError("Only left and right supported") | |
188 | ||
189 | def __setitem__(self, key, val): | |
190 | if key == 'left': | |
191 | return self.set_left(val) | |
192 | if key == 'right': | |
193 | return self.set_right(val) | |
194 | ||
195 | raise KeyError("Only left and right supported: %s" % key) | |
196 | ||
197 | ||
198 | root = Node('root') | |
199 | root.set_left(Node('a')) | |
200 | root.left.set_left(Node('aa')) | |
201 | root.left.set_right(Node('ab')) | |
202 | root.set_right(Node('b')) | |
203 | root.right.set_left(Node('ba')) | |
204 | root.right.set_right(Node('bb')) | |
205 | ||
206 | self.assertEqual(resolve_pointer(root, '/left').name, 'a') | |
207 | self.assertEqual(resolve_pointer(root, '/left/right').name, 'ab') | |
208 | self.assertEqual(resolve_pointer(root, '/right').name, 'b') | |
209 | self.assertEqual(resolve_pointer(root, '/right/left').name, 'ba') | |
210 | ||
211 | newroot = set_pointer(root, '/left/right', Node('AB'), inplace=False) | |
212 | self.assertEqual(resolve_pointer(root, '/left/right').name, 'ab') | |
213 | self.assertEqual(resolve_pointer(newroot, '/left/right').name, 'AB') | |
214 | ||
215 | set_pointer(root, '/left/right', Node('AB')) | |
216 | self.assertEqual(resolve_pointer(root, '/left/right').name, 'AB') | |
217 | ||
112 | 218 | suite = unittest.TestSuite() |
113 | 219 | suite.addTest(unittest.makeSuite(SpecificationTests)) |
114 | 220 | suite.addTest(unittest.makeSuite(ComparisonTests)) |
115 | 221 | suite.addTest(unittest.makeSuite(WrongInputTests)) |
116 | 222 | suite.addTest(unittest.makeSuite(ToLastTests)) |
223 | suite.addTest(unittest.makeSuite(SetTests)) | |
224 | suite.addTest(unittest.makeSuite(AltTypesTests)) | |
117 | 225 | |
118 | 226 | modules = ['jsonpointer'] |
119 | 227 |