jsonpointer.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. # python-json-pointer - An implementation of the JSON Pointer syntax
  2. # https://github.com/stefankoegl/python-json-pointer
  3. #
  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 without
  8. # modification, are permitted provided that the following conditions
  9. # are met:
  10. #
  11. # 1. Redistributions of source code must retain the above copyright
  12. # notice, this list of conditions and the following disclaimer.
  13. # 2. Redistributions in binary form must reproduce the above copyright
  14. # notice, this list of conditions and the following disclaimer in the
  15. # documentation and/or other materials provided with the distribution.
  16. # 3. The name of the author may not be used to endorse or promote products
  17. # derived from this software without specific prior written permission.
  18. #
  19. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
  20. # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  21. # 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, BUT
  24. # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  28. # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. #
  30. """ Identify specific nodes in a JSON document (RFC 6901) """
  31. # Will be parsed by setup.py to determine package metadata
  32. __author__ = 'Stefan Kögl <stefan@skoegl.net>'
  33. __version__ = '3.1.1'
  34. __website__ = 'https://github.com/stefankoegl/python-json-pointer'
  35. __license__ = 'Modified BSD License'
  36. import copy
  37. import re
  38. from collections.abc import Mapping, Sequence
  39. from itertools import tee, chain
  40. _nothing = object()
  41. def set_pointer(doc, pointer, value, inplace=True):
  42. """Resolves a pointer against doc and sets the value of the target within doc.
  43. With inplace set to true, doc is modified as long as pointer is not the
  44. root.
  45. >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}}
  46. >>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \
  47. {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}}
  48. True
  49. >>> set_pointer(obj, '/foo/yet another prop', 'added prop') == \
  50. {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}}
  51. True
  52. >>> obj = {'foo': {}}
  53. >>> set_pointer(obj, '/foo/a%20b', 'x') == \
  54. {'foo': {'a%20b': 'x' }}
  55. True
  56. """
  57. pointer = JsonPointer(pointer)
  58. return pointer.set(doc, value, inplace)
  59. def resolve_pointer(doc, pointer, default=_nothing):
  60. """ Resolves pointer against doc and returns the referenced object
  61. >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}, 'a%20b': 1, 'c d': 2}
  62. >>> resolve_pointer(obj, '') == obj
  63. True
  64. >>> resolve_pointer(obj, '/foo') == obj['foo']
  65. True
  66. >>> resolve_pointer(obj, '/foo/another prop') == obj['foo']['another prop']
  67. True
  68. >>> resolve_pointer(obj, '/foo/another prop/baz') == obj['foo']['another prop']['baz']
  69. True
  70. >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0]
  71. True
  72. >>> resolve_pointer(obj, '/some/path', None) == None
  73. True
  74. >>> resolve_pointer(obj, '/a b', None) == None
  75. True
  76. >>> resolve_pointer(obj, '/a%20b') == 1
  77. True
  78. >>> resolve_pointer(obj, '/c d') == 2
  79. True
  80. >>> resolve_pointer(obj, '/c%20d', None) == None
  81. True
  82. """
  83. pointer = JsonPointer(pointer)
  84. return pointer.resolve(doc, default)
  85. def pairwise(iterable):
  86. """ Transforms a list to a list of tuples of adjacent items
  87. s -> (s0,s1), (s1,s2), (s2, s3), ...
  88. >>> list(pairwise([]))
  89. []
  90. >>> list(pairwise([1]))
  91. []
  92. >>> list(pairwise([1, 2, 3, 4]))
  93. [(1, 2), (2, 3), (3, 4)]
  94. """
  95. a, b = tee(iterable)
  96. for _ in b:
  97. break
  98. return zip(a, b)
  99. class JsonPointerException(Exception):
  100. pass
  101. class EndOfList:
  102. """Result of accessing element "-" of a list"""
  103. def __init__(self, list_):
  104. self.list_ = list_
  105. def __repr__(self):
  106. return '{cls}({lst})'.format(cls=self.__class__.__name__,
  107. lst=repr(self.list_))
  108. class JsonPointer:
  109. """A JSON Pointer that can reference parts of a JSON document"""
  110. # Array indices must not contain:
  111. # leading zeros, signs, spaces, decimals, etc
  112. _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$')
  113. _RE_INVALID_ESCAPE = re.compile('(~[^01]|~$)')
  114. def __init__(self, pointer):
  115. # validate escapes
  116. invalid_escape = self._RE_INVALID_ESCAPE.search(pointer)
  117. if invalid_escape:
  118. raise JsonPointerException('Found invalid escape {}'.format(
  119. invalid_escape.group()))
  120. parts = pointer.split('/')
  121. if parts.pop(0) != '':
  122. raise JsonPointerException('Location must start with /')
  123. parts = [unescape(part) for part in parts]
  124. self.parts = parts
  125. def to_last(self, doc):
  126. """Resolves ptr until the last step, returns (sub-doc, last-step)"""
  127. if not self.parts:
  128. return doc, None
  129. for part in self.parts[:-1]:
  130. doc = self.walk(doc, part)
  131. return doc, JsonPointer.get_part(doc, self.parts[-1])
  132. def resolve(self, doc, default=_nothing):
  133. """Resolves the pointer against doc and returns the referenced object"""
  134. for part in self.parts:
  135. try:
  136. doc = self.walk(doc, part)
  137. except JsonPointerException:
  138. if default is _nothing:
  139. raise
  140. else:
  141. return default
  142. return doc
  143. get = resolve
  144. def set(self, doc, value, inplace=True):
  145. """Resolve the pointer against the doc and replace the target with value."""
  146. if len(self.parts) == 0:
  147. if inplace:
  148. raise JsonPointerException('Cannot set root in place')
  149. return value
  150. if not inplace:
  151. doc = copy.deepcopy(doc)
  152. (parent, part) = self.to_last(doc)
  153. if isinstance(parent, Sequence) and part == '-':
  154. parent.append(value)
  155. else:
  156. parent[part] = value
  157. return doc
  158. @classmethod
  159. def get_part(cls, doc, part):
  160. """Returns the next step in the correct type"""
  161. if isinstance(doc, Mapping):
  162. return part
  163. elif isinstance(doc, Sequence):
  164. if part == '-':
  165. return part
  166. if not JsonPointer._RE_ARRAY_INDEX.fullmatch(str(part)):
  167. raise JsonPointerException("'%s' is not a valid sequence index" % part)
  168. return int(part)
  169. elif hasattr(doc, '__getitem__'):
  170. # Allow indexing via ducktyping
  171. # if the target has defined __getitem__
  172. return part
  173. else:
  174. raise JsonPointerException("Document '%s' does not support indexing, "
  175. "must be mapping/sequence or support __getitem__" % type(doc))
  176. def get_parts(self):
  177. """Returns the list of the parts. For example, JsonPointer('/a/b').get_parts() == ['a', 'b']"""
  178. return self.parts
  179. def walk(self, doc, part):
  180. """ Walks one step in doc and returns the referenced part """
  181. part = JsonPointer.get_part(doc, part)
  182. assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),)
  183. if isinstance(doc, Sequence):
  184. if part == '-':
  185. return EndOfList(doc)
  186. try:
  187. return doc[part]
  188. except IndexError:
  189. raise JsonPointerException("index '%s' is out of bounds" % (part,))
  190. # Else the object is a mapping or supports __getitem__(so assume custom indexing)
  191. try:
  192. return doc[part]
  193. except KeyError:
  194. raise JsonPointerException("member '%s' not found in %s" % (part, doc))
  195. def contains(self, ptr):
  196. """ Returns True if self contains the given ptr """
  197. return self.parts[:len(ptr.parts)] == ptr.parts
  198. def __contains__(self, item):
  199. """ Returns True if self contains the given ptr """
  200. return self.contains(item)
  201. def join(self, suffix):
  202. """ Returns a new JsonPointer with the given suffix append to this ptr """
  203. if isinstance(suffix, JsonPointer):
  204. suffix_parts = suffix.parts
  205. elif isinstance(suffix, str):
  206. suffix_parts = JsonPointer(suffix).parts
  207. else:
  208. suffix_parts = suffix
  209. try:
  210. return JsonPointer.from_parts(chain(self.parts, suffix_parts))
  211. except: # noqa E722
  212. raise JsonPointerException("Invalid suffix")
  213. def __truediv__(self, suffix):
  214. return self.join(suffix)
  215. @property
  216. def path(self):
  217. """Returns the string representation of the pointer
  218. >>> ptr = JsonPointer('/~0/0/~1').path == '/~0/0/~1'
  219. """
  220. parts = [escape(part) for part in self.parts]
  221. return ''.join('/' + part for part in parts)
  222. def __eq__(self, other):
  223. """Compares a pointer to another object
  224. Pointers can be compared by comparing their strings (or splitted
  225. strings), because no two different parts can point to the same
  226. structure in an object (eg no different number representations)
  227. """
  228. if not isinstance(other, JsonPointer):
  229. return False
  230. return self.parts == other.parts
  231. def __hash__(self):
  232. return hash(tuple(self.parts))
  233. def __str__(self):
  234. return self.path
  235. def __repr__(self):
  236. return type(self).__name__ + "(" + repr(self.path) + ")"
  237. @classmethod
  238. def from_parts(cls, parts):
  239. """Constructs a JsonPointer from a list of (unescaped) paths
  240. >>> JsonPointer.from_parts(['a', '~', '/', 0]).path == '/a/~0/~1/0'
  241. True
  242. """
  243. parts = [escape(str(part)) for part in parts]
  244. ptr = cls(''.join('/' + part for part in parts))
  245. return ptr
  246. def escape(s):
  247. return s.replace('~', '~0').replace('/', '~1')
  248. def unescape(s):
  249. return s.replace('~1', '/').replace('~0', '~')