##// END OF EJS Templates
Add feature description to whatsnew
Add feature description to whatsnew

File last commit:

r8673:7fe4928b
r12369:1882b7b0
Show More
_jsonpointer.py
228 lines | 6.4 KiB | text/x-python | PythonLexer
# -*- coding: utf-8 -*-
#
# python-json-pointer - An implementation of the JSON Pointer syntax
# https://github.com/stefankoegl/python-json-pointer
#
# Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
""" Identify specific nodes in a JSON document according to
http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-04 """
# Will be parsed by setup.py to determine package metadata
__author__ = 'Stefan Kögl <stefan@skoegl.net>'
__version__ = '0.3'
__website__ = 'https://github.com/stefankoegl/python-json-pointer'
__license__ = 'Modified BSD License'
try:
from urllib import unquote
from itertools import izip
except ImportError: # Python 3
from urllib.parse import unquote
izip = zip
from itertools import tee
class JsonPointerException(Exception):
pass
_nothing = object()
def resolve_pointer(doc, pointer, default=_nothing):
"""
Resolves pointer against doc and returns the referenced object
>>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}}
>>> resolve_pointer(obj, '') == obj
True
>>> resolve_pointer(obj, '/foo') == obj['foo']
True
>>> resolve_pointer(obj, '/foo/another%20prop') == obj['foo']['another prop']
True
>>> resolve_pointer(obj, '/foo/another%20prop/baz') == obj['foo']['another prop']['baz']
True
>>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0]
True
>>> resolve_pointer(obj, '/some/path', None) == None
True
"""
pointer = JsonPointer(pointer)
return pointer.resolve(doc, default)
def set_pointer(doc, pointer, value):
"""
Set a field to a given value
The field is indicates by a base location that is given in the constructor,
and an optional relative location in the call to set. If the path doesn't
exist, it is created if possible
>>> obj = {"foo": 2}
>>> pointer = JsonPointer('/bar')
>>> pointer.set(obj, 'one', '0')
>>> pointer.set(obj, 'two', '1')
>>> obj
{'foo': 2, 'bar': ['one', 'two']}
>>> obj = {"foo": 2, "bar": []}
>>> pointer = JsonPointer('/bar')
>>> pointer.set(obj, 5, '0/x')
>>> obj
{'foo': 2, 'bar': [{'x': 5}]}
>>> obj = {'foo': 2, 'bar': [{'x': 5}]}
>>> pointer = JsonPointer('/bar/0')
>>> pointer.set(obj, 10, 'y/0')
>>> obj == {'foo': 2, 'bar': [{'y': [10], 'x': 5}]}
True
"""
pointer = JsonPointer(pointer)
pointer.set(doc, value)
class JsonPointer(object):
""" A JSON Pointer that can reference parts of an JSON document """
def __init__(self, pointer):
parts = pointer.split('/')
if parts.pop(0) != '':
raise JsonPointerException('location must starts with /')
parts = map(unquote, parts)
parts = [part.replace('~1', '/') for part in parts]
parts = [part.replace('~0', '~') for part in parts]
self.parts = parts
def resolve(self, doc, default=_nothing):
"""Resolves the pointer against doc and returns the referenced object"""
for part in self.parts:
try:
doc = self.walk(doc, part)
except JsonPointerException:
if default is _nothing:
raise
else:
return default
return doc
get = resolve
def set(self, doc, value, path=None):
""" Sets a field of doc to value
The location of the field is given by the pointers base location and
the optional path which is relative to the base location """
fullpath = list(self.parts)
if path:
fullpath += path.split('/')
for part, nextpart in pairwise(fullpath):
try:
doc = self.walk(doc, part)
except JsonPointerException:
step_val = [] if nextpart.isdigit() else {}
doc = self._set_value(doc, part, step_val)
self._set_value(doc, fullpath[-1], value)
@staticmethod
def _set_value(doc, part, value):
part = int(part) if part.isdigit() else part
if isinstance(doc, dict):
doc[part] = value
if isinstance(doc, list):
if len(doc) < part:
doc[part] = value
if len(doc) == part:
doc.append(value)
else:
raise IndexError
return doc[part]
def walk(self, doc, part):
""" Walks one step in doc and returns the referenced part """
# Its not clear if a location "1" should be considered as 1 or "1"
# We prefer the integer-variant if possible
part_variants = self._try_parse(part) + [part]
for variant in part_variants:
try:
return doc[variant]
except:
continue
raise JsonPointerException("'%s' not found in %s" % (part, doc))
@staticmethod
def _try_parse(val, cls=int):
try:
return [cls(val)]
except:
return []
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = tee(iterable)
for _ in b:
break
return izip(a, b)