diff --git a/.coveragerc b/.coveragerc index 40fd2df..357bebd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,3 @@ exclude_lines = # No need to test __repr__ def __repr__ - - # Python 2/3 compatibility - except ImportError diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..7ec5232 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,58 @@ +name: Python package + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + 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: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Run flake8 + shell: bash + run: | + flake8 + + test: + needs: [ lint ] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ "3.9", "3.10", "3.11", "3.12" ] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Test + run: | + make coverage 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 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..b8c2f2b --- /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: doc/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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 822fb93..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: python -python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "3.4" - - "pypy" - - "pypy3" - -install: - - travis_retry pip install coveralls - -script: - - coverage run --source=jsonpointer tests.py - -after_script: - - coveralls - -sudo: false 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 diff --git a/README.md b/README.md index cae175d..06c8aef 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,21 @@ -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/) +[![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 ------------------------------- 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 diff --git a/bin/jsonpointer b/bin/jsonpointer index 1d49fae..ba2117c 100755 --- a/bin/jsonpointer +++ b/bin/jsonpointer @@ -1,19 +1,26 @@ #!/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') -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 +36,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: diff --git a/doc/index.rst b/doc/index.rst index dbdf2c6..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.6, 2.7, 3.2, 3.3 +6901 `_). Python 3.9+ and PyPy are supported. **Contents** 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] diff --git a/jsonpointer.py b/jsonpointer.py index 718ea9a..3e97add 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,63 +30,52 @@ # 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 - # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.9' +__version__ = '3.0.0' __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 - -from itertools import tee -import re import copy +import re +from collections.abc import Mapping, Sequence +from itertools import tee, chain +_nothing = object() -# array indices must not contain leading zeros, signs, spaces, decimals, etc -RE_ARRAY_INDEX=re.compile('0|[1-9][0-9]*$') - - -class JsonPointerException(Exception): - pass +def set_pointer(doc, pointer, value, inplace=True): + """Resolves a 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 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 + """ -_nothing = object() + pointer = JsonPointer(pointer) + return pointer.set(doc, value, inplace) def resolve_pointer(doc, pointer, default=_nothing): - """ - Resolves pointer against doc and returns the referenced object + """ Resolves pointer against doc and returns the referenced object - >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} + >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}, 'a%20b': 1, 'c d': 2} >>> resolve_pointer(obj, '') == obj True @@ -94,10 +83,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] @@ -106,50 +95,83 @@ 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) return pointer.resolve(doc, default) -def set_pointer(doc, pointer, value, inplace=True): - """ - Resolves pointer against doc and sets the value of the target within doc. - With inplace set to true, doc is modified as long as pointer is not the - root. +def pairwise(iterable): + """ Transforms a list to a list of tuples of adjacent items - >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} + s -> (s0,s1), (s1,s2), (s2, s3), ... - >>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \ - {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} - True + >>> list(pairwise([])) + [] - >>> set_pointer(obj, '/foo/yet%20another%20prop', 'added prop') == \ - {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}} - True + >>> list(pairwise([1])) + [] + >>> list(pairwise([1, 2, 3, 4])) + [(1, 2), (2, 3), (3, 4)] """ + a, b = tee(iterable) + for _ in b: + break + return zip(a, b) - pointer = JsonPointer(pointer) - return pointer.set(doc, value, inplace) + +class JsonPointerException(Exception): + pass + + +class EndOfList(object): + """Result of accessing element "-" of a list""" + + def __init__(self, list_): + self.list_ = list_ + + def __repr__(self): + return '{cls}({lst})'.format(cls=self.__class__.__name__, + lst=repr(self.list_)) class JsonPointer(object): - """ A JSON Pointer that can reference parts of an JSON document """ + """A JSON Pointer that can reference parts of a JSON document""" + + # 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 {}'.format( + invalid_escape.group())) + parts = pointer.split('/') if parts.pop(0) != '': - raise JsonPointerException('location must starts with /') + raise JsonPointerException('Location must start 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 - 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 @@ -157,8 +179,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""" @@ -175,15 +196,14 @@ 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: - raise JsonPointerException('cannot set root in place') + raise JsonPointerException('Cannot set root in place') return value if not inplace: @@ -191,11 +211,16 @@ 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 - def get_part(self, doc, part): - """ Returns the next step in the correct type """ + @classmethod + def get_part(cls, doc, part): + """Returns the next step in the correct type""" if isinstance(doc, Mapping): return part @@ -205,36 +230,33 @@ 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 JsonPointer._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)) + "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 (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): + assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),) + if isinstance(doc, Sequence): if part == '-': return EndOfList(doc) @@ -242,74 +264,85 @@ 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 + # 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""" + """ Returns True if self contains the given ptr """ return self.parts[:len(ptr.parts)] == ptr.parts def __contains__(self, item): - """Returns True if self contains the given ptr""" + """ 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: # noqa E722 + raise JsonPointerException("Invalid suffix") + + def __truediv__(self, suffix): # Python 3 + return self.join(suffix) + @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' """ - 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): - """ 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)) + def __str__(self): + return self.path + + def __repr__(self): + return type(self).__name__ + "(" + repr(self.path) + ")" + @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 """ - 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 +def escape(s): + return s.replace('~', '~0').replace('/', '~1') -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) +def unescape(s): + return s.replace('~1', '/').replace('~0', '~') 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/requirements-dev.txt b/requirements-dev.txt index fd0fd6c..239fcca 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,4 @@ wheel -pandoc==1.0.0-alpha.3 +setuptools +coverage +flake8 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ab8f354 --- /dev/null +++ b/setup.cfg @@ -0,0 +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 6e683c8..75f5426 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,13 +27,8 @@ # 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', @@ -41,13 +37,11 @@ 'License :: OSI Approved :: BSD License', '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.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', + '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', @@ -57,7 +51,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, @@ -65,4 +60,5 @@ py_modules=MODULES, scripts=['bin/jsonpointer'], classifiers=CLASSIFIERS, -) + python_requires='>=3.9', + ) diff --git a/tests.py b/tests.py index cf5c61a..668982d 100755 --- a/tests.py +++ b/tests.py @@ -1,20 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import unicode_literals - +import copy import doctest import unittest -import sys -import copy + +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, @@ -40,7 +40,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"] @@ -69,10 +68,64 @@ 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_str_and_repr(self): + paths = [ + ("", "", "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) + self.assertEqual(ptr_str, str(ptr)) + 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)) + + self.assertIsInstance(str(ptr), str) + self.assertIsInstance(repr(ptr), str) + + 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): def setUp(self): @@ -97,17 +150,49 @@ 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") + + 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): @@ -125,6 +210,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): @@ -135,7 +226,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') @@ -147,7 +237,7 @@ def test_path(self): class SetTests(unittest.TestCase): def test_set(self): - doc = { + doc = { "foo": ["bar", "baz"], "": 0, "a/b": 1, @@ -165,12 +255,18 @@ 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) 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) @@ -181,53 +277,67 @@ 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) 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 + self.parent = parent + self.left = None + self.right = None - def test_alttypes(self): - JsonPointer.alttypes = True + def set_left(self, node): + node.parent = self + self.left = node + + def set_right(self, node): + node.parent = self + self.right = node - class Node(object): - def __init__(self, name, parent=None): - self.name = name - self.parent = parent - self.left = None - self.right = None + def __getitem__(self, key): + if key == 'left': + return self.left + if key == 'right': + return self.right - def set_left(self, node): - node.parent = self - self.left = node + raise KeyError("Only left and right supported") - def set_right(self, node): - node.parent = self - self.right = node + def __setitem__(self, key, val): + if key == 'left': + return self.set_left(val) + if key == 'right': + return self.set_right(val) - def __getitem__(self, key): - if key == 'left': - return self.left - if key == 'right': - return self.right + raise KeyError("Only left and right supported: %s" % key) - raise KeyError("Only left and right supported") + class mdict(object): + def __init__(self, d): + self._d = d - def __setitem__(self, key, val): - if key == 'left': - return self.set_left(val) - if key == 'right': - return self.set_right(val) + def __getitem__(self, item): + return self._d[item] - raise KeyError("Only left and right supported: %s" % key) + mdict = mdict({'root': {'1': {'2': '3'}}}) + Node = Node + def test_alttypes(self): + Node = self.Node root = Node('root') root.set_left(Node('a')) @@ -249,22 +359,38 @@ def __setitem__(self, key, val): set_pointer(root, '/left/right', Node('AB')) self.assertEqual(resolve_pointer(root, '/left/right').name, 'AB') -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)) + 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 + } -modules = ['jsonpointer'] + for path, expected_value in iter(path_to_expected_value.items()): + self.assertEqual(resolve_pointer(doc, path, default), expected_value) -for module in modules: - m = __import__(module, fromlist=[module]) - suite.addTest(doctest.DocTestSuite(m)) + def test_mock_dict_raises_key_error(self): + doc = self.mdict + self.assertRaises(JsonPointerException, resolve_pointer, doc, '/foo') + self.assertRaises(JsonPointerException, resolve_pointer, doc, '/root/1/2/3/4') -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