From 4dc82340e04f1f445e072952ec03a3e9f544bc66 Mon Sep 17 00:00:00 2001 From: kafkaf- Date: Wed, 5 Nov 2014 23:06:31 +0200 Subject: [PATCH 01/75] Catching a broad exception to ensure the return of the default, added tests, fixed a bug where obj.contains(obj) returned false, added test case --- jsonpointer.py | 7 ++++--- tests.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index ee7f266..02aa594 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -166,7 +166,8 @@ def resolve(self, doc, default=_nothing): try: doc = self.walk(doc, part) - except JsonPointerException: + # Catching a broad exception to ensure the return of the default + except: if default is _nothing: raise else: @@ -215,7 +216,7 @@ def get_part(self, doc, part): else: raise JsonPointerException("Document '%s' does not support indexing, " - "must be dict/list or support __getitem__" % type(doc)) + "must be mapping/sequence or support __getitem__" % type(doc)) def walk(self, doc, part): @@ -249,7 +250,7 @@ def walk(self, doc, part): def contains(self, ptr): """ Returns True if self contains the given ptr """ - return len(self.parts) > len(ptr.parts) and \ + return len(self.parts) >= len(ptr.parts) and \ self.parts[:len(ptr.parts)] == ptr.parts @property diff --git a/tests.py b/tests.py index 2b5608e..db9c1c7 100755 --- a/tests.py +++ b/tests.py @@ -8,6 +8,7 @@ from jsonpointer import resolve_pointer, EndOfList, JsonPointerException, \ JsonPointer, set_pointer + class SpecificationTests(unittest.TestCase): """ Tests all examples from the JSON Pointer specification """ @@ -70,6 +71,7 @@ def test_round_trip(self): new_ptr = JsonPointer.from_parts(parts) self.assertEqual(ptr, new_ptr) + class ComparisonTests(unittest.TestCase): def test_eq_hash(self): @@ -94,10 +96,10 @@ def test_contains(self): p3 = JsonPointer("/b/c") self.assertTrue(p1.contains(p2)) + self.assertTrue(p1.contains(p1)) self.assertFalse(p1.contains(p3)) - class WrongInputTests(unittest.TestCase): def test_no_start_slash(self): @@ -181,11 +183,11 @@ def test_set(self): self.assertRaises(JsonPointerException, set_pointer, doc, "", 9) -class AltTypesTests(unittest.TestCase): - def test_alttypes(self): - JsonPointer.alttypes = True +class AltTypesTests(unittest.TestCase): + @classmethod + def setUpClass(cls): class Node(object): def __init__(self, name, parent=None): self.name = name @@ -217,6 +219,18 @@ def __setitem__(self, key, val): raise KeyError("Only left and right supported: %s" % key) + class mdict(object): + def __init__(self, d): + self._d = d + def __getitem__(self, item): + return self._d[item] + + cls.mdict = mdict({'root': {'1': {'2': '3'}}}) + cls.Node = Node + + + def test_alttypes(self): + Node = self.Node root = Node('root') root.set_left(Node('a')) @@ -238,6 +252,38 @@ def __setitem__(self, key, val): set_pointer(root, '/left/right', Node('AB')) self.assertEqual(resolve_pointer(root, '/left/right').name, 'AB') + def test_mock_dict_sanity(self): + doc = self.mdict + default = None + + # TODO: Generate this automatically for any given object + path_to_expected_value = { + '/root/1': {'2': '3'}, + '/root': {'1': {'2': '3'}}, + '/root/1/2': '3', + } + + for path, expected_value in iter(path_to_expected_value.items()): + self.assertEqual(resolve_pointer(doc, path, default), expected_value) + + def test_mock_dict_returns_default(self): + doc = self.mdict + default = None + + path_to_expected_value = { + '/foo': default, + '/x/y/z/d': default + } + + for path, expected_value in iter(path_to_expected_value.items()): + self.assertEqual(resolve_pointer(doc, path, default), expected_value) + + def test_mock_dict_raises_key_error(self): + doc = self.mdict + self.assertRaises(KeyError, resolve_pointer, doc, '/foo') + + + suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(SpecificationTests)) suite.addTest(unittest.makeSuite(ComparisonTests)) From 7a4071ae03c10ce4bf158d1fed52cca11e09b247 Mon Sep 17 00:00:00 2001 From: kafkaf- Date: Wed, 5 Nov 2014 23:25:17 +0200 Subject: [PATCH 02/75] Refactored jsonpointer.py, added tox.ini --- jsonpointer.py | 185 +++++++++++++++++++++++++++---------------------- 1 file changed, 102 insertions(+), 83 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index ee7f266..0405b65 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -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 @@ -30,18 +30,13 @@ # 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 ' -__version__ = '1.5' +__version__ = '1.4' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' @@ -49,43 +44,51 @@ 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]*$') - +_nothing = object() -class JsonPointerException(Exception): - pass +def set_pointer(doc, pointer, value, inplace=True): + """ + Resolves pointer against doc and sets the value of the target within doc. -class EndOfList(object): - """ Result of accessing element "-" of a list """ + With inplace set to true, doc is modified as long as pointer is not the + root. - def __init__(self, list_): - self.list_ = list_ + >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}} + >>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \ + {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} + True - def __repr__(self): - return '{cls}({lst})'.format(cls=self.__class__.__name__, - lst=repr(self.list_)) + >>> 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 - >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} + >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}} >>> resolve_pointer(obj, '') == obj True @@ -110,31 +113,54 @@ def resolve_pointer(doc, pointer, default=_nothing): pointer = JsonPointer(pointer) return pointer.resolve(doc, default) -def set_pointer(doc, pointer, value, inplace=True): + +def pairwise(iterable): """ - Resolves pointer against doc and sets the value of the target within doc. + s -> (s0,s1), (s1,s2), (s2, s3), ... - With inplace set to true, doc is modified as long as pointer is not the - root. + >>> list(pairwise([])) + [] - >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} + >>> list(pairwise([1])) + [] - >>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \ - {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} - True + >>> list(pairwise([1, 2, 3, 4])) + [(1, 2), (2, 3), (3, 4)] + """ + a, b = tee(iterable) + for _ in b: + break + return izip(a, b) - >>> set_pointer(obj, '/foo/yet%20another%20prop', 'added prop') == \ - {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}} - True +class JsonPointerException(Exception): + pass + + +class EndOfList(object): + """ + Result of accessing element "-" of a list """ - pointer = JsonPointer(pointer) - return pointer.set(doc, value, inplace) + 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 + """ + _GETITEM_SUPPORT_ERROR = """document '%s' does not support indexing, + must be mapping/sequence + or support __getitem__""" + + # Array indices must not contain: + # leading zeros, signs, spaces, decimals, etc + _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$') def __init__(self, pointer): parts = pointer.split('/') @@ -146,9 +172,10 @@ 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 @@ -158,9 +185,10 @@ 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""" + """ + Resolves the pointer against doc and returns the referenced object + """ for part in self.parts: @@ -174,11 +202,12 @@ 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: @@ -194,7 +223,9 @@ 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 @@ -204,26 +235,28 @@ 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(self._GETITEM_SUPPORT_ERROR % 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: @@ -241,20 +274,23 @@ def walk(self, doc, part): return doc[part] except IndexError: - raise JsonPointerException("index '%s' is out of bounds" % (part, )) + raise JsonPointerException("index '%s' is out of bounds" % part) else: # Object supports __getitem__, assume custom indexing return doc[part] def contains(self, ptr): - """ Returns True if self contains the given ptr """ - return len(self.parts) > len(ptr.parts) and \ - self.parts[:len(ptr.parts)] == ptr.parts + """ + Returns True if self contains the given ptr + """ + return len(self.parts) >= len(ptr.parts) and \ + 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' """ @@ -263,24 +299,26 @@ 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 @@ -290,22 +328,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) From 165004e2396419653b838cc0f23f40ef0f371ecb Mon Sep 17 00:00:00 2001 From: kafkaf- Date: Mon, 10 Nov 2014 22:04:08 +0200 Subject: [PATCH 03/75] Revreted docstrings to pep8, added a docstring summary to pairwise, reverted a change to contains methods --- jsonpointer.py | 56 ++++++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 0405b65..cfd0698 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -62,8 +62,7 @@ def set_pointer(doc, pointer, value, inplace=True): - """ - Resolves pointer against doc and sets the value of the target within doc. + """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. @@ -77,7 +76,6 @@ def set_pointer(doc, pointer, value, inplace=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 - """ pointer = JsonPointer(pointer) @@ -85,8 +83,7 @@ def set_pointer(doc, pointer, value, inplace=True): 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' }}} @@ -107,7 +104,6 @@ def resolve_pointer(doc, pointer, default=_nothing): >>> resolve_pointer(obj, '/some/path', None) == None True - """ pointer = JsonPointer(pointer) @@ -115,7 +111,8 @@ def resolve_pointer(doc, pointer, default=_nothing): def pairwise(iterable): - """ + """ Transforms a list to a list of tuples of adjacent items + s -> (s0,s1), (s1,s2), (s2, s3), ... >>> list(pairwise([])) @@ -138,9 +135,7 @@ class JsonPointerException(Exception): class EndOfList(object): - """ - Result of accessing element "-" of a list - """ + """Result of accessing element "-" of a list""" def __init__(self, list_): self.list_ = list_ @@ -151,9 +146,8 @@ def __repr__(self): 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""" + _GETITEM_SUPPORT_ERROR = """document '%s' does not support indexing, must be mapping/sequence or support __getitem__""" @@ -173,9 +167,7 @@ def __init__(self, pointer): 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 @@ -186,9 +178,7 @@ 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 - """ + """Resolves the pointer against doc and returns the referenced object""" for part in self.parts: @@ -205,9 +195,7 @@ def resolve(self, doc, default=_nothing): 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: @@ -223,9 +211,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 @@ -249,9 +235,7 @@ def get_part(self, doc, part): raise JsonPointerException(self._GETITEM_SUPPORT_ERROR % 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) @@ -281,16 +265,14 @@ def walk(self, doc, part): return doc[part] def contains(self, ptr): - """ - Returns True if self contains the given ptr - """ - return len(self.parts) >= len(ptr.parts) and \ + """Returns True if self contains the given ptr""" + + return len(self.parts) > len(ptr.parts) and \ 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' """ @@ -299,8 +281,7 @@ 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 @@ -317,8 +298,7 @@ def __hash__(self): @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 From 6e64a22dc7d8564ceaa399bb2e97765e2e70d270 Mon Sep 17 00:00:00 2001 From: kafkaf- Date: Mon, 10 Nov 2014 22:11:20 +0200 Subject: [PATCH 04/75] Removed redundent conts, reverted the version number, reverted formatting of a part --- jsonpointer.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index cfd0698..7e94705 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,7 +36,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.4' +__version__ = '1.5' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' @@ -148,10 +148,6 @@ def __repr__(self): class JsonPointer(object): """A JSON Pointer that can reference parts of an JSON document""" - _GETITEM_SUPPORT_ERROR = """document '%s' does not support indexing, - must be mapping/sequence - or support __getitem__""" - # Array indices must not contain: # leading zeros, signs, spaces, decimals, etc _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$') @@ -232,7 +228,8 @@ def get_part(self, doc, part): return part else: - raise JsonPointerException(self._GETITEM_SUPPORT_ERROR % 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""" @@ -258,7 +255,7 @@ def walk(self, doc, part): return doc[part] except IndexError: - raise JsonPointerException("index '%s' is out of bounds" % part) + raise JsonPointerException("index '%s' is out of bounds" % (part, )) else: # Object supports __getitem__, assume custom indexing From ec26151db15c241e29c7730dc12657434370ff67 Mon Sep 17 00:00:00 2001 From: kafkaf- Date: Fri, 14 Nov 2014 16:05:00 +0200 Subject: [PATCH 05/75] Removed unrelated changes to the contains method, reverted the exception handle --- jsonpointer.py | 5 ++--- tests.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 02aa594..b3f0362 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -166,8 +166,7 @@ def resolve(self, doc, default=_nothing): try: doc = self.walk(doc, part) - # Catching a broad exception to ensure the return of the default - except: + except JsonPointerException: if default is _nothing: raise else: @@ -250,7 +249,7 @@ def walk(self, doc, part): def contains(self, ptr): """ Returns True if self contains the given ptr """ - return len(self.parts) >= len(ptr.parts) and \ + return len(self.parts) > len(ptr.parts) and \ self.parts[:len(ptr.parts)] == ptr.parts @property diff --git a/tests.py b/tests.py index db9c1c7..cc81dc6 100755 --- a/tests.py +++ b/tests.py @@ -96,7 +96,6 @@ def test_contains(self): p3 = JsonPointer("/b/c") self.assertTrue(p1.contains(p2)) - self.assertTrue(p1.contains(p1)) self.assertFalse(p1.contains(p3)) From 8a371c786fc77139be2a325c05e0a5c95ea83246 Mon Sep 17 00:00:00 2001 From: kafkaf- Date: Fri, 14 Nov 2014 16:06:20 +0200 Subject: [PATCH 06/75] Removed the setUpClass method to support py26 --- tests.py | 80 +++++++++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/tests.py b/tests.py index cc81dc6..3bec626 100755 --- a/tests.py +++ b/tests.py @@ -185,47 +185,45 @@ def test_set(self): class AltTypesTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - class Node(object): - def __init__(self, name, parent=None): - self.name = name - self.parent = parent - self.left = None - self.right = None - - def set_left(self, node): - node.parent = self - self.left = node - - def set_right(self, node): - node.parent = self - self.right = node - - def __getitem__(self, key): - if key == 'left': - return self.left - if key == 'right': - return self.right - - raise KeyError("Only left and right supported") - - def __setitem__(self, key, val): - if key == 'left': - return self.set_left(val) - if key == 'right': - return self.set_right(val) - - raise KeyError("Only left and right supported: %s" % key) - - class mdict(object): - def __init__(self, d): - self._d = d - def __getitem__(self, item): - return self._d[item] - - cls.mdict = mdict({'root': {'1': {'2': '3'}}}) - cls.Node = Node + class Node(object): + def __init__(self, name, parent=None): + self.name = name + self.parent = parent + self.left = None + self.right = None + + def set_left(self, node): + node.parent = self + self.left = node + + def set_right(self, node): + node.parent = self + self.right = node + + def __getitem__(self, key): + if key == 'left': + return self.left + if key == 'right': + return self.right + + raise KeyError("Only left and right supported") + + def __setitem__(self, key, val): + if key == 'left': + return self.set_left(val) + if key == 'right': + return self.set_right(val) + + raise KeyError("Only left and right supported: %s" % key) + + class mdict(object): + def __init__(self, d): + self._d = d + def __getitem__(self, item): + return self._d[item] + + mdict = mdict({'root': {'1': {'2': '3'}}}) + Node = Node def test_alttypes(self): From 4c5cb493f2a8e41fd05b9b25df419701e215812a Mon Sep 17 00:00:00 2001 From: kafkaf- Date: Fri, 14 Nov 2014 16:18:39 +0200 Subject: [PATCH 07/75] Using the same handling for the case in which the doc is a mapping or an object that support __getitem__(but not a sequence), Updated the tests --- jsonpointer.py | 18 +++++++----------- tests.py | 3 ++- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index b3f0362..06d2cb7 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -225,15 +225,7 @@ def walk(self, doc, part): assert (type(doc) in (dict, list) or hasattr(doc, '__getitem__')), "invalid document type %s" % (type(doc),) - if isinstance(doc, Mapping): - try: - return doc[part] - - except KeyError: - raise JsonPointerException("member '%s' not found in %s" % (part, doc)) - - elif isinstance(doc, Sequence): - + if isinstance(doc, Sequence): if part == '-': return EndOfList(doc) @@ -243,10 +235,14 @@ def walk(self, doc, part): except IndexError: raise JsonPointerException("index '%s' is out of bounds" % (part, )) - else: - # Object supports __getitem__, assume custom indexing + # Else the object is a mapping or supports __getitem__(so assume custom indexing) + try: return doc[part] + except KeyError: + raise JsonPointerException("member '%s' not found in %s" % (part, doc)) + + def contains(self, ptr): """ Returns True if self contains the given ptr """ return len(self.parts) > len(ptr.parts) and \ diff --git a/tests.py b/tests.py index 3bec626..246b507 100755 --- a/tests.py +++ b/tests.py @@ -277,7 +277,8 @@ def test_mock_dict_returns_default(self): def test_mock_dict_raises_key_error(self): doc = self.mdict - self.assertRaises(KeyError, resolve_pointer, doc, '/foo') + self.assertRaises(JsonPointerException, resolve_pointer, doc, '/foo') + self.assertRaises(JsonPointerException, resolve_pointer, doc, '/root/1/2/3/4') From 2ca270f01e7667366800e5648c2561d7072dd0e9 Mon Sep 17 00:00:00 2001 From: kafkaf- Date: Fri, 14 Nov 2014 16:19:32 +0200 Subject: [PATCH 08/75] Removed redundent assertion(part of it) --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index 06d2cb7..e4c407d 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -223,7 +223,7 @@ def walk(self, doc, 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, Sequence): if part == '-': From 3f460a54c2004802cc96b1c619b5edc011ec555b Mon Sep 17 00:00:00 2001 From: Alexander Dutton Date: Sun, 15 Feb 2015 19:45:43 +0000 Subject: [PATCH 09/75] Simplify `walk` method. No need to look before we leap. --- jsonpointer.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 0f115d7..85afe00 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -223,29 +223,14 @@ def walk(self, doc, part): part = self.get_part(doc, part) - assert (type(doc) in (dict, list) or hasattr(doc, '__getitem__')), "invalid document type %s" % (type(doc),) - - if isinstance(doc, Mapping): - try: - return doc[part] - - except KeyError: - raise JsonPointerException("member '%s' not found in %s" % (part, doc)) - - elif isinstance(doc, Sequence): - - if part == '-': - return EndOfList(doc) - - try: - return doc[part] - - except IndexError: - raise JsonPointerException("index '%s' is out of bounds" % (part, )) - - else: - # Object supports __getitem__, assume custom indexing + if part == '-' and isinstance(doc, Sequence): + return EndOfList(doc) + try: return doc[part] + except KeyError: + raise JsonPointerException("member '%s' not found in %s" % (part, doc)) + except IndexError: + raise JsonPointerException("index '%s' is out of bounds" % (part, )) def contains(self, ptr): """Returns True if self contains the given ptr""" From 76b542b3fbfb3ef2a1f9fd289e8a74765c5cb53f Mon Sep 17 00:00:00 2001 From: Alexander Dutton Date: Sun, 15 Feb 2015 20:00:41 +0000 Subject: [PATCH 10/75] Reduce number of isinstance() calls in get_part --- jsonpointer.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 85afe00..29a71f5 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -196,27 +196,14 @@ def set(self, doc, value, inplace=True): def get_part(self, doc, part): """ Returns the next step in the correct type """ - if isinstance(doc, Mapping): - return part - - elif isinstance(doc, Sequence): - + if isinstance(doc, Sequence): if part == '-': return part - if not RE_ARRAY_INDEX.match(str(part)): raise JsonPointerException("'%s' is not a valid list index" % (part, )) - return int(part) - - elif hasattr(doc, '__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)) - + return part def walk(self, doc, part): """ Walks one step in doc and returns the referenced part """ @@ -231,6 +218,10 @@ def walk(self, doc, part): raise JsonPointerException("member '%s' not found in %s" % (part, doc)) except IndexError: raise JsonPointerException("index '%s' is out of bounds" % (part, )) + except TypeError: + raise JsonPointerException("Document '%s' does not support indexing, " + "must be dict/list or support __getitem__" % type(doc)) + def contains(self, ptr): """Returns True if self contains the given ptr""" From c4372cdb3829138a1e5287e1c4ef8dc9c6c5b5a5 Mon Sep 17 00:00:00 2001 From: Alexander Dutton Date: Sun, 15 Feb 2015 20:06:05 +0000 Subject: [PATCH 11/75] Optimize in get_part for the common cases of doc being dict or list --- jsonpointer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index 29a71f5..3e8b31c 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -196,7 +196,12 @@ def set(self, doc, value, inplace=True): def get_part(self, doc, part): """ Returns the next step in the correct type """ - if isinstance(doc, Sequence): + # Optimize for common cases of doc being a dict or list, but not a + # sub-class (because isinstance() is far slower) + ptype = type(doc) + if ptype == dict: + return part + if ptype == list or isinstance(doc, Sequence): if part == '-': return part if not RE_ARRAY_INDEX.match(str(part)): From 9653d1be314e14ae00d86b13807d2d22c0d55e98 Mon Sep 17 00:00:00 2001 From: Alexander Dutton Date: Thu, 19 Feb 2015 00:22:03 +0000 Subject: [PATCH 12/75] Re-factor resolve and to_last to not make lots of function calls. Also means that isinstance(..., Sequence) doesn't get called twice for non-list sequences (once in walk, and one in get_part) --- jsonpointer.py | 53 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 3e8b31c..72e5f93 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -153,28 +153,53 @@ def to_last(self, doc): if not self.parts: return doc, None - for part in self.parts[:-1]: - doc = self.walk(doc, part) - - return doc, self.get_part(doc, self.parts[-1]) + doc = self.resolve(doc, parts=self.parts[:-1]) + last = self.parts[-1] + ptype = type(doc) + if ptype == dict: + pass + elif ptype == list or isinstance(doc, Sequence): + if not RE_ARRAY_INDEX.match(str(last)): + raise JsonPointerException("'%s' is not a valid list index" % (last, )) + last = int(last) + return doc, last - def resolve(self, doc, default=_nothing): + def resolve(self, doc, default=_nothing, parts=None): """Resolves the pointer against doc and returns the referenced object""" + if parts is None: + parts = self.parts - for part in self.parts: - - try: - doc = self.walk(doc, part) - except JsonPointerException: - if default is _nothing: - raise + try: + for part in parts: + ptype = type(doc) + if ptype == dict: + doc = doc[part] + elif ptype == list or isinstance(doc, Sequence): + if part == '-': + doc = EndOfList(doc) + else: + if not RE_ARRAY_INDEX.match(str(part)): + raise JsonPointerException("'%s' is not a valid list index" % (part, )) + doc = doc[int(part)] else: - return default + doc = doc[part] + except KeyError: + if default is not _nothing: + return default + raise JsonPointerException("member '%s' not found in %s" % (part, doc)) + except IndexError: + if default is not _nothing: + return default + raise JsonPointerException("index '%s' is out of bounds" % (part, )) + except TypeError: + if default is not _nothing: + return default + raise JsonPointerException("Document '%s' does not support indexing, " + "must be dict/list or support __getitem__" % type(doc)) return doc - get = resolve def set(self, doc, value, inplace=True): From 9a276f9974988be0feee447aeadeb36ade7ead60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 18 Sep 2015 16:26:01 +0200 Subject: [PATCH 13/75] Add Travis tests for Python 3.5 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 822fb93..9719f53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.2" - "3.3" - "3.4" + - "3.5" - "pypy" - "pypy3" From 309468887e2726d6ba76be04038b6996485907ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Wed, 28 Oct 2015 20:05:40 +0100 Subject: [PATCH 14/75] Add Python 3.5 to trove classifiers --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 6e683c8..2558c16 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', From de059035de9c268d4d964f248d0f82ecfb4bb30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Wed, 28 Oct 2015 20:05:54 +0100 Subject: [PATCH 15/75] Drop support for Python 3.2 coverage.py does not support it anymore and it is not worth to work around it. --- .travis.yml | 1 - setup.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9719f53..9065721 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python python: - "2.6" - "2.7" - - "3.2" - "3.3" - "3.4" - "3.5" diff --git a/setup.py b/setup.py index 2558c16..2c65e44 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,6 @@ 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', From fa17b619239afeea63ca1eb89a36ba17ce9134a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Wed, 28 Oct 2015 20:06:37 +0100 Subject: [PATCH 16/75] bump version to 1.10 --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index 718ea9a..eea100c 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -41,7 +41,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.9' +__version__ = '1.10' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' From 45b04c77bbc8782cd522f839a9a823caf1fe0e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 17:30:54 +0200 Subject: [PATCH 17/75] Extract (un)escape function --- jsonpointer.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index eea100c..d79bb19 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -143,8 +143,7 @@ def __init__(self, pointer): 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] + parts = [unescape(part) for part in parts] self.parts = parts @@ -262,8 +261,7 @@ def path(self): >>> ptr = JsonPointer('/~0/0/~1').path == '/~0/0/~1' """ - parts = [part.replace('~', '~0') for part in self.parts] - parts = [part.replace('/', '~1') for part in parts] + parts = [escape(part) for part in self.parts] return ''.join('/' + part for part in parts) def __eq__(self, other): @@ -289,9 +287,7 @@ def from_parts(cls, parts): >>> JsonPointer.from_parts(['a', '~', '/', 0]).path == '/a/~0/~1/0' True """ - parts = [str(part) for part in parts] - parts = [part.replace('~', '~0') for part in parts] - parts = [part.replace('/', '~1') for part in parts] + parts = [escape(str(part)) for part in parts] ptr = cls(''.join('/' + part for part in parts)) return ptr @@ -313,3 +309,10 @@ def pairwise(iterable): for _ in b: break return izip(a, b) + + +def escape(s): + return s.replace('~', '~0').replace('/', '~1') + +def unescape(s): + return s.replace('~1', '/').replace('~0', '~') From c237196e29d4ed46393577dd660ec0aa6401c163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 17:44:27 +0200 Subject: [PATCH 18/75] Add support for Python 3.6 --- .travis.yml | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9065721..389dc95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.3" - "3.4" - "3.5" + - "3.6" - "pypy" - "pypy3" diff --git a/setup.py b/setup.py index 2c65e44..f5e850c 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', From 8c64674098e539ae2bf113115b5fdad7fd424dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 18:57:10 +0200 Subject: [PATCH 19/75] Resolve merge conflicts, style issues --- jsonpointer.py | 123 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 37 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 9a6a613..c411682 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -168,23 +168,62 @@ def to_last(self, doc): if not self.parts: return doc, None - for part in self.parts[:-1]: - doc = self.walk(doc, part) + doc = self.resolve(doc, parts=self.parts[:-1]) + last = self.parts[-1] + ptype = type(doc) + if ptype == dict: + pass + elif ptype == list or isinstance(doc, Sequence): + if not self._RE_ARRAY_INDEX.match(str(last)): + raise JsonPointerException( + "'%s' is not a valid list index" % (last, ) + ) + last = int(last) + + return doc, last + + def resolve(self, doc, default=_nothing, parts=None): + """ Resolves the pointer against doc, returns the referenced object """ + if parts is None: + parts = self.parts - 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""" - - for part in self.parts: - - try: - doc = self.walk(doc, part) - except JsonPointerException: - if default is _nothing: - raise + try: + for part in parts: + ptype = type(doc) + if ptype == dict: + doc = doc[part] + elif ptype == list or isinstance(doc, Sequence): + if part == '-': + doc = EndOfList(doc) + else: + if not self._RE_ARRAY_INDEX.match(str(part)): + raise JsonPointerException( + "'%s' is not a valid list index" % (part, ) + ) + doc = doc[int(part)] else: - return default + doc = doc[part] + except KeyError: + if default is not _nothing: + return default + raise JsonPointerException( + "member '%s' not found in %s" % (part, doc) + ) + + except IndexError: + if default is not _nothing: + return default + raise JsonPointerException( + "index '%s' is out of bounds" % (part, ) + ) + + except TypeError: + if default is not _nothing: + return default + raise JsonPointerException( + "Document '%s' does not support indexing, must be dict/list " + "or support __getitem__" % type(doc) + ) return doc @@ -209,17 +248,19 @@ def set(self, doc, value, inplace=True): def get_part(self, doc, part): """Returns the next step in the correct type""" - if isinstance(doc, Mapping): + # Optimize for common cases of doc being a dict or list, but not a + # sub-class (because isinstance() is far slower) + ptype = type(doc) + if ptype == dict: return part - - elif isinstance(doc, Sequence): - + if ptype == list or isinstance(doc, Sequence): if part == '-': return part if not self._RE_ARRAY_INDEX.match(str(part)): - raise JsonPointerException("'%s' is not a valid sequence index" % part) - + raise JsonPointerException( + "'%s' is not a valid list index" % (part, ) + ) return int(part) elif hasattr(doc, '__getitem__'): @@ -228,34 +269,42 @@ def get_part(self, doc, part): return part else: - raise JsonPointerException("Document '%s' does not support indexing, " - "must be mapping/sequence 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 """ part = self.get_part(doc, part) - assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),) - - if isinstance(doc, Sequence): - if part == '-': - return EndOfList(doc) - - try: - return doc[part] + if part == '-' and isinstance(doc, Sequence): + return EndOfList(doc) - except IndexError: - raise JsonPointerException("index '%s' is out of bounds" % (part, )) - - # Else the object is a mapping or supports __getitem__(so assume custom indexing) try: return doc[part] except KeyError: - raise JsonPointerException("member '%s' not found in %s" % (part, doc)) + raise JsonPointerException( + "member '%s' not found in %s" % (part, doc) + ) + + except IndexError: + raise JsonPointerException( + "index '%s' is out of bounds" % (part, ) + ) + except TypeError: + raise JsonPointerException( + "Document '%s' does not support indexing, must be dict/list " + "or support __getitem__" % type(doc) + ) + + except KeyError: + raise JsonPointerException( + "member '%s' not found in %s" % (part, doc) + ) def contains(self, ptr): """ Returns True if self contains the given ptr """ From cace88ea30df6d4ca4d745f8ba526654689572d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 19:04:22 +0200 Subject: [PATCH 20/75] Bump version to 1.11 --- jsonpointer.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index c411682..5d41b58 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,25 +36,22 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.10' +__version__ = '1.11' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' try: from urllib import unquote - from itertools import izip str = unicode 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 @@ -111,26 +108,6 @@ def resolve_pointer(doc, pointer, default=_nothing): return pointer.resolve(doc, default) -def pairwise(iterable): - """ Transforms a list to a list of tuples of adjacent items - - 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) - - class JsonPointerException(Exception): pass From a8d9a99d9d0d0d3f6500ccbf581178924b74432b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 19:06:16 +0200 Subject: [PATCH 21/75] Add setup.cfg for universal wheels --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 From 9572f604700a58188aa6047b38db4f77ab2a5610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 19:08:23 +0200 Subject: [PATCH 22/75] Bump requirement for pandoc --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fd0fd6c..51e335b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ wheel -pandoc==1.0.0-alpha.3 +pandoc==1.0.0b2 From 85ff9a1b2165edce7baae2ddbf60a2a7f2557ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 19:19:31 +0200 Subject: [PATCH 23/75] Add config for automatic deployment from Travis-CI --- .travis.yml | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 389dc95..7c737e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,25 @@ language: python python: - - "2.6" - - "2.7" - - "3.3" - - "3.4" - - "3.5" - - "3.6" - - "pypy" - - "pypy3" - +- '2.6' +- '2.7' +- '3.3' +- '3.4' +- '3.5' +- '3.6' +- pypy +- pypy3 install: - - travis_retry pip install coveralls - +- travis_retry pip install coveralls script: - - coverage run --source=jsonpointer tests.py - +- coverage run --source=jsonpointer tests.py after_script: - - coveralls - +- coveralls sudo: false +deploy: + provider: pypi + user: skoegl + password: + secure: g+81X6n8etmQQ9ZbxoSx/OoXsDHBwgoBlesnrYP4RBHLoez3wBbI2w1tu7Uce2irWMmkoFIpQHguXxOqHVoGzx18s1njp9/1fyrJ3f6gYbcQrUnwRFjYGcIc0TUSTI9dLpnRKCp9glNwIvPx7n6/5avZIaXveLU9j4DIm1xA1ZM= + on: + tags: true + distributions: sdist bdist_wheel From 244bed3e827166dcffaf5fec5abb16736fe9d8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 19:40:12 +0200 Subject: [PATCH 24/75] Revert optimizations due to compatibility issues with jsonpatch --- .travis.yml | 34 +++++----- jsonpointer.py | 148 ++++++++++++++++++------------------------- requirements-dev.txt | 2 +- 3 files changed, 77 insertions(+), 107 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7c737e4..389dc95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,21 @@ language: python python: -- '2.6' -- '2.7' -- '3.3' -- '3.4' -- '3.5' -- '3.6' -- pypy -- pypy3 + - "2.6" + - "2.7" + - "3.3" + - "3.4" + - "3.5" + - "3.6" + - "pypy" + - "pypy3" + install: -- travis_retry pip install coveralls + - travis_retry pip install coveralls + script: -- coverage run --source=jsonpointer tests.py + - coverage run --source=jsonpointer tests.py + after_script: -- coveralls + - coveralls + sudo: false -deploy: - provider: pypi - user: skoegl - password: - secure: g+81X6n8etmQQ9ZbxoSx/OoXsDHBwgoBlesnrYP4RBHLoez3wBbI2w1tu7Uce2irWMmkoFIpQHguXxOqHVoGzx18s1njp9/1fyrJ3f6gYbcQrUnwRFjYGcIc0TUSTI9dLpnRKCp9glNwIvPx7n6/5avZIaXveLU9j4DIm1xA1ZM= - on: - tags: true - distributions: sdist bdist_wheel diff --git a/jsonpointer.py b/jsonpointer.py index 5d41b58..9a6a613 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,22 +36,25 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.11' +__version__ = '1.10' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' try: from urllib import unquote + from itertools import izip str = unicode 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 @@ -108,6 +111,26 @@ def resolve_pointer(doc, pointer, default=_nothing): return pointer.resolve(doc, default) +def pairwise(iterable): + """ Transforms a list to a list of tuples of adjacent items + + 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) + + class JsonPointerException(Exception): pass @@ -145,62 +168,23 @@ def to_last(self, doc): if not self.parts: return doc, None - doc = self.resolve(doc, parts=self.parts[:-1]) - last = self.parts[-1] - ptype = type(doc) - if ptype == dict: - pass - elif ptype == list or isinstance(doc, Sequence): - if not self._RE_ARRAY_INDEX.match(str(last)): - raise JsonPointerException( - "'%s' is not a valid list index" % (last, ) - ) - last = int(last) - - return doc, last - - def resolve(self, doc, default=_nothing, parts=None): - """ Resolves the pointer against doc, returns the referenced object """ - if parts is None: - parts = self.parts + for part in self.parts[:-1]: + doc = self.walk(doc, part) - try: - for part in parts: - ptype = type(doc) - if ptype == dict: - doc = doc[part] - elif ptype == list or isinstance(doc, Sequence): - if part == '-': - doc = EndOfList(doc) - else: - if not self._RE_ARRAY_INDEX.match(str(part)): - raise JsonPointerException( - "'%s' is not a valid list index" % (part, ) - ) - doc = doc[int(part)] + 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""" + + for part in self.parts: + + try: + doc = self.walk(doc, part) + except JsonPointerException: + if default is _nothing: + raise else: - doc = doc[part] - except KeyError: - if default is not _nothing: - return default - raise JsonPointerException( - "member '%s' not found in %s" % (part, doc) - ) - - except IndexError: - if default is not _nothing: - return default - raise JsonPointerException( - "index '%s' is out of bounds" % (part, ) - ) - - except TypeError: - if default is not _nothing: - return default - raise JsonPointerException( - "Document '%s' does not support indexing, must be dict/list " - "or support __getitem__" % type(doc) - ) + return default return doc @@ -225,19 +209,17 @@ def set(self, doc, value, inplace=True): def get_part(self, doc, part): """Returns the next step in the correct type""" - # Optimize for common cases of doc being a dict or list, but not a - # sub-class (because isinstance() is far slower) - ptype = type(doc) - if ptype == dict: + if isinstance(doc, Mapping): return part - if ptype == list or isinstance(doc, Sequence): + + elif isinstance(doc, Sequence): + if part == '-': return part if not self._RE_ARRAY_INDEX.match(str(part)): - raise JsonPointerException( - "'%s' is not a valid list index" % (part, ) - ) + raise JsonPointerException("'%s' is not a valid sequence index" % part) + return int(part) elif hasattr(doc, '__getitem__'): @@ -246,42 +228,34 @@ def get_part(self, doc, part): return part else: - raise JsonPointerException( - "Document '%s' does not support indexing, must be " - "mapping/sequence 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 """ part = self.get_part(doc, part) - if part == '-' and isinstance(doc, Sequence): - return EndOfList(doc) + assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),) - try: - return doc[part] + if isinstance(doc, Sequence): + if part == '-': + return EndOfList(doc) - except KeyError: - raise JsonPointerException( - "member '%s' not found in %s" % (part, doc) - ) + try: + return doc[part] - except IndexError: - raise JsonPointerException( - "index '%s' is out of bounds" % (part, ) - ) + except IndexError: + raise JsonPointerException("index '%s' is out of bounds" % (part, )) - except TypeError: - raise JsonPointerException( - "Document '%s' does not support indexing, must be dict/list " - "or support __getitem__" % type(doc) - ) + # Else the object is a mapping or supports __getitem__(so assume custom indexing) + try: + return doc[part] except KeyError: - raise JsonPointerException( - "member '%s' not found in %s" % (part, doc) - ) + raise JsonPointerException("member '%s' not found in %s" % (part, doc)) + def contains(self, ptr): """ Returns True if self contains the given ptr """ diff --git a/requirements-dev.txt b/requirements-dev.txt index 51e335b..fd0fd6c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ wheel -pandoc==1.0.0b2 +pandoc==1.0.0-alpha.3 From 3c8562c9e6b5c362d79dc733fde4a87df3ce8ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Sep 2017 19:40:58 +0200 Subject: [PATCH 25/75] Bump version to 1.12 --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index 9a6a613..8592508 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,7 +36,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.10' +__version__ = '1.12' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' From bf33bfa724c808b83df47b20f6fc5070bb3251c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Mon, 2 Oct 2017 11:15:26 +0200 Subject: [PATCH 26/75] For pypandoc requirement for release procedure --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fd0fd6c..0dc8f0c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ wheel -pandoc==1.0.0-alpha.3 +pypandoc==1.4 From 33d1513ad8453d345f0d7c6b9c350c25c9fddcd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Mon, 2 Oct 2017 11:16:03 +0200 Subject: [PATCH 27/75] Rename license, fixes #23 --- COPYING => LICENSE.txt | 0 MANIFEST.in | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename COPYING => LICENSE.txt (100%) diff --git a/COPYING b/LICENSE.txt similarity index 100% rename from COPYING rename to LICENSE.txt diff --git a/MANIFEST.in b/MANIFEST.in index dcb2e4a..0f9b9f6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include AUTHORS -include COPYING +include LICENSE.txt include README.md include tests.py From c118415bed27c8c0dcb11d045ca406177b7481ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 26 Oct 2017 13:16:34 +0200 Subject: [PATCH 28/75] Pointers should not be assumed urlencoded, fix #22 --- jsonpointer.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 8592508..33095f7 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -42,11 +42,9 @@ try: - from urllib import unquote from itertools import izip str = unicode except ImportError: # Python 3 - from urllib.parse import unquote izip = zip try: @@ -74,9 +72,14 @@ def set_pointer(doc, pointer, value, inplace=True): {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} True - >>> set_pointer(obj, '/foo/yet%20another%20prop', 'added prop') == \ + >>> set_pointer(obj, '/foo/yet another prop', 'added prop') == \ {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}} True + + >>> obj = {'foo': {}} + >>> set_pointer(obj, '/foo/a%20b', 'x') == \ + {'foo': {'a%20b': 'x' }} + True """ pointer = JsonPointer(pointer) @@ -86,7 +89,7 @@ def set_pointer(doc, pointer, value, inplace=True): 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' }}} + >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}, 'a%20b': 1, 'c d': 2} >>> resolve_pointer(obj, '') == obj True @@ -94,10 +97,10 @@ def resolve_pointer(doc, pointer, default=_nothing): >>> resolve_pointer(obj, '/foo') == obj['foo'] True - >>> resolve_pointer(obj, '/foo/another%20prop') == obj['foo']['another prop'] + >>> resolve_pointer(obj, '/foo/another prop') == obj['foo']['another prop'] True - >>> resolve_pointer(obj, '/foo/another%20prop/baz') == obj['foo']['another prop']['baz'] + >>> resolve_pointer(obj, '/foo/another prop/baz') == obj['foo']['another prop']['baz'] True >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0] @@ -105,6 +108,18 @@ def resolve_pointer(doc, pointer, default=_nothing): >>> resolve_pointer(obj, '/some/path', None) == None True + + >>> resolve_pointer(obj, '/a b', None) == None + True + + >>> resolve_pointer(obj, '/a%20b') == 1 + True + + >>> resolve_pointer(obj, '/c d') == 2 + True + + >>> resolve_pointer(obj, '/c%20d', None) == None + True """ pointer = JsonPointer(pointer) @@ -158,7 +173,6 @@ def __init__(self, pointer): if parts.pop(0) != '': raise JsonPointerException('location must starts with /') - parts = map(unquote, parts) parts = [unescape(part) for part in parts] self.parts = parts From 7854cd70958f4248bbebc689a960a4a41807b824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 26 Oct 2017 13:22:40 +0200 Subject: [PATCH 29/75] Also test on Python development versions --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 389dc95..6396f6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,9 @@ python: - "3.4" - "3.5" - "3.6" + - "3.6-dev" + - "3.7-dev" + - "nightly" - "pypy" - "pypy3" From d53e6576c27d8b677d0b715741ffeebff2d312d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 26 Oct 2017 13:24:11 +0200 Subject: [PATCH 30/75] Reformat .travis.yml with travis commandline tool --- .travis.yml | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6396f6f..1650f36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,20 @@ language: python python: - - "2.6" - - "2.7" - - "3.3" - - "3.4" - - "3.5" - - "3.6" - - "3.6-dev" - - "3.7-dev" - - "nightly" - - "pypy" - - "pypy3" - +- '2.6' +- '2.7' +- '3.3' +- '3.4' +- '3.5' +- '3.6' +- 3.6-dev +- 3.7-dev +- nightly +- pypy +- pypy3 install: - - travis_retry pip install coveralls - +- travis_retry pip install coveralls script: - - coverage run --source=jsonpointer tests.py - +- coverage run --source=jsonpointer tests.py after_script: - - coveralls - +- coveralls sudo: false From 4f2383b6d7888bfadaf2e3ca6633426f3f7390c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 26 Oct 2017 13:25:03 +0200 Subject: [PATCH 31/75] Enable automatic deployments from Travis-CI --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1650f36..5508e2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,3 +18,11 @@ script: after_script: - coveralls sudo: false +deploy: + provider: pypi + user: skoegl + password: + secure: bKET/1sK+uWetPM3opPTt4qHHfZ6bMcjuLGe3Z/EUfNnGazcbDezy9CHJSqofuMXynF3xc8yluEojjfaqos3Ge/Y4o8pdFMY8ABp8KkxMnAJYGtYzbneSHgdgxPKsmdcUMVtIfioqkeNJTJClWUhRikWSlpKZ7TtkK4AmWtKNwc= + on: + tags: true + distributions: sdist bdist_wheel From 13292bbf2ab9c3d7ed6dbffc6a2c095c9f6decd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 26 Oct 2017 13:29:00 +0200 Subject: [PATCH 32/75] Install dev requirements before deploy from Travis-CI --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5508e2d..8e7b5d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ script: after_script: - coveralls sudo: false +before_deploy: +- pip install -r requirements-dev.txt deploy: provider: pypi user: skoegl From 50d50051c30004b2e7b69e34e610e86fbf9114f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 26 Oct 2017 13:31:50 +0200 Subject: [PATCH 33/75] Bump version to 1.13 --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index 33095f7..097627f 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,7 +36,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.12' +__version__ = '1.13' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' From ba2af26490f16d1e4f7bf57791af2ade8e90fcb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 26 Oct 2017 13:45:08 +0200 Subject: [PATCH 34/75] Install pandoc for deployment https://docs.travis-ci.com/user/installing-dependencies/#Installing-Packages-on-Container-Based-Infrastructure --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8e7b5d0..bd9a8a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,10 @@ script: after_script: - coveralls sudo: false +addons: + apt: + packages: + - pandoc before_deploy: - pip install -r requirements-dev.txt deploy: From 0d30f12f3f64dbe58bd816597856fb34d9e31fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 28 Oct 2017 12:46:41 +0200 Subject: [PATCH 35/75] Perform input validation in JsonPoiner --- jsonpointer.py | 8 ++++++++ tests.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/jsonpointer.py b/jsonpointer.py index 097627f..fd54569 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -167,8 +167,16 @@ class JsonPointer(object): # Array indices must not contain: # leading zeros, signs, spaces, decimals, etc _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$') + _RE_INVALID_ESCAPE = re.compile('(~[^01]|~$)') def __init__(self, pointer): + + # validate escapes + invalid_escape = self._RE_INVALID_ESCAPE.search(pointer) + if invalid_escape: + raise JsonPointerException('Found invalid escape {0}'.format( + invalid_escape.group())) + parts = pointer.split('/') if parts.pop(0) != '': raise JsonPointerException('location must starts with /') diff --git a/tests.py b/tests.py index 21483eb..54ca436 100755 --- a/tests.py +++ b/tests.py @@ -126,6 +126,12 @@ def test_oob(self): doc = [0, 1, 2] self.assertRaises(JsonPointerException, resolve_pointer, doc, '/10') + def test_trailing_escape(self): + self.assertRaises(JsonPointerException, JsonPointer, '/foo/bar~') + + def test_invalid_escape(self): + self.assertRaises(JsonPointerException, JsonPointer, '/foo/bar~2') + class ToLastTests(unittest.TestCase): From bae64f61ec5f1057342faf19b89d5bd571b038da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Mon, 30 Oct 2017 20:45:38 +0100 Subject: [PATCH 36/75] Bump version to 1.14 --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index fd54569..285fc0c 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,7 +36,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.13' +__version__ = '1.14' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' From 0fb516fa310db0eb4900a7b529c64bde290390bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 31 Dec 2017 12:00:35 +0100 Subject: [PATCH 37/75] Accept pointer as argument in commandline utility Fixes #25 --- bin/jsonpointer | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/bin/jsonpointer b/bin/jsonpointer index 1d49fae..d577d01 100755 --- a/bin/jsonpointer +++ b/bin/jsonpointer @@ -12,8 +12,17 @@ import argparse parser = argparse.ArgumentParser( description='Resolve a JSON pointer on JSON files') -parser.add_argument('POINTER', type=argparse.FileType('r'), - help='File containing a JSON pointer expression') + +# Accept pointer as argument or as file +ptr_group = parser.add_mutually_exclusive_group(required=True) + +ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r'), + nargs='?', + help='File containing a JSON pointer expression') + +ptr_group.add_argument('POINTER', type=str, nargs='?', + help='A JSON pointer expression') + parser.add_argument('FILE', type=argparse.FileType('r'), nargs='+', help='Files for which the pointer should be resolved') parser.add_argument('--indent', type=int, default=None, @@ -29,10 +38,24 @@ def main(): sys.exit(1) +def parse_pointer(args): + if args.POINTER: + ptr = args.POINTER + elif args.pointer_file: + ptr = args.pointer_file.read().strip() + else: + parser.print_usage() + sys.exit(1) + + return ptr + + def resolve_files(): """ Resolve a JSON pointer on JSON files """ args = parser.parse_args() - ptr = json.load(args.POINTER) + + ptr = parse_pointer(args) + for f in args.FILE: doc = json.load(f) try: From 76bf9a029fe8c64541a661909e617a7abf31998b Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 15 Jan 2018 16:46:27 +0200 Subject: [PATCH 38/75] Fix badges * pypip.in is down: https://github.com/badges/pypipins/issues/39 * PyPI download badges no longer available: https://github.com/badges/shields/issues/716 * Fix some typos --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cae175d..5248dc8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,22 @@ -python-json-pointer [![Build Status](https://secure.travis-ci.org/stefankoegl/python-json-pointer.png?branch=master)](https://travis-ci.org/stefankoegl/python-json-pointer) [![Coverage Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.png?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master) ![Downloads](https://pypip.in/d/jsonpointer/badge.png) ![Version](https://pypip.in/v/jsonpointer/badge.png) +python-json-pointer =================== +[![PyPI version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) +[![Build Status](https://travis-ci.org/stefankoegl/python-json-pointer.png?branch=master)](https://travis-ci.org/stefankoegl/python-json-pointer) +[![Coverage Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.png?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master) + + Resolve JSON Pointers in Python ------------------------------- Library to resolve JSON Pointers according to [RFC 6901](http://tools.ietf.org/html/rfc6901) -See Sourcecode for Examples +See source code for examples * Website: https://github.com/stefankoegl/python-json-pointer * Repository: https://github.com/stefankoegl/python-json-pointer.git * Documentation: https://python-json-pointer.readthedocs.org/ * PyPI: https://pypi.python.org/pypi/jsonpointer -* Travis-CI: https://travis-ci.org/stefankoegl/python-json-pointer +* Travis CI: https://travis-ci.org/stefankoegl/python-json-pointer * Coveralls: https://coveralls.io/r/stefankoegl/python-json-pointer From df3395ab1db3957e71c5a9701f0d3bb622faf16f Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 15 Jan 2018 16:57:26 +0200 Subject: [PATCH 39/75] Drop support for EOL Python 2.6 and 3.3 --- .travis.yml | 2 -- doc/index.rst | 2 +- jsonpointer.py | 2 +- setup.py | 3 +-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd9a8a3..f67ebbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: -- '2.6' - '2.7' -- '3.3' - '3.4' - '3.5' - '3.6' diff --git a/doc/index.rst b/doc/index.rst index dbdf2c6..53b1dea 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -7,7 +7,7 @@ python-json-pointer =================== *python-json-pointer* is a Python library for resolving JSON pointers (`RFC -6901 `_). Python 2.6, 2.7, 3.2, 3.3 +6901 `_). Python 2.7, 3.4+ and PyPy are supported. **Contents** diff --git a/jsonpointer.py b/jsonpointer.py index 285fc0c..a844729 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -174,7 +174,7 @@ def __init__(self, pointer): # validate escapes invalid_escape = self._RE_INVALID_ESCAPE.search(pointer) if invalid_escape: - raise JsonPointerException('Found invalid escape {0}'.format( + raise JsonPointerException('Found invalid escape {}'.format( invalid_escape.group())) parts = pointer.split('/') diff --git a/setup.py b/setup.py index f5e850c..21660f5 100644 --- a/setup.py +++ b/setup.py @@ -42,10 +42,8 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', @@ -66,4 +64,5 @@ py_modules=MODULES, scripts=['bin/jsonpointer'], classifiers=CLASSIFIERS, + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', ) From d348a5ecf28fa9152117632112b2fc05a6f42868 Mon Sep 17 00:00:00 2001 From: Jordan Danford Date: Wed, 16 May 2018 16:38:47 -0700 Subject: [PATCH 40/75] Use SVG versions of status icons in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5248dc8..210b07b 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ python-json-pointer [![PyPI version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) -[![Build Status](https://travis-ci.org/stefankoegl/python-json-pointer.png?branch=master)](https://travis-ci.org/stefankoegl/python-json-pointer) -[![Coverage Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.png?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master) +[![Build Status](https://travis-ci.org/stefankoegl/python-json-pointer.svg?branch=master)](https://travis-ci.org/stefankoegl/python-json-pointer) +[![Coverage Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.svg?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master) Resolve JSON Pointers in Python From 63b2682663fb14e4af42edffea1eea106019eabe Mon Sep 17 00:00:00 2001 From: Andrey Kislyuk Date: Fri, 1 Jun 2018 16:36:03 -0700 Subject: [PATCH 41/75] Fix typos in messages --- jsonpointer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index a844729..ec18cbe 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -162,7 +162,7 @@ def __repr__(self): class JsonPointer(object): - """A JSON Pointer that can reference parts of an JSON document""" + """A JSON Pointer that can reference parts of a JSON document""" # Array indices must not contain: # leading zeros, signs, spaces, decimals, etc @@ -179,7 +179,7 @@ def __init__(self, pointer): parts = pointer.split('/') if parts.pop(0) != '': - raise JsonPointerException('location must starts with /') + raise JsonPointerException('Location must start with /') parts = [unescape(part) for part in parts] self.parts = parts @@ -217,7 +217,7 @@ def set(self, doc, value, inplace=True): if len(self.parts) == 0: if inplace: - raise JsonPointerException('cannot set root in place') + raise JsonPointerException('Cannot set root in place') return value if not inplace: From f48c0878f8ecfa5209fc861855265bc4c8601006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Tue, 16 Jan 2018 16:18:26 +0100 Subject: [PATCH 42/75] Bump version to 2.0 --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index ec18cbe..5bbf7de 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,7 +36,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.14' +__version__ = '2.0' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' From 277e106016db0e81c64c561052be5360b9870974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Tue, 25 Sep 2018 10:46:50 +0200 Subject: [PATCH 43/75] Avoid converting readme to rST for PyPI upload PyPI now supports markdown descriptions. See https://dustingram.com/articles/2018/03/16/markdown-descriptions-on-pypi --- requirements-dev.txt | 3 ++- setup.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0dc8f0c..9fcb076 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ wheel -pypandoc==1.4 +twine>=1.11.0 +setuptools>=38.6.0 diff --git a/setup.py b/setup.py index 21660f5..e86bc1c 100644 --- a/setup.py +++ b/setup.py @@ -26,13 +26,10 @@ # Extract name and e-mail ("Firstname Lastname ") AUTHOR, EMAIL = re.match(r'(.*) <(.*)>', AUTHOR_EMAIL).groups() -try: - from pypandoc import convert - read_md = lambda f: convert(f, 'rst') -except ImportError: - print('warning: pypandoc module not found, could not convert ' - 'Markdown to RST') - read_md = lambda f: open(f, 'r').read() + +with open('README.md') as readme: + long_description = readme.read() + CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', @@ -56,7 +53,8 @@ setup(name=PACKAGE, version=VERSION, description=DESCRIPTION, - long_description=read_md('README.md'), + long_description=long_description, + long_description_content_type="text/markdown", author=AUTHOR, author_email=EMAIL, license=LICENSE, From 318be98cc9d04b7fdc08cd00fdc66c9cbb118c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Wed, 18 Sep 2019 19:09:31 +0200 Subject: [PATCH 44/75] Support Python 3.7 --- .travis.yml | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f67ebbe..0949d44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,9 @@ python: - '3.4' - '3.5' - '3.6' -- 3.6-dev +- '3.7' - 3.7-dev +- 3.8-dev - nightly - pypy - pypy3 diff --git a/setup.py b/setup.py index e86bc1c..16ff064 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', From fa6f0cca141f74512e2b177f2320774165f0346e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Wed, 13 Nov 2019 20:50:36 +0100 Subject: [PATCH 45/75] Update Python versions --- .travis.yml | 7 ++++--- setup.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f67ebbe..1780f30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ language: python python: - '2.7' -- '3.4' - '3.5' - '3.6' -- 3.6-dev -- 3.7-dev +- '3.7' +- '3.8' +- 3.8-dev +- 3.9-dev - nightly - pypy - pypy3 diff --git a/setup.py b/setup.py index e86bc1c..837c51a 100644 --- a/setup.py +++ b/setup.py @@ -41,9 +41,10 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', From 1855741f3dba6c603c5e806420973214e550a756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 14 Nov 2019 09:03:44 +0100 Subject: [PATCH 46/75] Remove 3.9-dev as test target --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1780f30..ed8bfb3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ python: - '3.7' - '3.8' - 3.8-dev -- 3.9-dev - nightly - pypy - pypy3 From 2ecc44f7f21f3551b43c44b7dd0bd9d14904b4a7 Mon Sep 17 00:00:00 2001 From: Martin Bless Date: Tue, 3 Mar 2020 13:46:19 +0100 Subject: [PATCH 47/75] Fix example in tutorial.rst --- doc/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 4519b29..9d7b21d 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -16,10 +16,10 @@ method is basically a deep ``get``. >>> resolve_pointer(obj, '/foo') == obj['foo'] True - >>> resolve_pointer(obj, '/foo/another%20prop') == obj['foo']['another prop'] + >>> resolve_pointer(obj, '/foo/another prop') == obj['foo']['another prop'] True - >>> resolve_pointer(obj, '/foo/another%20prop/baz') == obj['foo']['another prop']['baz'] + >>> resolve_pointer(obj, '/foo/another prop/baz') == obj['foo']['another prop']['baz'] True >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0] From 3b84fd3fc9424902e98a4a4990ea766e4f0f5380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Mar 2021 19:48:53 +0100 Subject: [PATCH 48/75] Also include Python 3.9 --- .travis.yml | 4 ++-- setup.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ed8bfb3..62ad875 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,9 @@ python: - '3.6' - '3.7' - '3.8' -- 3.8-dev +- '3.9' +- 3.10-dev - nightly -- pypy - pypy3 install: - travis_retry pip install coveralls diff --git a/setup.py b/setup.py index 837c51a..da27f3f 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', From 7d146bd7caf196bd04b44bdb6d395e91256fa88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Mar 2021 19:54:24 +0100 Subject: [PATCH 49/75] Bump version to 2.1 --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index 5bbf7de..29b4c80 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,7 +36,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '2.0' +__version__ = '2.1' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' From 73cb9ef10ac748141f066ada2f29f0f6daf3eb01 Mon Sep 17 00:00:00 2001 From: peterlits zo Date: Thu, 28 Oct 2021 18:11:58 +0800 Subject: [PATCH 50/75] Add method and add classmethod tag Set get_part as a classmethod, and add method `get_parts` For: #36, #45 --- jsonpointer.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 29b4c80..53191ef 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -193,7 +193,7 @@ def to_last(self, doc): for part in self.parts[:-1]: doc = self.walk(doc, part) - return doc, self.get_part(doc, self.parts[-1]) + return doc, JsonPointer.get_part(doc, self.parts[-1]) def resolve(self, doc, default=_nothing): """Resolves the pointer against doc and returns the referenced object""" @@ -228,7 +228,8 @@ def set(self, doc, value, inplace=True): parent[part] = value return doc - def get_part(self, doc, part): + @classmethod + def get_part(cls, doc, part): """Returns the next step in the correct type""" if isinstance(doc, Mapping): @@ -239,7 +240,7 @@ def get_part(self, doc, part): if part == '-': return part - if not self._RE_ARRAY_INDEX.match(str(part)): + if not JsonPointer._RE_ARRAY_INDEX.match(str(part)): raise JsonPointerException("'%s' is not a valid sequence index" % part) return int(part) @@ -252,12 +253,17 @@ def get_part(self, doc, part): else: raise JsonPointerException("Document '%s' does not support indexing, " "must be mapping/sequence or support __getitem__" % type(doc)) + + def get_parts(self): + """Returns the list of the parts. For example, JsonPointer('/a/b').get_parts() == ['a', 'b']""" + + return self.parts def walk(self, doc, part): """ Walks one step in doc and returns the referenced part """ - part = self.get_part(doc, part) + part = JsonPointer.get_part(doc, part) assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),) From a5e9f91e59e54bf1889a7eb2bfb24539a281e181 Mon Sep 17 00:00:00 2001 From: Peterlits Zo Date: Sun, 31 Oct 2021 19:26:30 +0800 Subject: [PATCH 51/75] Add test for get_parts (new method) --- tests.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 54ca436..409990e 100755 --- a/tests.py +++ b/tests.py @@ -70,10 +70,31 @@ def test_round_trip(self): ptr = JsonPointer(path) self.assertEqual(path, ptr.path) - parts = ptr.parts + parts = ptr.get_parts() + self.assertEqual(parts, ptr.parts) new_ptr = JsonPointer.from_parts(parts) self.assertEqual(ptr, new_ptr) + def test_parts(self): + paths = [ + ("", []), + ("/foo", ['foo']), + ("/foo/0", ['foo', '0']), + ("/", ['']), + ("/a~1b", ['a/b']), + ("/c%d", ['c%d']), + ("/e^f", ['e^f']), + ("/g|h", ['g|h']), + ("/i\\j", ['i\j']), + ("/k\"l", ['k"l']), + ("/ ", [' ']), + ("/m~0n", ['m~n']), + ('/\xee', ['\xee']), + ] + for path in paths: + ptr = JsonPointer(path[0]) + self.assertEqual(ptr.get_parts(), path[1]) + class ComparisonTests(unittest.TestCase): From 66ec8f71ac52384cc61d1db88672a86903671de4 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Sun, 20 Feb 2022 13:39:45 -0700 Subject: [PATCH 52/75] Support setting - for arrays --- jsonpointer.py | 6 +++++- tests.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index 53191ef..d353632 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -225,7 +225,11 @@ def set(self, doc, value, inplace=True): (parent, part) = self.to_last(doc) - parent[part] = value + if isinstance(parent, Sequence) and part == '-': + parent.append(value) + else: + parent[part] = value + return doc @classmethod diff --git a/tests.py b/tests.py index 409990e..4fefd9f 100755 --- a/tests.py +++ b/tests.py @@ -193,6 +193,12 @@ def test_set(self): newdoc = set_pointer(doc, "/foo/1", "cod", inplace=False) self.assertEqual(resolve_pointer(newdoc, "/foo/1"), "cod") + self.assertEqual(len(doc["foo"]), 2) + newdoc = set_pointer(doc, "/foo/-", "xyz", inplace=False) + self.assertEqual(resolve_pointer(newdoc, "/foo/2"), "xyz") + self.assertEqual(len(doc["foo"]), 2) + self.assertEqual(len(newdoc["foo"]), 3) + newdoc = set_pointer(doc, "/", 9, inplace=False) self.assertEqual(resolve_pointer(newdoc, "/"), 9) @@ -209,6 +215,11 @@ def test_set(self): set_pointer(doc, "/foo/1", "cod") self.assertEqual(resolve_pointer(doc, "/foo/1"), "cod") + self.assertEqual(len(doc["foo"]), 2) + set_pointer(doc, "/foo/-", "xyz") + self.assertEqual(resolve_pointer(doc, "/foo/2"), "xyz") + self.assertEqual(len(doc["foo"]), 3) + set_pointer(doc, "/", 9) self.assertEqual(resolve_pointer(doc, "/"), 9) From 04e4ac28b935f7538fa54d8ffd385549015040b5 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Sun, 20 Feb 2022 14:09:08 -0700 Subject: [PATCH 53/75] Add __str__ and __repr__ methods --- jsonpointer.py | 8 ++++++++ tests.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/jsonpointer.py b/jsonpointer.py index d353632..3eaea19 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -44,8 +44,10 @@ try: from itertools import izip str = unicode + encode_str = lambda u: u.encode("raw_unicode_escape") except ImportError: # Python 3 izip = zip + encode_str = lambda u: u try: from collections.abc import Mapping, Sequence @@ -322,6 +324,12 @@ def __eq__(self, other): def __hash__(self): return hash(tuple(self.parts)) + def __str__(self): + return encode_str(self.path) + + def __repr__(self): + return "JsonPointer(" + repr(self.path) + ")" + @classmethod def from_parts(cls, parts): """Constructs a JsonPointer from a list of (unescaped) paths diff --git a/tests.py b/tests.py index 4fefd9f..7a2520b 100755 --- a/tests.py +++ b/tests.py @@ -75,6 +75,50 @@ def test_round_trip(self): new_ptr = JsonPointer.from_parts(parts) self.assertEqual(ptr, new_ptr) + def test_str_and_repr(self): + paths = [ + ("", "", "JsonPointer({u}'')"), + ("/foo", "/foo", "JsonPointer({u}'/foo')"), + ("/foo/0", "/foo/0", "JsonPointer({u}'/foo/0')"), + ("/", "/", "JsonPointer({u}'/')"), + ("/a~1b", "/a~1b", "JsonPointer({u}'/a~1b')"), + ("/c%d", "/c%d", "JsonPointer({u}'/c%d')"), + ("/e^f", "/e^f", "JsonPointer({u}'/e^f')"), + ("/g|h", "/g|h", "JsonPointer({u}'/g|h')"), + ("/i\\j", "/i\\j", "JsonPointer({u}'/i\\\\j')"), + ("/k\"l", "/k\"l", "JsonPointer({u}'/k\"l')"), + ("/ ", "/ ", "JsonPointer({u}'/ ')"), + ("/m~0n", "/m~0n", "JsonPointer({u}'/m~0n')"), + ] + for path, ptr_str, ptr_repr in paths: + ptr = JsonPointer(path) + self.assertEqual(path, ptr.path) + + if sys.version_info[0] == 2: + u_str = "u" + else: + u_str = "" + self.assertEqual(ptr_str, str(ptr)) + self.assertEqual(ptr_repr.format(u=u_str), repr(ptr)) + + if sys.version_info[0] == 2: + path = "/\xee" + ptr_str = b"/\xee" + ptr_repr = "JsonPointer(u'/\\xee')" + else: + path = "/\xee" + ptr_str = "/\xee" + ptr_repr = "JsonPointer('/\xee')" + ptr = JsonPointer(path) + self.assertEqual(path, ptr.path) + + self.assertEqual(ptr_str, str(ptr)) + self.assertEqual(ptr_repr, repr(ptr)) + + # should not be unicode in Python 2 + self.assertIsInstance(str(ptr), str) + self.assertIsInstance(repr(ptr), str) + def test_parts(self): paths = [ ("", []), From d811454192adb18fc3a9aa333fc27e4e48ddd723 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Sun, 20 Feb 2022 14:10:10 -0700 Subject: [PATCH 54/75] Add join method and / operator --- jsonpointer.py | 19 ++++++++++++++++++- tests.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index 3eaea19..d9fe0c0 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -54,7 +54,7 @@ except ImportError: # Python 3 from collections import Mapping, Sequence -from itertools import tee +from itertools import tee, chain import re import copy @@ -299,6 +299,23 @@ def __contains__(self, item): """ Returns True if self contains the given ptr """ return self.contains(item) + def join(self, suffix): + """ Returns a new JsonPointer with the given suffix append to this ptr """ + if isinstance(suffix, JsonPointer): + suffix_parts = suffix.parts + elif isinstance(suffix, str): + suffix_parts = JsonPointer(suffix).parts + else: + suffix_parts = suffix + try: + return JsonPointer.from_parts(chain(self.parts, suffix_parts)) + except: + raise JsonPointerException("Invalid suffix") + + def __truediv__(self, suffix): # Python 3 + return self.join(suffix) + __div__ = __truediv__ # Python 2 + @property def path(self): """Returns the string representation of the pointer diff --git a/tests.py b/tests.py index 7a2520b..dafbe89 100755 --- a/tests.py +++ b/tests.py @@ -175,6 +175,42 @@ def test_contains_magic(self): self.assertTrue(self.ptr1 in self.ptr1) self.assertFalse(self.ptr3 in self.ptr1) + def test_join(self): + + ptr12a = self.ptr1.join(self.ptr2) + self.assertEqual(ptr12a.path, "/a/b/c/a/b") + + ptr12b = self.ptr1.join(self.ptr2.parts) + self.assertEqual(ptr12b.path, "/a/b/c/a/b") + + ptr12c = self.ptr1.join(self.ptr2.parts[0:1]) + self.assertEqual(ptr12c.path, "/a/b/c/a") + + ptr12d = self.ptr1.join("/a/b") + self.assertEqual(ptr12d.path, "/a/b/c/a/b") + + ptr12e = self.ptr1.join(["a", "b"]) + self.assertEqual(ptr12e.path, "/a/b/c/a/b") + + self.assertRaises(JsonPointerException, self.ptr1.join, 0) + + def test_join_magic(self): + + ptr12a = self.ptr1 / self.ptr2 + self.assertEqual(ptr12a.path, "/a/b/c/a/b") + + ptr12b = self.ptr1 / self.ptr2.parts + self.assertEqual(ptr12b.path, "/a/b/c/a/b") + + ptr12c = self.ptr1 / self.ptr2.parts[0:1] + self.assertEqual(ptr12c.path, "/a/b/c/a") + + ptr12d = self.ptr1 / "/a/b" + self.assertEqual(ptr12d.path, "/a/b/c/a/b") + + ptr12e = self.ptr1 / ["a", "b"] + self.assertEqual(ptr12e.path, "/a/b/c/a/b") + class WrongInputTests(unittest.TestCase): def test_no_start_slash(self): From d9ec0f5f74975dfb4ebcc18efeb4cee69aa8e788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Apr 2022 13:36:37 +0200 Subject: [PATCH 55/75] Fix invalid escape sequence (#50) --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index dafbe89..9252369 100755 --- a/tests.py +++ b/tests.py @@ -129,7 +129,7 @@ def test_parts(self): ("/c%d", ['c%d']), ("/e^f", ['e^f']), ("/g|h", ['g|h']), - ("/i\\j", ['i\j']), + ("/i\\j", ['i\\j']), ("/k\"l", ['k"l']), ("/ ", [' ']), ("/m~0n", ['m~n']), From a95c26fba8ef44af6d16ad6c5b70d7f9c69ae36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sun, 10 Apr 2022 13:37:21 +0200 Subject: [PATCH 56/75] Bump version to 2.3 --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index d9fe0c0..b45684d 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,7 +36,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '2.1' +__version__ = '2.3' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' From 04a864f33848da6af1dea906ba4922770022ef66 Mon Sep 17 00:00:00 2001 From: Ross Burton Date: Thu, 16 Mar 2023 14:21:32 +0000 Subject: [PATCH 57/75] Clean up test runner Test code doesn't need to manually construct a TestSuite and a TextTestRunner, the unittest module has a discovery function that does all this for you. Delete all of the manual logic from tests.py, replace it with the two lines to bring in the doctest unit tests, and update the makefile to run the unittest discovery. --- makefile | 2 +- tests.py | 24 ++++-------------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/makefile b/makefile index e0a2fcf..c40b485 100644 --- a/makefile +++ b/makefile @@ -10,7 +10,7 @@ help: @echo test: - python tests.py + python -munittest coverage: coverage run --source=jsonpointer tests.py diff --git a/tests.py b/tests.py index 9252369..6b4b8cc 100755 --- a/tests.py +++ b/tests.py @@ -7,6 +7,7 @@ import unittest import sys import copy +import jsonpointer from jsonpointer import resolve_pointer, EndOfList, JsonPointerException, \ JsonPointer, set_pointer @@ -410,23 +411,6 @@ def test_mock_dict_raises_key_error(self): self.assertRaises(JsonPointerException, resolve_pointer, doc, '/root/1/2/3/4') - -suite = unittest.TestSuite() -suite.addTest(unittest.makeSuite(SpecificationTests)) -suite.addTest(unittest.makeSuite(ComparisonTests)) -suite.addTest(unittest.makeSuite(WrongInputTests)) -suite.addTest(unittest.makeSuite(ToLastTests)) -suite.addTest(unittest.makeSuite(SetTests)) -suite.addTest(unittest.makeSuite(AltTypesTests)) - -modules = ['jsonpointer'] - -for module in modules: - m = __import__(module, fromlist=[module]) - suite.addTest(doctest.DocTestSuite(m)) - -runner = unittest.TextTestRunner(verbosity=1) -result = runner.run(suite) - -if not result.wasSuccessful(): - sys.exit(1) +def load_tests(loader, tests, ignore): + tests.addTests(doctest.DocTestSuite(jsonpointer)) + return tests From dc3627cc0e28ad1d09078df07b6303593a06ae72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 16 Jun 2023 23:05:57 +0200 Subject: [PATCH 58/75] Migrate to GitHub Actions, remove py 3.5, 3.6 --- .github/workflows/test.yaml | 33 +++++++++++++++++++++++++++++++++ .travis.yml | 32 -------------------------------- setup.py | 6 +++--- 3 files changed, 36 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/test.yaml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..dbc52cb --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,33 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + pip install coveralls +# - name: Lint with flake8 +# run: | + # stop the build if there are Python syntax errors or undefined names + # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test + run: | + coverage run --source=jsonpatch tests.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 62ad875..0000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: python -python: -- '2.7' -- '3.5' -- '3.6' -- '3.7' -- '3.8' -- '3.9' -- 3.10-dev -- nightly -- pypy3 -install: -- travis_retry pip install coveralls -script: -- coverage run --source=jsonpointer tests.py -after_script: -- coveralls -sudo: false -addons: - apt: - packages: - - pandoc -before_deploy: -- pip install -r requirements-dev.txt -deploy: - provider: pypi - user: skoegl - password: - secure: bKET/1sK+uWetPM3opPTt4qHHfZ6bMcjuLGe3Z/EUfNnGazcbDezy9CHJSqofuMXynF3xc8yluEojjfaqos3Ge/Y4o8pdFMY8ABp8KkxMnAJYGtYzbneSHgdgxPKsmdcUMVtIfioqkeNJTJClWUhRikWSlpKZ7TtkK4AmWtKNwc= - on: - tags: true - distributions: sdist bdist_wheel diff --git a/setup.py b/setup.py index da27f3f..ca33840 100644 --- a/setup.py +++ b/setup.py @@ -41,11 +41,11 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', @@ -64,5 +64,5 @@ py_modules=MODULES, scripts=['bin/jsonpointer'], classifiers=CLASSIFIERS, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*', ) From 3c62d8a8b1669dedd3e6e99236e615b0289ef4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 16 Jun 2023 23:10:57 +0200 Subject: [PATCH 59/75] bump version to 2.4 --- jsonpointer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpointer.py b/jsonpointer.py index b45684d..47d90ad 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -36,7 +36,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '2.3' +__version__ = '2.4' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' From 87630ab0ed59efb1b4e7fbd0f87fbfbb4ce43f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 16 Jun 2023 23:17:28 +0200 Subject: [PATCH 60/75] add .readthedocs.yaml https://blog.readthedocs.com/migrate-configuration-v2/ --- .readthedocs.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..f3aa1f5 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt From 880029ba106735296cb37e01e0d7de9142e3ffdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 16 Jun 2023 23:19:24 +0200 Subject: [PATCH 61/75] fix config name in .readthedocs.yaml --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f3aa1f5..b8c2f2b 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,7 +13,7 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: - configuration: docs/conf.py + configuration: doc/conf.py # We recommend specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html From 1c70f56b2c5083050bfaec7316724b6505e92b19 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 26 Sep 2023 12:48:15 -0600 Subject: [PATCH 62/75] Remove Travis CI badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 210b07b..06c8aef 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ python-json-pointer [![PyPI version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) -[![Build Status](https://travis-ci.org/stefankoegl/python-json-pointer.svg?branch=master)](https://travis-ci.org/stefankoegl/python-json-pointer) [![Coverage Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.svg?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master) From ed92974ad1f5ecf2000c4d9a7bb1494d67772161 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 26 Sep 2023 21:51:57 +0300 Subject: [PATCH 63/75] Add support for Python 3.12 --- .github/workflows/test.yaml | 5 +++-- setup.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index dbc52cb..ea95e8b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,14 +9,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/setup.py b/setup.py index ca33840..37d3657 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', From c3509e4f6dcbb303e5c8465e2c8b2e524be4e1af Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 26 Sep 2023 22:36:54 +0300 Subject: [PATCH 64/75] Fix test command --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ea95e8b..554c462 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -31,4 +31,4 @@ jobs: # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test run: | - coverage run --source=jsonpatch tests.py + make coverage From 6b2c95dad7710156d8b3522b316103911a83694b Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 28 May 2024 09:23:09 -0400 Subject: [PATCH 65/75] !feat:remove python 2.7 support --- .github/workflows/test.yaml | 12 ++++++------ bin/jsonpointer | 10 ++++------ jsonpointer.py | 31 +++++++++++-------------------- requirements-dev.txt | 5 +++-- setup.py | 4 +--- tests.py | 27 +++++++++++---------------- 6 files changed, 36 insertions(+), 53 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 554c462..92afae6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,6 +1,6 @@ name: Python package -on: [push] +on: [ push ] jobs: build: @@ -9,12 +9,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -23,12 +23,12 @@ jobs: python -m pip install --upgrade pip pip install flake8 pytest pip install coveralls -# - name: Lint with flake8 -# run: | + # - name: Lint with flake8 + # run: | # stop the build if there are Python syntax errors or undefined names # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test run: | make coverage diff --git a/bin/jsonpointer b/bin/jsonpointer index d577d01..ba2117c 100755 --- a/bin/jsonpointer +++ b/bin/jsonpointer @@ -1,14 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import print_function -import sys -import os.path -import json -import jsonpointer import argparse +import json +import sys +import jsonpointer parser = argparse.ArgumentParser( description='Resolve a JSON pointer on JSON files') @@ -20,7 +18,7 @@ ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r'), nargs='?', help='File containing a JSON pointer expression') -ptr_group.add_argument('POINTER', type=str, nargs='?', +ptr_group.add_argument('POINTER', type=str, nargs='?', help='A JSON pointer expression') parser.add_argument('FILE', type=argparse.FileType('r'), nargs='+', diff --git a/jsonpointer.py b/jsonpointer.py index 47d90ad..8e878cd 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -32,22 +32,14 @@ """ Identify specific nodes in a JSON document (RFC 6901) """ -from __future__ import unicode_literals # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '2.4' +__version__ = '3.0.0' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' -try: - from itertools import izip - str = unicode - encode_str = lambda u: u.encode("raw_unicode_escape") -except ImportError: # Python 3 - izip = zip - encode_str = lambda u: u try: from collections.abc import Mapping, Sequence @@ -58,7 +50,6 @@ import re import copy - _nothing = object() @@ -145,7 +136,7 @@ def pairwise(iterable): a, b = tee(iterable) for _ in b: break - return izip(a, b) + return zip(a, b) class JsonPointerException(Exception): @@ -259,12 +250,11 @@ def get_part(cls, doc, part): else: raise JsonPointerException("Document '%s' does not support indexing, " "must be mapping/sequence or support __getitem__" % type(doc)) - + def get_parts(self): """Returns the list of the parts. For example, JsonPointer('/a/b').get_parts() == ['a', 'b']""" - - return self.parts + return self.parts def walk(self, doc, part): """ Walks one step in doc and returns the referenced part """ @@ -281,7 +271,7 @@ def walk(self, doc, part): return doc[part] except IndexError: - raise JsonPointerException("index '%s' is out of bounds" % (part, )) + raise JsonPointerException("index '%s' is out of bounds" % (part,)) # Else the object is a mapping or supports __getitem__(so assume custom indexing) try: @@ -290,7 +280,6 @@ def walk(self, doc, part): except KeyError: raise JsonPointerException("member '%s' not found in %s" % (part, doc)) - def contains(self, ptr): """ Returns True if self contains the given ptr """ return self.parts[:len(ptr.parts)] == ptr.parts @@ -312,9 +301,10 @@ def join(self, suffix): except: raise JsonPointerException("Invalid suffix") - def __truediv__(self, suffix): # Python 3 + def __truediv__(self, suffix): # Python 3 return self.join(suffix) - __div__ = __truediv__ # Python 2 + + __div__ = __truediv__ # Python 2 @property def path(self): @@ -342,10 +332,10 @@ def __hash__(self): return hash(tuple(self.parts)) def __str__(self): - return encode_str(self.path) + return self.path def __repr__(self): - return "JsonPointer(" + repr(self.path) + ")" + return type(self).__name__ + "(" + repr(self.path) + ")" @classmethod def from_parts(cls, parts): @@ -362,5 +352,6 @@ def from_parts(cls, parts): def escape(s): return s.replace('~', '~0').replace('/', '~1') + def unescape(s): return s.replace('~1', '/').replace('~0', '~') diff --git a/requirements-dev.txt b/requirements-dev.txt index 9fcb076..6476efe 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ wheel -twine>=1.11.0 -setuptools>=38.6.0 +twine>=5.1.0 +setuptools>=70 +coverage diff --git a/setup.py b/setup.py index 37d3657..2ab2d38 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,6 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', @@ -65,5 +63,5 @@ py_modules=MODULES, scripts=['bin/jsonpointer'], classifiers=CLASSIFIERS, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*', + python_requires='>=3.7', ) diff --git a/tests.py b/tests.py index 6b4b8cc..7b1cdac 100755 --- a/tests.py +++ b/tests.py @@ -3,20 +3,21 @@ from __future__ import unicode_literals +import copy import doctest -import unittest import sys -import copy +import unittest + import jsonpointer from jsonpointer import resolve_pointer, EndOfList, JsonPointerException, \ - JsonPointer, set_pointer + JsonPointer, set_pointer class SpecificationTests(unittest.TestCase): """ Tests all examples from the JSON Pointer specification """ def test_example(self): - doc = { + doc = { "foo": ["bar", "baz"], "": 0, "a/b": 1, @@ -42,7 +43,6 @@ def test_example(self): self.assertEqual(resolve_pointer(doc, "/ "), 7) self.assertEqual(resolve_pointer(doc, "/m~0n"), 8) - def test_eol(self): doc = { "foo": ["bar", "baz"] @@ -165,19 +165,16 @@ def test_eq_hash(self): self.assertFalse(p1 == "/something/1/b") def test_contains(self): - self.assertTrue(self.ptr1.contains(self.ptr2)) self.assertTrue(self.ptr1.contains(self.ptr1)) self.assertFalse(self.ptr1.contains(self.ptr3)) def test_contains_magic(self): - self.assertTrue(self.ptr2 in self.ptr1) self.assertTrue(self.ptr1 in self.ptr1) self.assertFalse(self.ptr3 in self.ptr1) def test_join(self): - ptr12a = self.ptr1.join(self.ptr2) self.assertEqual(ptr12a.path, "/a/b/c/a/b") @@ -196,7 +193,6 @@ def test_join(self): self.assertRaises(JsonPointerException, self.ptr1.join, 0) def test_join_magic(self): - ptr12a = self.ptr1 / self.ptr2 self.assertEqual(ptr12a.path, "/a/b/c/a/b") @@ -212,6 +208,7 @@ def test_join_magic(self): ptr12e = self.ptr1 / ["a", "b"] self.assertEqual(ptr12e.path, "/a/b/c/a/b") + class WrongInputTests(unittest.TestCase): def test_no_start_slash(self): @@ -244,7 +241,6 @@ def test_empty_path(self): self.assertEqual(doc, last) self.assertTrue(nxt is None) - def test_path(self): doc = {'a': [{'b': 1, 'c': 2}, 5]} ptr = JsonPointer('/a/0/b') @@ -256,7 +252,7 @@ def test_path(self): class SetTests(unittest.TestCase): def test_set(self): - doc = { + doc = { "foo": ["bar", "baz"], "": 0, "a/b": 1, @@ -285,7 +281,7 @@ def test_set(self): newdoc = set_pointer(doc, "/fud", {}, inplace=False) newdoc = set_pointer(newdoc, "/fud/gaw", [1, 2, 3], inplace=False) - self.assertEqual(resolve_pointer(newdoc, "/fud"), {'gaw' : [1, 2, 3]}) + self.assertEqual(resolve_pointer(newdoc, "/fud"), {'gaw': [1, 2, 3]}) newdoc = set_pointer(doc, "", 9, inplace=False) self.assertEqual(newdoc, 9) @@ -307,14 +303,13 @@ def test_set(self): self.assertRaises(JsonPointerException, set_pointer, doc, "/fud/gaw", 9) set_pointer(doc, "/fud", {}) - set_pointer(doc, "/fud/gaw", [1, 2, 3] ) - self.assertEqual(resolve_pointer(doc, "/fud"), {'gaw' : [1, 2, 3]}) + set_pointer(doc, "/fud/gaw", [1, 2, 3]) + self.assertEqual(resolve_pointer(doc, "/fud"), {'gaw': [1, 2, 3]}) self.assertRaises(JsonPointerException, set_pointer, doc, "", 9) class AltTypesTests(unittest.TestCase): - class Node(object): def __init__(self, name, parent=None): self.name = name @@ -349,13 +344,13 @@ def __setitem__(self, key, val): class mdict(object): def __init__(self, d): self._d = d + def __getitem__(self, item): return self._d[item] mdict = mdict({'root': {'1': {'2': '3'}}}) Node = Node - def test_alttypes(self): Node = self.Node From 4601845016595bbf6f71ca962d9cf27412db4be4 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 28 May 2024 09:35:57 -0400 Subject: [PATCH 66/75] fix:github action --- .github/workflows/test.yaml | 41 ++++++++++++++++++++++++++++--------- jsonpointer.py | 9 +++----- requirements-dev.txt | 1 + setup.cfg | 4 ++++ setup.py | 13 ++++++------ 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 92afae6..693e81d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,10 +1,37 @@ name: Python package -on: [ push ] +on: + push: + branches: + - master + pull_request: + branches: + - master jobs: - build: + lint: + name: "flake8 on code" + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + allow-prereleases: true + + - name: Run flake8 + shell: bash + run: | + flake8 + + test: + needs: [ lint ] runs-on: ubuntu-latest strategy: fail-fast: false @@ -21,14 +48,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - pip install coveralls - # - name: Lint with flake8 - # run: | - # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + pip install -r requirements-dev.txt + - name: Test run: | make coverage diff --git a/jsonpointer.py b/jsonpointer.py index 8e878cd..f04804a 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -32,23 +32,20 @@ """ Identify specific nodes in a JSON document (RFC 6901) """ - # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' __version__ = '3.0.0' __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' - - try: from collections.abc import Mapping, Sequence except ImportError: # Python 3 from collections import Mapping, Sequence -from itertools import tee, chain -import re import copy +import re +from itertools import tee, chain _nothing = object() @@ -298,7 +295,7 @@ def join(self, suffix): suffix_parts = suffix try: return JsonPointer.from_parts(chain(self.parts, suffix_parts)) - except: + except: # noqa E722 raise JsonPointerException("Invalid suffix") def __truediv__(self, suffix): # Python 3 diff --git a/requirements-dev.txt b/requirements-dev.txt index 6476efe..f06e81a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ wheel twine>=5.1.0 setuptools>=70 coverage +flake8==7.0.0 diff --git a/setup.cfg b/setup.cfg index 2a9acf1..ab8f354 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [bdist_wheel] universal = 1 + +[flake8] +max-line-length = 120 +exclude = .git,.tox,dist,doc,*egg,build,.venv \ No newline at end of file diff --git a/setup.py b/setup.py index 2ab2d38..3e87a4c 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ #!/usr/bin/env python -from setuptools import setup -import re import io import os.path +import re + +from setuptools import setup dirname = os.path.dirname(os.path.abspath(__file__)) filename = os.path.join(dirname, 'jsonpointer.py') @@ -14,7 +15,7 @@ PACKAGE = 'jsonpointer' MODULES = ( - 'jsonpointer', + 'jsonpointer', ) AUTHOR_EMAIL = metadata['author'] @@ -26,10 +27,8 @@ # Extract name and e-mail ("Firstname Lastname ") AUTHOR, EMAIL = re.match(r'(.*) <(.*)>', AUTHOR_EMAIL).groups() - with open('README.md') as readme: - long_description = readme.read() - + long_description = readme.read() CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', @@ -64,4 +63,4 @@ scripts=['bin/jsonpointer'], classifiers=CLASSIFIERS, python_requires='>=3.7', -) + ) From abad7950685c81bd03c1758fc321ec58b7010c8e Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 28 May 2024 09:36:26 -0400 Subject: [PATCH 67/75] maint:add .idea to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ce09cfb..861d6b2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist *.swp doc/_build *.egg-info +.idea From 386646d765211430fcf4ffcf269f3b87ee25918c Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 28 May 2024 09:38:04 -0400 Subject: [PATCH 68/75] fix:install flake8 --- .github/workflows/test.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 693e81d..fa172c1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,7 +24,10 @@ jobs: with: python-version: 3.12 allow-prereleases: true - + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 - name: Run flake8 shell: bash run: | From a9ae88093ec183bfd53becf847b1e31742898da5 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 28 May 2024 09:40:35 -0400 Subject: [PATCH 69/75] remove twine --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f06e81a..7c98cef 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ wheel -twine>=5.1.0 setuptools>=70 coverage flake8==7.0.0 From 0bc72c83915c75718b05daac9f128a4db3161fde Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 28 May 2024 09:42:43 -0400 Subject: [PATCH 70/75] remove twine --- jsonpointer.py | 2 +- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index f04804a..0a03ddd 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -51,7 +51,7 @@ def set_pointer(doc, pointer, value, inplace=True): - """Resolves pointer against doc and sets the value of the target within doc. + """Resolves a 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. diff --git a/requirements-dev.txt b/requirements-dev.txt index 7c98cef..cff99fa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ wheel -setuptools>=70 +setuptools>=38.6.0 coverage flake8==7.0.0 From c06953431785ca9c7b951869b9792ea3cdd4a73c Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 28 May 2024 09:43:55 -0400 Subject: [PATCH 71/75] remove twine --- requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index cff99fa..239fcca 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ wheel -setuptools>=38.6.0 +setuptools coverage -flake8==7.0.0 +flake8 From cce7ed2513e4b4383f76ea300422a5a2287daf4e Mon Sep 17 00:00:00 2001 From: Daniel Moran Date: Tue, 28 May 2024 15:57:26 -0400 Subject: [PATCH 72/75] address comments --- jsonpointer.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/jsonpointer.py b/jsonpointer.py index 0a03ddd..3e97add 100644 --- a/jsonpointer.py +++ b/jsonpointer.py @@ -38,13 +38,9 @@ __website__ = 'https://github.com/stefankoegl/python-json-pointer' __license__ = 'Modified BSD License' -try: - from collections.abc import Mapping, Sequence -except ImportError: # Python 3 - from collections import Mapping, Sequence - import copy import re +from collections.abc import Mapping, Sequence from itertools import tee, chain _nothing = object() @@ -301,8 +297,6 @@ def join(self, suffix): def __truediv__(self, suffix): # Python 3 return self.join(suffix) - __div__ = __truediv__ # Python 2 - @property def path(self): """Returns the string representation of the pointer From 2f31bece9db847427f913614a790dac855ac1b0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 May 2025 09:21:25 +0000 Subject: [PATCH 73/75] Initial plan for issue From a86f1860e9b51bd18979ed8adbd8770719493e0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 May 2025 09:27:16 +0000 Subject: [PATCH 74/75] Drop Python 2.7, 3.7, 3.8 support - update config and remove compatibility code Co-authored-by: stefankoegl <184196+stefankoegl@users.noreply.github.com> --- .coveragerc | 2 +- .github/workflows/test.yaml | 2 +- doc/index.rst | 2 +- setup.py | 4 +-- tests.py | 49 +++++++++++++------------------------ 5 files changed, 21 insertions(+), 38 deletions(-) diff --git a/.coveragerc b/.coveragerc index 40fd2df..0ebe947 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,5 +11,5 @@ exclude_lines = # No need to test __repr__ def __repr__ - # Python 2/3 compatibility + # Import fallbacks except ImportError diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fa172c1..7ec5232 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -39,7 +39,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] steps: - uses: actions/checkout@v4 diff --git a/doc/index.rst b/doc/index.rst index 53b1dea..7d079e8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -7,7 +7,7 @@ python-json-pointer =================== *python-json-pointer* is a Python library for resolving JSON pointers (`RFC -6901 `_). Python 2.7, 3.4+ +6901 `_). Python 3.9+ and PyPy are supported. **Contents** diff --git a/setup.py b/setup.py index 3e87a4c..75f5426 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,6 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', @@ -62,5 +60,5 @@ py_modules=MODULES, scripts=['bin/jsonpointer'], classifiers=CLASSIFIERS, - python_requires='>=3.7', + python_requires='>=3.9', ) diff --git a/tests.py b/tests.py index 7b1cdac..668982d 100755 --- a/tests.py +++ b/tests.py @@ -1,11 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import unicode_literals - import copy import doctest -import sys import unittest import jsonpointer @@ -78,45 +75,33 @@ def test_round_trip(self): def test_str_and_repr(self): paths = [ - ("", "", "JsonPointer({u}'')"), - ("/foo", "/foo", "JsonPointer({u}'/foo')"), - ("/foo/0", "/foo/0", "JsonPointer({u}'/foo/0')"), - ("/", "/", "JsonPointer({u}'/')"), - ("/a~1b", "/a~1b", "JsonPointer({u}'/a~1b')"), - ("/c%d", "/c%d", "JsonPointer({u}'/c%d')"), - ("/e^f", "/e^f", "JsonPointer({u}'/e^f')"), - ("/g|h", "/g|h", "JsonPointer({u}'/g|h')"), - ("/i\\j", "/i\\j", "JsonPointer({u}'/i\\\\j')"), - ("/k\"l", "/k\"l", "JsonPointer({u}'/k\"l')"), - ("/ ", "/ ", "JsonPointer({u}'/ ')"), - ("/m~0n", "/m~0n", "JsonPointer({u}'/m~0n')"), + ("", "", "JsonPointer('')"), + ("/foo", "/foo", "JsonPointer('/foo')"), + ("/foo/0", "/foo/0", "JsonPointer('/foo/0')"), + ("/", "/", "JsonPointer('/')"), + ("/a~1b", "/a~1b", "JsonPointer('/a~1b')"), + ("/c%d", "/c%d", "JsonPointer('/c%d')"), + ("/e^f", "/e^f", "JsonPointer('/e^f')"), + ("/g|h", "/g|h", "JsonPointer('/g|h')"), + ("/i\\j", "/i\\j", "JsonPointer('/i\\\\j')"), + ("/k\"l", "/k\"l", "JsonPointer('/k\"l')"), + ("/ ", "/ ", "JsonPointer('/ ')"), + ("/m~0n", "/m~0n", "JsonPointer('/m~0n')"), ] for path, ptr_str, ptr_repr in paths: ptr = JsonPointer(path) self.assertEqual(path, ptr.path) - - if sys.version_info[0] == 2: - u_str = "u" - else: - u_str = "" self.assertEqual(ptr_str, str(ptr)) - self.assertEqual(ptr_repr.format(u=u_str), repr(ptr)) - - if sys.version_info[0] == 2: - path = "/\xee" - ptr_str = b"/\xee" - ptr_repr = "JsonPointer(u'/\\xee')" - else: - path = "/\xee" - ptr_str = "/\xee" - ptr_repr = "JsonPointer('/\xee')" + self.assertEqual(ptr_repr, repr(ptr)) + + path = "/\xee" + ptr_str = "/\xee" + ptr_repr = "JsonPointer('/\xee')" ptr = JsonPointer(path) self.assertEqual(path, ptr.path) - self.assertEqual(ptr_str, str(ptr)) self.assertEqual(ptr_repr, repr(ptr)) - # should not be unicode in Python 2 self.assertIsInstance(str(ptr), str) self.assertIsInstance(repr(ptr), str) From 4f12e7c42b0996750ded1235da2ca45e04b240ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 May 2025 09:38:21 +0000 Subject: [PATCH 75/75] Remove obsolete 'except ImportError' exclusion from .coveragerc Co-authored-by: stefankoegl <184196+stefankoegl@users.noreply.github.com> --- .coveragerc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 0ebe947..357bebd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,3 @@ exclude_lines = # No need to test __repr__ def __repr__ - - # Import fallbacks - except ImportError