Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 80 additions & 84 deletions jsonpointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 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.
# 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.
# 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
Expand All @@ -30,14 +30,9 @@
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

from __future__ import unicode_literals

""" Identify specific nodes in a JSON document (RFC 6901) """

try:
from collections.abc import Mapping, Sequence
except ImportError:
from collections import Mapping, Sequence
from __future__ import unicode_literals

# Will be parsed by setup.py to determine package metadata
__author__ = 'Stefan Kögl <stefan@skoegl.net>'
Expand All @@ -49,43 +44,48 @@
try:
from urllib import unquote
from itertools import izip
except ImportError: # Python 3
except ImportError: # Python 3
from urllib.parse import unquote
izip = zip

try:
from collections.abc import Mapping, Sequence
except ImportError: # Python 3
from collections import Mapping, Sequence

from itertools import tee
import re
import copy


# array indices must not contain leading zeros, signs, spaces, decimals, etc
RE_ARRAY_INDEX=re.compile('0|[1-9][0-9]*$')


class JsonPointerException(Exception):
pass
_nothing = object()


class EndOfList(object):
""" Result of accessing element "-" of a list """
def set_pointer(doc, pointer, value, inplace=True):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you rearrange the methods and classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks much more clearer and nicer, more organized

"""Resolves pointer against doc and sets the value of the target within doc.

def __init__(self, list_):
self.list_ = list_
With inplace set to true, doc is modified as long as pointer is not the
root.

>>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}}

def __repr__(self):
return '{cls}({lst})'.format(cls=self.__class__.__name__,
lst=repr(self.list_))
>>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \
{'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}}
True

>>> set_pointer(obj, '/foo/yet%20another%20prop', 'added prop') == \
{'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}}
True
"""

_nothing = object()
pointer = JsonPointer(pointer)
return pointer.set(doc, value, inplace)


def resolve_pointer(doc, pointer, default=_nothing):
"""
Resolves pointer against doc and returns the referenced object
"""Resolves pointer against doc and returns the referenced object

>>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}}
>>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you change the quotes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code had a mixed use of quotes, in my opinion, one set of quotes should be used consistenly (in cases which allow it), and this type of quotes are more common in python code


>>> resolve_pointer(obj, '') == obj
True
Expand All @@ -104,37 +104,53 @@ def resolve_pointer(doc, pointer, default=_nothing):

>>> resolve_pointer(obj, '/some/path', None) == None
True

"""

pointer = JsonPointer(pointer)
return pointer.resolve(doc, default)

def set_pointer(doc, pointer, value, inplace=True):
"""
Resolves pointer against doc and sets the value of the target within doc.

With inplace set to true, doc is modified as long as pointer is not the
root.
def pairwise(iterable):
""" Transforms a list to a list of tuples of adjacent items

>>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}}
s -> (s0,s1), (s1,s2), (s2, s3), ...

>>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \
{'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}}
True
>>> list(pairwise([]))
[]

>>> set_pointer(obj, '/foo/yet%20another%20prop', 'added prop') == \
{'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}}
True
>>> list(pairwise([1]))
[]

>>> list(pairwise([1, 2, 3, 4]))
[(1, 2), (2, 3), (3, 4)]
"""
a, b = tee(iterable)
for _ in b:
break
return izip(a, b)

pointer = JsonPointer(pointer)
return pointer.set(doc, value, inplace)

class JsonPointerException(Exception):
pass


class EndOfList(object):
"""Result of accessing element "-" of a list"""

def __init__(self, list_):
self.list_ = list_

def __repr__(self):
return '{cls}({lst})'.format(cls=self.__class__.__name__,
lst=repr(self.list_))


class JsonPointer(object):
""" A JSON Pointer that can reference parts of an JSON document """
"""A JSON Pointer that can reference parts of an JSON document"""

# Array indices must not contain:
# leading zeros, signs, spaces, decimals, etc
_RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you move this here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only use of this re is made inside the class, thus it's a const not a global/


def __init__(self, pointer):
parts = pointer.split('/')
Expand All @@ -146,9 +162,8 @@ def __init__(self, pointer):
parts = [part.replace('~0', '~') for part in parts]
self.parts = parts


def to_last(self, doc):
""" Resolves ptr until the last step, returns (sub-doc, last-step) """
"""Resolves ptr until the last step, returns (sub-doc, last-step)"""

if not self.parts:
return doc, None
Expand All @@ -158,7 +173,6 @@ def to_last(self, doc):

return doc, self.get_part(doc, self.parts[-1])


def resolve(self, doc, default=_nothing):
"""Resolves the pointer against doc and returns the referenced object"""

Expand All @@ -174,11 +188,10 @@ def resolve(self, doc, default=_nothing):

return doc


get = resolve

def set(self, doc, value, inplace=True):
""" Resolve the pointer against the doc and replace the target with value. """
"""Resolve the pointer against the doc and replace the target with value."""

if len(self.parts) == 0:
if inplace:
Expand All @@ -194,7 +207,7 @@ def set(self, doc, value, inplace=True):
return doc

def get_part(self, doc, part):
""" Returns the next step in the correct type """
"""Returns the next step in the correct type"""

if isinstance(doc, Mapping):
return part
Expand All @@ -204,26 +217,27 @@ def get_part(self, doc, part):
if part == '-':
return part

if not RE_ARRAY_INDEX.match(str(part)):
raise JsonPointerException("'%s' is not a valid list index" % (part, ))
if not self._RE_ARRAY_INDEX.match(str(part)):
raise JsonPointerException("'%s' is not a valid sequence index" % part)

return int(part)

elif hasattr(doc, '__getitem__'):
# Allow indexing via ducktyping if the target has defined __getitem__
# Allow indexing via ducktyping
# if the target has defined __getitem__
return part

else:
raise JsonPointerException("Document '%s' does not support indexing, "
"must be dict/list or support __getitem__" % type(doc))

raise JsonPointerException("document '%s' does not support indexing, "
"must be mapping/sequence or support __getitem__" % type(doc))

def walk(self, doc, part):
""" Walks one step in doc and returns the referenced part """
"""Walks one step in doc and returns the referenced part"""

part = self.get_part(doc, part)

assert (type(doc) in (dict, list) or hasattr(doc, '__getitem__')), "invalid document type %s" % (type(doc),)
assert hasattr(doc, '__getitem__'), \
'invalid document type %s' % type(doc)

if isinstance(doc, Mapping):
try:
Expand All @@ -248,13 +262,14 @@ def walk(self, doc, part):
return doc[part]

def contains(self, ptr):
""" Returns True if self contains the given ptr """
"""Returns True if self contains the given ptr"""

return len(self.parts) > len(ptr.parts) and \
self.parts[:len(ptr.parts)] == ptr.parts
self.parts[:len(ptr.parts)] == ptr.parts

@property
def path(self):
""" Returns the string representation of the pointer
"""Returns the string representation of the pointer

>>> ptr = JsonPointer('/~0/0/~1').path == '/~0/0/~1'
"""
Expand All @@ -263,24 +278,24 @@ def path(self):
return ''.join('/' + part for part in parts)

def __eq__(self, other):
""" compares a pointer to another object
"""Compares a pointer to another object

Pointers can be compared by comparing their strings (or splitted
strings), because no two different parts can point to the same
structure in an object (eg no different number representations) """
structure in an object (eg no different number representations)
"""

if not isinstance(other, JsonPointer):
return False

return self.parts == other.parts


def __hash__(self):
return hash(tuple(self.parts))

@classmethod
def from_parts(cls, parts):
""" Constructs a JsonPointer from a list of (unescaped) paths
"""Constructs a JsonPointer from a list of (unescaped) paths

>>> JsonPointer.from_parts(['a', '~', '/', 0]).path == '/a/~0/~1/0'
True
Expand All @@ -290,22 +305,3 @@ def from_parts(cls, parts):
parts = [part.replace('/', '~1') for part in parts]
ptr = cls(''.join('/' + part for part in parts))
return ptr



def pairwise(iterable):
""" s -> (s0,s1), (s1,s2), (s2, s3), ...

>>> list(pairwise([]))
[]

>>> list(pairwise([1]))
[]

>>> list(pairwise([1, 2, 3, 4]))
[(1, 2), (2, 3), (3, 4)]
"""
a, b = tee(iterable)
for _ in b:
break
return izip(a, b)