jsonpointer.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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. #
  32. """ Identify specific nodes in a JSON document (RFC 6901) """
  33. from __future__ import unicode_literals
  34. # Will be parsed by setup.py to determine package metadata
  35. __author__ = 'Stefan Kögl <stefan@skoegl.net>'
  36. __version__ = '2.1'
  37. __website__ = 'https://github.com/stefankoegl/python-json-pointer'
  38. __license__ = 'Modified BSD License'
  39. try:
  40. from itertools import izip
  41. str = unicode
  42. except ImportError: # Python 3
  43. izip = zip
  44. try:
  45. from collections.abc import Mapping, Sequence
  46. except ImportError: # Python 3
  47. from collections import Mapping, Sequence
  48. from itertools import tee
  49. import re
  50. import copy
  51. _nothing = object()
  52. def set_pointer(doc, pointer, value, inplace=True):
  53. """Resolves pointer against doc and sets the value of the target within doc.
  54. With inplace set to true, doc is modified as long as pointer is not the
  55. root.
  56. >>> 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
  60. >>> set_pointer(obj, '/foo/yet another prop', 'added prop') == \
  61. {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}}
  62. True
  63. >>> obj = {'foo': {}}
  64. >>> set_pointer(obj, '/foo/a%20b', 'x') == \
  65. {'foo': {'a%20b': 'x' }}
  66. True
  67. """
  68. pointer = JsonPointer(pointer)
  69. return pointer.set(doc, value, inplace)
  70. def resolve_pointer(doc, pointer, default=_nothing):
  71. """ Resolves pointer against doc and returns the referenced object
  72. >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}, 'a%20b': 1, 'c d': 2}
  73. >>> resolve_pointer(obj, '') == obj
  74. True
  75. >>> resolve_pointer(obj, '/foo') == obj['foo']
  76. True
  77. >>> resolve_pointer(obj, '/foo/another prop') == obj['foo']['another prop']
  78. True
  79. >>> resolve_pointer(obj, '/foo/another prop/baz') == obj['foo']['another prop']['baz']
  80. True
  81. >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0]
  82. True
  83. >>> resolve_pointer(obj, '/some/path', None) == None
  84. True
  85. >>> resolve_pointer(obj, '/a b', None) == None
  86. True
  87. >>> resolve_pointer(obj, '/a%20b') == 1
  88. True
  89. >>> resolve_pointer(obj, '/c d') == 2
  90. True
  91. >>> resolve_pointer(obj, '/c%20d', None) == None
  92. True
  93. """
  94. pointer = JsonPointer(pointer)
  95. return pointer.resolve(doc, default)
  96. def pairwise(iterable):
  97. """ Transforms a list to a list of tuples of adjacent items
  98. s -> (s0,s1), (s1,s2), (s2, s3), ...
  99. >>> list(pairwise([]))
  100. []
  101. >>> list(pairwise([1]))
  102. []
  103. >>> list(pairwise([1, 2, 3, 4]))
  104. [(1, 2), (2, 3), (3, 4)]
  105. """
  106. a, b = tee(iterable)
  107. for _ in b:
  108. break
  109. return izip(a, b)
  110. class JsonPointerException(Exception):
  111. pass
  112. class EndOfList(object):
  113. """Result of accessing element "-" of a list"""
  114. def __init__(self, list_):
  115. self.list_ = list_
  116. def __repr__(self):
  117. return '{cls}({lst})'.format(cls=self.__class__.__name__,
  118. lst=repr(self.list_))
  119. class JsonPointer(object):
  120. """A JSON Pointer that can reference parts of a JSON document"""
  121. # Array indices must not contain:
  122. # leading zeros, signs, spaces, decimals, etc
  123. _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$')
  124. _RE_INVALID_ESCAPE = re.compile('(~[^01]|~$)')
  125. def __init__(self, pointer):
  126. # validate escapes
  127. invalid_escape = self._RE_INVALID_ESCAPE.search(pointer)
  128. if invalid_escape:
  129. raise JsonPointerException('Found invalid escape {}'.format(
  130. invalid_escape.group()))
  131. parts = pointer.split('/')
  132. if parts.pop(0) != '':
  133. raise JsonPointerException('Location must start with /')
  134. parts = [unescape(part) for part in parts]
  135. self.parts = parts
  136. def to_last(self, doc):
  137. """Resolves ptr until the last step, returns (sub-doc, last-step)"""
  138. if not self.parts:
  139. return doc, None
  140. for part in self.parts[:-1]:
  141. doc = self.walk(doc, part)
  142. return doc, self.get_part(doc, self.parts[-1])
  143. def resolve(self, doc, default=_nothing):
  144. """Resolves the pointer against doc and returns the referenced object"""
  145. for part in self.parts:
  146. try:
  147. doc = self.walk(doc, part)
  148. except JsonPointerException:
  149. if default is _nothing:
  150. raise
  151. else:
  152. return default
  153. return doc
  154. get = resolve
  155. def set(self, doc, value, inplace=True):
  156. """Resolve the pointer against the doc and replace the target with value."""
  157. if len(self.parts) == 0:
  158. if inplace:
  159. raise JsonPointerException('Cannot set root in place')
  160. return value
  161. if not inplace:
  162. doc = copy.deepcopy(doc)
  163. (parent, part) = self.to_last(doc)
  164. parent[part] = value
  165. return doc
  166. def get_part(self, doc, part):
  167. """Returns the next step in the correct type"""
  168. if isinstance(doc, Mapping):
  169. return part
  170. elif isinstance(doc, Sequence):
  171. if part == '-':
  172. return part
  173. if not self._RE_ARRAY_INDEX.match(str(part)):
  174. raise JsonPointerException("'%s' is not a valid sequence index" % part)
  175. return int(part)
  176. elif hasattr(doc, '__getitem__'):
  177. # Allow indexing via ducktyping
  178. # if the target has defined __getitem__
  179. return part
  180. else:
  181. raise JsonPointerException("Document '%s' does not support indexing, "
  182. "must be mapping/sequence or support __getitem__" % type(doc))
  183. def walk(self, doc, part):
  184. """ Walks one step in doc and returns the referenced part """
  185. part = self.get_part(doc, part)
  186. assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),)
  187. if isinstance(doc, Sequence):
  188. if part == '-':
  189. return EndOfList(doc)
  190. try:
  191. return doc[part]
  192. except IndexError:
  193. raise JsonPointerException("index '%s' is out of bounds" % (part, ))
  194. # Else the object is a mapping or supports __getitem__(so assume custom indexing)
  195. try:
  196. return doc[part]
  197. except KeyError:
  198. raise JsonPointerException("member '%s' not found in %s" % (part, doc))
  199. def contains(self, ptr):
  200. """ Returns True if self contains the given ptr """
  201. return self.parts[:len(ptr.parts)] == ptr.parts
  202. def __contains__(self, item):
  203. """ Returns True if self contains the given ptr """
  204. return self.contains(item)
  205. @property
  206. def path(self):
  207. """Returns the string representation of the pointer
  208. >>> ptr = JsonPointer('/~0/0/~1').path == '/~0/0/~1'
  209. """
  210. parts = [escape(part) for part in self.parts]
  211. return ''.join('/' + part for part in parts)
  212. def __eq__(self, other):
  213. """Compares a pointer to another object
  214. Pointers can be compared by comparing their strings (or splitted
  215. strings), because no two different parts can point to the same
  216. structure in an object (eg no different number representations)
  217. """
  218. if not isinstance(other, JsonPointer):
  219. return False
  220. return self.parts == other.parts
  221. def __hash__(self):
  222. return hash(tuple(self.parts))
  223. @classmethod
  224. def from_parts(cls, parts):
  225. """Constructs a JsonPointer from a list of (unescaped) paths
  226. >>> JsonPointer.from_parts(['a', '~', '/', 0]).path == '/a/~0/~1/0'
  227. True
  228. """
  229. parts = [escape(str(part)) for part in parts]
  230. ptr = cls(''.join('/' + part for part in parts))
  231. return ptr
  232. def escape(s):
  233. return s.replace('~', '~0').replace('/', '~1')
  234. def unescape(s):
  235. return s.replace('~1', '/').replace('~0', '~')