Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonpointer.py: 32%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#
3# python-json-pointer - An implementation of the JSON Pointer syntax
4# https://github.com/stefankoegl/python-json-pointer
5#
6# Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net>
7# All rights reserved.
8#
9# Redistribution and use in source and binary forms, with or without
10# modification, are permitted provided that the following conditions
11# are met:
12#
13# 1. Redistributions of source code must retain the above copyright
14# notice, this list of conditions and the following disclaimer.
15# 2. Redistributions in binary form must reproduce the above copyright
16# notice, this list of conditions and the following disclaimer in the
17# documentation and/or other materials provided with the distribution.
18# 3. The name of the author may not be used to endorse or promote products
19# derived from this software without specific prior written permission.
20#
21# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31#
33""" Identify specific nodes in a JSON document (RFC 6901) """
35# Will be parsed by setup.py to determine package metadata
36__author__ = 'Stefan Kögl <stefan@skoegl.net>'
37__version__ = '3.0.0'
38__website__ = 'https://github.com/stefankoegl/python-json-pointer'
39__license__ = 'Modified BSD License'
41import copy
42import re
43from collections.abc import Mapping, Sequence
44from itertools import tee, chain
46_nothing = object()
49def set_pointer(doc, pointer, value, inplace=True):
50 """Resolves a pointer against doc and sets the value of the target within doc.
52 With inplace set to true, doc is modified as long as pointer is not the
53 root.
55 >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}}
57 >>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \
58 {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}}
59 True
61 >>> set_pointer(obj, '/foo/yet another prop', 'added prop') == \
62 {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}}
63 True
65 >>> obj = {'foo': {}}
66 >>> set_pointer(obj, '/foo/a%20b', 'x') == \
67 {'foo': {'a%20b': 'x' }}
68 True
69 """
71 pointer = JsonPointer(pointer)
72 return pointer.set(doc, value, inplace)
75def resolve_pointer(doc, pointer, default=_nothing):
76 """ Resolves pointer against doc and returns the referenced object
78 >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}, 'a%20b': 1, 'c d': 2}
80 >>> resolve_pointer(obj, '') == obj
81 True
83 >>> resolve_pointer(obj, '/foo') == obj['foo']
84 True
86 >>> resolve_pointer(obj, '/foo/another prop') == obj['foo']['another prop']
87 True
89 >>> resolve_pointer(obj, '/foo/another prop/baz') == obj['foo']['another prop']['baz']
90 True
92 >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0]
93 True
95 >>> resolve_pointer(obj, '/some/path', None) == None
96 True
98 >>> resolve_pointer(obj, '/a b', None) == None
99 True
101 >>> resolve_pointer(obj, '/a%20b') == 1
102 True
104 >>> resolve_pointer(obj, '/c d') == 2
105 True
107 >>> resolve_pointer(obj, '/c%20d', None) == None
108 True
109 """
111 pointer = JsonPointer(pointer)
112 return pointer.resolve(doc, default)
115def pairwise(iterable):
116 """ Transforms a list to a list of tuples of adjacent items
118 s -> (s0,s1), (s1,s2), (s2, s3), ...
120 >>> list(pairwise([]))
121 []
123 >>> list(pairwise([1]))
124 []
126 >>> list(pairwise([1, 2, 3, 4]))
127 [(1, 2), (2, 3), (3, 4)]
128 """
129 a, b = tee(iterable)
130 for _ in b:
131 break
132 return zip(a, b)
135class JsonPointerException(Exception):
136 pass
139class EndOfList(object):
140 """Result of accessing element "-" of a list"""
142 def __init__(self, list_):
143 self.list_ = list_
145 def __repr__(self):
146 return '{cls}({lst})'.format(cls=self.__class__.__name__,
147 lst=repr(self.list_))
150class JsonPointer(object):
151 """A JSON Pointer that can reference parts of a JSON document"""
153 # Array indices must not contain:
154 # leading zeros, signs, spaces, decimals, etc
155 _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$')
156 _RE_INVALID_ESCAPE = re.compile('(~[^01]|~$)')
158 def __init__(self, pointer):
160 # validate escapes
161 invalid_escape = self._RE_INVALID_ESCAPE.search(pointer)
162 if invalid_escape:
163 raise JsonPointerException('Found invalid escape {}'.format(
164 invalid_escape.group()))
166 parts = pointer.split('/')
167 if parts.pop(0) != '':
168 raise JsonPointerException('Location must start with /')
170 parts = [unescape(part) for part in parts]
171 self.parts = parts
173 def to_last(self, doc):
174 """Resolves ptr until the last step, returns (sub-doc, last-step)"""
176 if not self.parts:
177 return doc, None
179 for part in self.parts[:-1]:
180 doc = self.walk(doc, part)
182 return doc, JsonPointer.get_part(doc, self.parts[-1])
184 def resolve(self, doc, default=_nothing):
185 """Resolves the pointer against doc and returns the referenced object"""
187 for part in self.parts:
189 try:
190 doc = self.walk(doc, part)
191 except JsonPointerException:
192 if default is _nothing:
193 raise
194 else:
195 return default
197 return doc
199 get = resolve
201 def set(self, doc, value, inplace=True):
202 """Resolve the pointer against the doc and replace the target with value."""
204 if len(self.parts) == 0:
205 if inplace:
206 raise JsonPointerException('Cannot set root in place')
207 return value
209 if not inplace:
210 doc = copy.deepcopy(doc)
212 (parent, part) = self.to_last(doc)
214 if isinstance(parent, Sequence) and part == '-':
215 parent.append(value)
216 else:
217 parent[part] = value
219 return doc
221 @classmethod
222 def get_part(cls, doc, part):
223 """Returns the next step in the correct type"""
225 if isinstance(doc, Mapping):
226 return part
228 elif isinstance(doc, Sequence):
230 if part == '-':
231 return part
233 if not JsonPointer._RE_ARRAY_INDEX.match(str(part)):
234 raise JsonPointerException("'%s' is not a valid sequence index" % part)
236 return int(part)
238 elif hasattr(doc, '__getitem__'):
239 # Allow indexing via ducktyping
240 # if the target has defined __getitem__
241 return part
243 else:
244 raise JsonPointerException("Document '%s' does not support indexing, "
245 "must be mapping/sequence or support __getitem__" % type(doc))
247 def get_parts(self):
248 """Returns the list of the parts. For example, JsonPointer('/a/b').get_parts() == ['a', 'b']"""
250 return self.parts
252 def walk(self, doc, part):
253 """ Walks one step in doc and returns the referenced part """
255 part = JsonPointer.get_part(doc, part)
257 assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),)
259 if isinstance(doc, Sequence):
260 if part == '-':
261 return EndOfList(doc)
263 try:
264 return doc[part]
266 except IndexError:
267 raise JsonPointerException("index '%s' is out of bounds" % (part,))
269 # Else the object is a mapping or supports __getitem__(so assume custom indexing)
270 try:
271 return doc[part]
273 except KeyError:
274 raise JsonPointerException("member '%s' not found in %s" % (part, doc))
276 def contains(self, ptr):
277 """ Returns True if self contains the given ptr """
278 return self.parts[:len(ptr.parts)] == ptr.parts
280 def __contains__(self, item):
281 """ Returns True if self contains the given ptr """
282 return self.contains(item)
284 def join(self, suffix):
285 """ Returns a new JsonPointer with the given suffix append to this ptr """
286 if isinstance(suffix, JsonPointer):
287 suffix_parts = suffix.parts
288 elif isinstance(suffix, str):
289 suffix_parts = JsonPointer(suffix).parts
290 else:
291 suffix_parts = suffix
292 try:
293 return JsonPointer.from_parts(chain(self.parts, suffix_parts))
294 except: # noqa E722
295 raise JsonPointerException("Invalid suffix")
297 def __truediv__(self, suffix): # Python 3
298 return self.join(suffix)
300 @property
301 def path(self):
302 """Returns the string representation of the pointer
304 >>> ptr = JsonPointer('/~0/0/~1').path == '/~0/0/~1'
305 """
306 parts = [escape(part) for part in self.parts]
307 return ''.join('/' + part for part in parts)
309 def __eq__(self, other):
310 """Compares a pointer to another object
312 Pointers can be compared by comparing their strings (or splitted
313 strings), because no two different parts can point to the same
314 structure in an object (eg no different number representations)
315 """
317 if not isinstance(other, JsonPointer):
318 return False
320 return self.parts == other.parts
322 def __hash__(self):
323 return hash(tuple(self.parts))
325 def __str__(self):
326 return self.path
328 def __repr__(self):
329 return type(self).__name__ + "(" + repr(self.path) + ")"
331 @classmethod
332 def from_parts(cls, parts):
333 """Constructs a JsonPointer from a list of (unescaped) paths
335 >>> JsonPointer.from_parts(['a', '~', '/', 0]).path == '/a/~0/~1/0'
336 True
337 """
338 parts = [escape(str(part)) for part in parts]
339 ptr = cls(''.join('/' + part for part in parts))
340 return ptr
343def escape(s):
344 return s.replace('~', '~0').replace('/', '~1')
347def unescape(s):
348 return s.replace('~1', '/').replace('~0', '~')