codekingpro/portable-devtools
115k
1# python-json-pointer - An implementation of the JSON Pointer syntax2# https://github.com/stefankoegl/python-json-pointer3#4# Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net>5# All rights reserved.6#7# Redistribution and use in source and binary forms, with or without8# modification, are permitted provided that the following conditions9# are met:10#11# 1. Redistributions of source code must retain the above copyright12# notice, this list of conditions and the following disclaimer.13# 2. Redistributions in binary form must reproduce the above copyright14# notice, this list of conditions and the following disclaimer in the15# documentation and/or other materials provided with the distribution.16# 3. The name of the author may not be used to endorse or promote products17# derived from this software without specific prior written permission.18#19# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR20# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES21# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.22# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,23# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT24# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF28# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.29#30 31""" Identify specific nodes in a JSON document (RFC 6901) """32 33# Will be parsed by setup.py to determine package metadata34__author__ = 'Stefan Kögl <stefan@skoegl.net>'35__version__ = '3.1.1'36__website__ = 'https://github.com/stefankoegl/python-json-pointer'37__license__ = 'Modified BSD License'38 39import copy40import re41from collections.abc import Mapping, Sequence42from itertools import tee, chain43 44_nothing = object()45 46 47def set_pointer(doc, pointer, value, inplace=True):48 """Resolves a pointer against doc and sets the value of the target within doc.49 50 With inplace set to true, doc is modified as long as pointer is not the51 root.52 53 >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}}54 55 >>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \56 {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}}57 True58 59 >>> set_pointer(obj, '/foo/yet another prop', 'added prop') == \60 {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}}61 True62 63 >>> obj = {'foo': {}}64 >>> set_pointer(obj, '/foo/a%20b', 'x') == \65 {'foo': {'a%20b': 'x' }}66 True67 """68 69 pointer = JsonPointer(pointer)70 return pointer.set(doc, value, inplace)71 72 73def resolve_pointer(doc, pointer, default=_nothing):74 """ Resolves pointer against doc and returns the referenced object75 76 >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}, 'a%20b': 1, 'c d': 2}77 78 >>> resolve_pointer(obj, '') == obj79 True80 81 >>> resolve_pointer(obj, '/foo') == obj['foo']82 True83 84 >>> resolve_pointer(obj, '/foo/another prop') == obj['foo']['another prop']85 True86 87 >>> resolve_pointer(obj, '/foo/another prop/baz') == obj['foo']['another prop']['baz']88 True89 90 >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0]91 True92 93 >>> resolve_pointer(obj, '/some/path', None) == None94 True95 96 >>> resolve_pointer(obj, '/a b', None) == None97 True98 99 >>> resolve_pointer(obj, '/a%20b') == 1100 True101 102 >>> resolve_pointer(obj, '/c d') == 2103 True104 105 >>> resolve_pointer(obj, '/c%20d', None) == None106 True107 """108 109 pointer = JsonPointer(pointer)110 return pointer.resolve(doc, default)111 112 113def pairwise(iterable):114 """ Transforms a list to a list of tuples of adjacent items115 116 s -> (s0,s1), (s1,s2), (s2, s3), ...117 118 >>> list(pairwise([]))119 []120 121 >>> list(pairwise([1]))122 []123 124 >>> list(pairwise([1, 2, 3, 4]))125 [(1, 2), (2, 3), (3, 4)]126 """127 a, b = tee(iterable)128 for _ in b:129 break130 return zip(a, b)131 132 133class JsonPointerException(Exception):134 pass135 136 137class EndOfList:138 """Result of accessing element "-" of a list"""139 140 def __init__(self, list_):141 self.list_ = list_142 143 def __repr__(self):144 return '{cls}({lst})'.format(cls=self.__class__.__name__,145 lst=repr(self.list_))146 147 148class JsonPointer:149 """A JSON Pointer that can reference parts of a JSON document"""150 151 # Array indices must not contain:152 # leading zeros, signs, spaces, decimals, etc153 _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$')154 _RE_INVALID_ESCAPE = re.compile('(~[^01]|~$)')155 156 def __init__(self, pointer):157 158 # validate escapes159 invalid_escape = self._RE_INVALID_ESCAPE.search(pointer)160 if invalid_escape:161 raise JsonPointerException('Found invalid escape {}'.format(162 invalid_escape.group()))163 164 parts = pointer.split('/')165 if parts.pop(0) != '':166 raise JsonPointerException('Location must start with /')167 168 parts = [unescape(part) for part in parts]169 self.parts = parts170 171 def to_last(self, doc):172 """Resolves ptr until the last step, returns (sub-doc, last-step)"""173 174 if not self.parts:175 return doc, None176 177 for part in self.parts[:-1]:178 doc = self.walk(doc, part)179 180 return doc, JsonPointer.get_part(doc, self.parts[-1])181 182 def resolve(self, doc, default=_nothing):183 """Resolves the pointer against doc and returns the referenced object"""184 185 for part in self.parts:186 187 try:188 doc = self.walk(doc, part)189 except JsonPointerException:190 if default is _nothing:191 raise192 else:193 return default194 195 return doc196 197 get = resolve198 199 def set(self, doc, value, inplace=True):200 """Resolve the pointer against the doc and replace the target with value."""201 202 if len(self.parts) == 0:203 if inplace:204 raise JsonPointerException('Cannot set root in place')205 return value206 207 if not inplace:208 doc = copy.deepcopy(doc)209 210 (parent, part) = self.to_last(doc)211 212 if isinstance(parent, Sequence) and part == '-':213 parent.append(value)214 else:215 parent[part] = value216 217 return doc218 219 @classmethod220 def get_part(cls, doc, part):221 """Returns the next step in the correct type"""222 223 if isinstance(doc, Mapping):224 return part225 226 elif isinstance(doc, Sequence):227 228 if part == '-':229 return part230 231 if not JsonPointer._RE_ARRAY_INDEX.fullmatch(str(part)):232 raise JsonPointerException("'%s' is not a valid sequence index" % part)233 234 return int(part)235 236 elif hasattr(doc, '__getitem__'):237 # Allow indexing via ducktyping238 # if the target has defined __getitem__239 return part240 241 else:242 raise JsonPointerException("Document '%s' does not support indexing, "243 "must be mapping/sequence or support __getitem__" % type(doc))244 245 def get_parts(self):246 """Returns the list of the parts. For example, JsonPointer('/a/b').get_parts() == ['a', 'b']"""247 248 return self.parts249 250 def walk(self, doc, part):251 """ Walks one step in doc and returns the referenced part """252 253 part = JsonPointer.get_part(doc, part)254 255 assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),)256 257 if isinstance(doc, Sequence):258 if part == '-':259 return EndOfList(doc)260 261 try:262 return doc[part]263 264 except IndexError:265 raise JsonPointerException("index '%s' is out of bounds" % (part,))266 267 # Else the object is a mapping or supports __getitem__(so assume custom indexing)268 try:269 return doc[part]270 271 except KeyError:272 raise JsonPointerException("member '%s' not found in %s" % (part, doc))273 274 def contains(self, ptr):275 """ Returns True if self contains the given ptr """276 return self.parts[:len(ptr.parts)] == ptr.parts277 278 def __contains__(self, item):279 """ Returns True if self contains the given ptr """280 return self.contains(item)281 282 def join(self, suffix):283 """ Returns a new JsonPointer with the given suffix append to this ptr """284 if isinstance(suffix, JsonPointer):285 suffix_parts = suffix.parts286 elif isinstance(suffix, str):287 suffix_parts = JsonPointer(suffix).parts288 else:289 suffix_parts = suffix290 try:291 return JsonPointer.from_parts(chain(self.parts, suffix_parts))292 except: # noqa E722293 raise JsonPointerException("Invalid suffix")294 295 def __truediv__(self, suffix):296 return self.join(suffix)297 298 @property299 def path(self):300 """Returns the string representation of the pointer301 302 >>> ptr = JsonPointer('/~0/0/~1').path == '/~0/0/~1'303 """304 parts = [escape(part) for part in self.parts]305 return ''.join('/' + part for part in parts)306 307 def __eq__(self, other):308 """Compares a pointer to another object309 310 Pointers can be compared by comparing their strings (or splitted311 strings), because no two different parts can point to the same312 structure in an object (eg no different number representations)313 """314 315 if not isinstance(other, JsonPointer):316 return False317 318 return self.parts == other.parts319 320 def __hash__(self):321 return hash(tuple(self.parts))322 323 def __str__(self):324 return self.path325 326 def __repr__(self):327 return type(self).__name__ + "(" + repr(self.path) + ")"328 329 @classmethod330 def from_parts(cls, parts):331 """Constructs a JsonPointer from a list of (unescaped) paths332 333 >>> JsonPointer.from_parts(['a', '~', '/', 0]).path == '/a/~0/~1/0'334 True335 """336 parts = [escape(str(part)) for part in parts]337 ptr = cls(''.join('/' + part for part in parts))338 return ptr339 340 341def escape(s):342 return s.replace('~', '~0').replace('/', '~1')343 344 345def unescape(s):346 return s.replace('~1', '/').replace('~0', '~')347 