├── doc ├── _static │ └── .empty ├── mod-jsonpointer.rst ├── index.rst ├── commandline.rst ├── tutorial.rst ├── Makefile └── conf.py ├── requirements-dev.txt ├── MANIFEST.in ├── .gitignore ├── AUTHORS ├── setup.cfg ├── .coveragerc ├── makefile ├── .readthedocs.yaml ├── README.md ├── LICENSE.txt ├── .github └── workflows │ └── test.yaml ├── bin └── jsonpointer ├── setup.py ├── jsonpointer.py └── tests.py /doc/_static/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | wheel 2 | setuptools 3 | coverage 4 | flake8 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE.txt 3 | include README.md 4 | include tests.py 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | .coverage 4 | MANIFEST 5 | dist 6 | *.swp 7 | doc/_build 8 | *.egg-info 9 | .idea 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Stefan Kögl 2 | Alexander Shorin 3 | Christopher J. White 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 120 6 | exclude = .git,.tox,dist,doc,*egg,build,.venv -------------------------------------------------------------------------------- /doc/mod-jsonpointer.rst: -------------------------------------------------------------------------------- 1 | .. mod-jsonpointer: 2 | 3 | The ``jsonpointer`` module 4 | ============================ 5 | 6 | .. automodule:: jsonpointer 7 | :members: 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | 5 | [report] 6 | # Regexes for lines to exclude from consideration 7 | exclude_lines = 8 | # Have to re-enable the standard pragma 9 | pragma: no cover 10 | 11 | # No need to test __repr__ 12 | def __repr__ 13 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | 2 | help: 3 | @echo "jsonpointer" 4 | @echo "Makefile targets" 5 | @echo " - test: run tests" 6 | @echo " - coverage: run tests with coverage" 7 | @echo 8 | @echo "To install jsonpointer, type" 9 | @echo " python setup.py install" 10 | @echo 11 | 12 | test: 13 | python -munittest 14 | 15 | coverage: 16 | coverage run --source=jsonpointer tests.py 17 | coverage report -m 18 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: doc/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | # python: 21 | # install: 22 | # - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. python-json-pointer documentation master file, created by 2 | sphinx-quickstart on Sat Apr 13 16:52:59 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | python-json-pointer 7 | =================== 8 | 9 | *python-json-pointer* is a Python library for resolving JSON pointers (`RFC 10 | 6901 `_). Python 3.9+ 11 | and PyPy are supported. 12 | 13 | **Contents** 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | tutorial 19 | mod-jsonpointer 20 | commandline 21 | RFC 6901 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-json-pointer 2 | =================== 3 | 4 | [![PyPI version](https://img.shields.io/pypi/v/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) 5 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpointer.svg)](https://pypi.python.org/pypi/jsonpointer/) 6 | [![Coverage Status](https://coveralls.io/repos/stefankoegl/python-json-pointer/badge.svg?branch=master)](https://coveralls.io/r/stefankoegl/python-json-pointer?branch=master) 7 | 8 | 9 | Resolve JSON Pointers in Python 10 | ------------------------------- 11 | 12 | Library to resolve JSON Pointers according to 13 | [RFC 6901](http://tools.ietf.org/html/rfc6901) 14 | 15 | See source code for examples 16 | * Website: https://github.com/stefankoegl/python-json-pointer 17 | * Repository: https://github.com/stefankoegl/python-json-pointer.git 18 | * Documentation: https://python-json-pointer.readthedocs.org/ 19 | * PyPI: https://pypi.python.org/pypi/jsonpointer 20 | * Travis CI: https://travis-ci.org/stefankoegl/python-json-pointer 21 | * Coveralls: https://coveralls.io/r/stefankoegl/python-json-pointer 22 | -------------------------------------------------------------------------------- /doc/commandline.rst: -------------------------------------------------------------------------------- 1 | The ``jsonpointer`` commandline utility 2 | ======================================= 3 | 4 | The JSON pointer package also installs a ``jsonpointer`` commandline utility 5 | that can be used to resolve a JSON pointers on JSON files. 6 | 7 | The program has the following usage :: 8 | 9 | usage: jsonpointer [-h] [--indent INDENT] [-v] POINTER FILE [FILE ...] 10 | 11 | Resolve a JSON pointer on JSON files 12 | 13 | positional arguments: 14 | POINTER File containing a JSON pointer expression 15 | FILE Files for which the pointer should be resolved 16 | 17 | optional arguments: 18 | -h, --help show this help message and exit 19 | --indent INDENT Indent output by n spaces 20 | -v, --version show program's version number and exit 21 | 22 | 23 | Example 24 | ^^^^^^^ 25 | 26 | .. code-block:: bash 27 | 28 | # inspect JSON files 29 | $ cat a.json 30 | { "a": [1, 2, 3] } 31 | 32 | $ cat b.json 33 | { "a": {"b": [1, 3, 4]}, "b": 1 } 34 | 35 | # inspect JSON pointer 36 | $ cat ptr.json 37 | "/a" 38 | 39 | # resolve JSON pointer 40 | $ jsonpointer ptr.json a.json b.json 41 | [1, 2, 3] 42 | {"b": [1, 3, 4]} 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Stefan Kögl 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 3. The name of the author may not be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 17 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 18 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 21 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 25 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | name: "flake8 on code" 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.ref }} 21 | 22 | - name: Set up Python 3.12 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: 3.12 26 | allow-prereleases: true 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install flake8 31 | - name: Run flake8 32 | shell: bash 33 | run: | 34 | flake8 35 | 36 | test: 37 | needs: [ lint ] 38 | runs-on: ubuntu-latest 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | python-version: [ "3.9", "3.10", "3.11", "3.12" ] 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Set up Python ${{ matrix.python-version }} 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: ${{ matrix.python-version }} 50 | allow-prereleases: true 51 | - name: Install dependencies 52 | run: | 53 | python -m pip install --upgrade pip 54 | pip install -r requirements-dev.txt 55 | 56 | - name: Test 57 | run: | 58 | make coverage 59 | -------------------------------------------------------------------------------- /bin/jsonpointer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import argparse 6 | import json 7 | import sys 8 | 9 | import jsonpointer 10 | 11 | parser = argparse.ArgumentParser( 12 | description='Resolve a JSON pointer on JSON files') 13 | 14 | # Accept pointer as argument or as file 15 | ptr_group = parser.add_mutually_exclusive_group(required=True) 16 | 17 | ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r'), 18 | nargs='?', 19 | help='File containing a JSON pointer expression') 20 | 21 | ptr_group.add_argument('POINTER', type=str, nargs='?', 22 | help='A JSON pointer expression') 23 | 24 | parser.add_argument('FILE', type=argparse.FileType('r'), nargs='+', 25 | help='Files for which the pointer should be resolved') 26 | parser.add_argument('--indent', type=int, default=None, 27 | help='Indent output by n spaces') 28 | parser.add_argument('-v', '--version', action='version', 29 | version='%(prog)s ' + jsonpointer.__version__) 30 | 31 | 32 | def main(): 33 | try: 34 | resolve_files() 35 | except KeyboardInterrupt: 36 | sys.exit(1) 37 | 38 | 39 | def parse_pointer(args): 40 | if args.POINTER: 41 | ptr = args.POINTER 42 | elif args.pointer_file: 43 | ptr = args.pointer_file.read().strip() 44 | else: 45 | parser.print_usage() 46 | sys.exit(1) 47 | 48 | return ptr 49 | 50 | 51 | def resolve_files(): 52 | """ Resolve a JSON pointer on JSON files """ 53 | args = parser.parse_args() 54 | 55 | ptr = parse_pointer(args) 56 | 57 | for f in args.FILE: 58 | doc = json.load(f) 59 | try: 60 | result = jsonpointer.resolve_pointer(doc, ptr) 61 | print(json.dumps(result, indent=args.indent)) 62 | except jsonpointer.JsonPointerException as e: 63 | print('Could not resolve pointer: %s' % str(e), file=sys.stderr) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import os.path 5 | import re 6 | 7 | from setuptools import setup 8 | 9 | dirname = os.path.dirname(os.path.abspath(__file__)) 10 | filename = os.path.join(dirname, 'jsonpointer.py') 11 | src = io.open(filename, encoding='utf-8').read() 12 | metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", src)) 13 | docstrings = re.findall('"""(.*)"""', src) 14 | 15 | PACKAGE = 'jsonpointer' 16 | 17 | MODULES = ( 18 | 'jsonpointer', 19 | ) 20 | 21 | AUTHOR_EMAIL = metadata['author'] 22 | VERSION = metadata['version'] 23 | WEBSITE = metadata['website'] 24 | LICENSE = metadata['license'] 25 | DESCRIPTION = docstrings[0] 26 | 27 | # Extract name and e-mail ("Firstname Lastname ") 28 | AUTHOR, EMAIL = re.match(r'(.*) <(.*)>', AUTHOR_EMAIL).groups() 29 | 30 | with open('README.md') as readme: 31 | long_description = readme.read() 32 | 33 | CLASSIFIERS = [ 34 | 'Development Status :: 5 - Production/Stable', 35 | 'Environment :: Console', 36 | 'Intended Audience :: Developers', 37 | 'License :: OSI Approved :: BSD License', 38 | 'Operating System :: OS Independent', 39 | 'Programming Language :: Python', 40 | 'Programming Language :: Python :: 3', 41 | 'Programming Language :: Python :: 3.9', 42 | 'Programming Language :: Python :: 3.10', 43 | 'Programming Language :: Python :: 3.11', 44 | 'Programming Language :: Python :: 3.12', 45 | 'Programming Language :: Python :: Implementation :: CPython', 46 | 'Programming Language :: Python :: Implementation :: PyPy', 47 | 'Topic :: Software Development :: Libraries', 48 | 'Topic :: Utilities', 49 | ] 50 | 51 | setup(name=PACKAGE, 52 | version=VERSION, 53 | description=DESCRIPTION, 54 | long_description=long_description, 55 | long_description_content_type="text/markdown", 56 | author=AUTHOR, 57 | author_email=EMAIL, 58 | license=LICENSE, 59 | url=WEBSITE, 60 | py_modules=MODULES, 61 | scripts=['bin/jsonpointer'], 62 | classifiers=CLASSIFIERS, 63 | python_requires='>=3.9', 64 | ) 65 | -------------------------------------------------------------------------------- /doc/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | Please refer to `RFC 6901 `_ for the exact 5 | pointer syntax. ``jsonpointer`` has two interfaces. The ``resolve_pointer`` 6 | method is basically a deep ``get``. 7 | 8 | .. code-block:: python 9 | 10 | >>> from jsonpointer import resolve_pointer 11 | >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} 12 | 13 | >>> resolve_pointer(obj, '') == obj 14 | True 15 | 16 | >>> resolve_pointer(obj, '/foo') == obj['foo'] 17 | True 18 | 19 | >>> resolve_pointer(obj, '/foo/another prop') == obj['foo']['another prop'] 20 | True 21 | 22 | >>> resolve_pointer(obj, '/foo/another prop/baz') == obj['foo']['another prop']['baz'] 23 | True 24 | 25 | >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0] 26 | True 27 | 28 | >>> resolve_pointer(obj, '/some/path', None) == None 29 | True 30 | 31 | 32 | The ``set_pointer`` method allows modifying a portion of an object using 33 | JSON pointer notation: 34 | 35 | .. code-block:: python 36 | 37 | >>> from jsonpointer import set_pointer 38 | >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} 39 | 40 | >>> set_pointer(obj, '/foo/anArray/0/prop', 55) 41 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} 42 | 43 | >>> obj 44 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} 45 | 46 | By default ``set_pointer`` modifies the original object. Pass ``inplace=False`` 47 | to create a copy and modify the copy instead: 48 | 49 | >>> from jsonpointer import set_pointer 50 | >>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}} 51 | 52 | >>> set_pointer(obj, '/foo/anArray/0/prop', 55, inplace=False) 53 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} 54 | 55 | >>> obj 56 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 44}]}} 57 | 58 | The ``JsonPointer`` class wraps a (string) path and can be used to access the 59 | same path on several objects. 60 | 61 | .. code-block:: python 62 | 63 | >>> import jsonpointer 64 | 65 | >>> pointer = jsonpointer.JsonPointer('/foo/1') 66 | 67 | >>> obj1 = {'foo': ['a', 'b', 'c']} 68 | >>> pointer.resolve(obj1) 69 | 'b' 70 | 71 | >>> obj2 = {'foo': {'0': 1, '1': 10, '2': 100}} 72 | >>> pointer.resolve(obj2) 73 | 10 74 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-json-pointer.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-json-pointer.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-json-pointer" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-json-pointer" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-json-pointer documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Apr 13 16:52:59 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.join(os.path.abspath('.'), '..')) 20 | 21 | import jsonpointer 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.autodoc'] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'python-json-pointer' 46 | copyright = jsonpointer.__author__ 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | 53 | # The short X.Y version. 54 | version = jsonpointer.__version__ 55 | # The full version, including alpha/beta/rc tags. 56 | release = jsonpointer.__version__ 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'python-json-pointerdoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'python-json-pointer.tex', u'python-json-pointer Documentation', 190 | u'Stefan Kögl', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'python-json-pointer', u'python-json-pointer Documentation', 220 | [u'Stefan Kögl'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'python-json-pointer', u'python-json-pointer Documentation', 234 | u'Stefan Kögl', 'python-json-pointer', 'One line description of project.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | -------------------------------------------------------------------------------- /jsonpointer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-json-pointer - An implementation of the JSON Pointer syntax 4 | # https://github.com/stefankoegl/python-json-pointer 5 | # 6 | # Copyright (c) 2011 Stefan Kögl 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions 11 | # are met: 12 | # 13 | # 1. Redistributions of source code must retain the above copyright 14 | # notice, this list of conditions and the following disclaimer. 15 | # 2. Redistributions in binary form must reproduce the above copyright 16 | # notice, this list of conditions and the following disclaimer in the 17 | # documentation and/or other materials provided with the distribution. 18 | # 3. The name of the author may not be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 22 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 23 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 24 | # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 26 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 30 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | # 32 | 33 | """ Identify specific nodes in a JSON document (RFC 6901) """ 34 | 35 | # Will be parsed by setup.py to determine package metadata 36 | __author__ = 'Stefan Kögl ' 37 | __version__ = '3.0.0' 38 | __website__ = 'https://github.com/stefankoegl/python-json-pointer' 39 | __license__ = 'Modified BSD License' 40 | 41 | import copy 42 | import re 43 | from collections.abc import Mapping, Sequence 44 | from itertools import tee, chain 45 | 46 | _nothing = object() 47 | 48 | 49 | def set_pointer(doc, pointer, value, inplace=True): 50 | """Resolves a pointer against doc and sets the value of the target within doc. 51 | 52 | With inplace set to true, doc is modified as long as pointer is not the 53 | root. 54 | 55 | >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}} 56 | 57 | >>> set_pointer(obj, '/foo/anArray/0/prop', 55) == \ 58 | {'foo': {'another prop': {'baz': 'A string'}, 'anArray': [{'prop': 55}]}} 59 | True 60 | 61 | >>> set_pointer(obj, '/foo/yet another prop', 'added prop') == \ 62 | {'foo': {'another prop': {'baz': 'A string'}, 'yet another prop': 'added prop', 'anArray': [{'prop': 55}]}} 63 | True 64 | 65 | >>> obj = {'foo': {}} 66 | >>> set_pointer(obj, '/foo/a%20b', 'x') == \ 67 | {'foo': {'a%20b': 'x' }} 68 | True 69 | """ 70 | 71 | pointer = JsonPointer(pointer) 72 | return pointer.set(doc, value, inplace) 73 | 74 | 75 | def resolve_pointer(doc, pointer, default=_nothing): 76 | """ Resolves pointer against doc and returns the referenced object 77 | 78 | >>> obj = {'foo': {'anArray': [ {'prop': 44}], 'another prop': {'baz': 'A string' }}, 'a%20b': 1, 'c d': 2} 79 | 80 | >>> resolve_pointer(obj, '') == obj 81 | True 82 | 83 | >>> resolve_pointer(obj, '/foo') == obj['foo'] 84 | True 85 | 86 | >>> resolve_pointer(obj, '/foo/another prop') == obj['foo']['another prop'] 87 | True 88 | 89 | >>> resolve_pointer(obj, '/foo/another prop/baz') == obj['foo']['another prop']['baz'] 90 | True 91 | 92 | >>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0] 93 | True 94 | 95 | >>> resolve_pointer(obj, '/some/path', None) == None 96 | True 97 | 98 | >>> resolve_pointer(obj, '/a b', None) == None 99 | True 100 | 101 | >>> resolve_pointer(obj, '/a%20b') == 1 102 | True 103 | 104 | >>> resolve_pointer(obj, '/c d') == 2 105 | True 106 | 107 | >>> resolve_pointer(obj, '/c%20d', None) == None 108 | True 109 | """ 110 | 111 | pointer = JsonPointer(pointer) 112 | return pointer.resolve(doc, default) 113 | 114 | 115 | def pairwise(iterable): 116 | """ Transforms a list to a list of tuples of adjacent items 117 | 118 | s -> (s0,s1), (s1,s2), (s2, s3), ... 119 | 120 | >>> list(pairwise([])) 121 | [] 122 | 123 | >>> list(pairwise([1])) 124 | [] 125 | 126 | >>> list(pairwise([1, 2, 3, 4])) 127 | [(1, 2), (2, 3), (3, 4)] 128 | """ 129 | a, b = tee(iterable) 130 | for _ in b: 131 | break 132 | return zip(a, b) 133 | 134 | 135 | class JsonPointerException(Exception): 136 | pass 137 | 138 | 139 | class EndOfList(object): 140 | """Result of accessing element "-" of a list""" 141 | 142 | def __init__(self, list_): 143 | self.list_ = list_ 144 | 145 | def __repr__(self): 146 | return '{cls}({lst})'.format(cls=self.__class__.__name__, 147 | lst=repr(self.list_)) 148 | 149 | 150 | class JsonPointer(object): 151 | """A JSON Pointer that can reference parts of a JSON document""" 152 | 153 | # Array indices must not contain: 154 | # leading zeros, signs, spaces, decimals, etc 155 | _RE_ARRAY_INDEX = re.compile('0|[1-9][0-9]*$') 156 | _RE_INVALID_ESCAPE = re.compile('(~[^01]|~$)') 157 | 158 | def __init__(self, pointer): 159 | 160 | # validate escapes 161 | invalid_escape = self._RE_INVALID_ESCAPE.search(pointer) 162 | if invalid_escape: 163 | raise JsonPointerException('Found invalid escape {}'.format( 164 | invalid_escape.group())) 165 | 166 | parts = pointer.split('/') 167 | if parts.pop(0) != '': 168 | raise JsonPointerException('Location must start with /') 169 | 170 | parts = [unescape(part) for part in parts] 171 | self.parts = parts 172 | 173 | def to_last(self, doc): 174 | """Resolves ptr until the last step, returns (sub-doc, last-step)""" 175 | 176 | if not self.parts: 177 | return doc, None 178 | 179 | for part in self.parts[:-1]: 180 | doc = self.walk(doc, part) 181 | 182 | return doc, JsonPointer.get_part(doc, self.parts[-1]) 183 | 184 | def resolve(self, doc, default=_nothing): 185 | """Resolves the pointer against doc and returns the referenced object""" 186 | 187 | for part in self.parts: 188 | 189 | try: 190 | doc = self.walk(doc, part) 191 | except JsonPointerException: 192 | if default is _nothing: 193 | raise 194 | else: 195 | return default 196 | 197 | return doc 198 | 199 | get = resolve 200 | 201 | def set(self, doc, value, inplace=True): 202 | """Resolve the pointer against the doc and replace the target with value.""" 203 | 204 | if len(self.parts) == 0: 205 | if inplace: 206 | raise JsonPointerException('Cannot set root in place') 207 | return value 208 | 209 | if not inplace: 210 | doc = copy.deepcopy(doc) 211 | 212 | (parent, part) = self.to_last(doc) 213 | 214 | if isinstance(parent, Sequence) and part == '-': 215 | parent.append(value) 216 | else: 217 | parent[part] = value 218 | 219 | return doc 220 | 221 | @classmethod 222 | def get_part(cls, doc, part): 223 | """Returns the next step in the correct type""" 224 | 225 | if isinstance(doc, Mapping): 226 | return part 227 | 228 | elif isinstance(doc, Sequence): 229 | 230 | if part == '-': 231 | return part 232 | 233 | if not JsonPointer._RE_ARRAY_INDEX.match(str(part)): 234 | raise JsonPointerException("'%s' is not a valid sequence index" % part) 235 | 236 | return int(part) 237 | 238 | elif hasattr(doc, '__getitem__'): 239 | # Allow indexing via ducktyping 240 | # if the target has defined __getitem__ 241 | return part 242 | 243 | else: 244 | raise JsonPointerException("Document '%s' does not support indexing, " 245 | "must be mapping/sequence or support __getitem__" % type(doc)) 246 | 247 | def get_parts(self): 248 | """Returns the list of the parts. For example, JsonPointer('/a/b').get_parts() == ['a', 'b']""" 249 | 250 | return self.parts 251 | 252 | def walk(self, doc, part): 253 | """ Walks one step in doc and returns the referenced part """ 254 | 255 | part = JsonPointer.get_part(doc, part) 256 | 257 | assert hasattr(doc, '__getitem__'), "invalid document type %s" % (type(doc),) 258 | 259 | if isinstance(doc, Sequence): 260 | if part == '-': 261 | return EndOfList(doc) 262 | 263 | try: 264 | return doc[part] 265 | 266 | except IndexError: 267 | raise JsonPointerException("index '%s' is out of bounds" % (part,)) 268 | 269 | # Else the object is a mapping or supports __getitem__(so assume custom indexing) 270 | try: 271 | return doc[part] 272 | 273 | except KeyError: 274 | raise JsonPointerException("member '%s' not found in %s" % (part, doc)) 275 | 276 | def contains(self, ptr): 277 | """ Returns True if self contains the given ptr """ 278 | return self.parts[:len(ptr.parts)] == ptr.parts 279 | 280 | def __contains__(self, item): 281 | """ Returns True if self contains the given ptr """ 282 | return self.contains(item) 283 | 284 | def join(self, suffix): 285 | """ Returns a new JsonPointer with the given suffix append to this ptr """ 286 | if isinstance(suffix, JsonPointer): 287 | suffix_parts = suffix.parts 288 | elif isinstance(suffix, str): 289 | suffix_parts = JsonPointer(suffix).parts 290 | else: 291 | suffix_parts = suffix 292 | try: 293 | return JsonPointer.from_parts(chain(self.parts, suffix_parts)) 294 | except: # noqa E722 295 | raise JsonPointerException("Invalid suffix") 296 | 297 | def __truediv__(self, suffix): # Python 3 298 | return self.join(suffix) 299 | 300 | @property 301 | def path(self): 302 | """Returns the string representation of the pointer 303 | 304 | >>> ptr = JsonPointer('/~0/0/~1').path == '/~0/0/~1' 305 | """ 306 | parts = [escape(part) for part in self.parts] 307 | return ''.join('/' + part for part in parts) 308 | 309 | def __eq__(self, other): 310 | """Compares a pointer to another object 311 | 312 | Pointers can be compared by comparing their strings (or splitted 313 | strings), because no two different parts can point to the same 314 | structure in an object (eg no different number representations) 315 | """ 316 | 317 | if not isinstance(other, JsonPointer): 318 | return False 319 | 320 | return self.parts == other.parts 321 | 322 | def __hash__(self): 323 | return hash(tuple(self.parts)) 324 | 325 | def __str__(self): 326 | return self.path 327 | 328 | def __repr__(self): 329 | return type(self).__name__ + "(" + repr(self.path) + ")" 330 | 331 | @classmethod 332 | def from_parts(cls, parts): 333 | """Constructs a JsonPointer from a list of (unescaped) paths 334 | 335 | >>> JsonPointer.from_parts(['a', '~', '/', 0]).path == '/a/~0/~1/0' 336 | True 337 | """ 338 | parts = [escape(str(part)) for part in parts] 339 | ptr = cls(''.join('/' + part for part in parts)) 340 | return ptr 341 | 342 | 343 | def escape(s): 344 | return s.replace('~', '~0').replace('/', '~1') 345 | 346 | 347 | def unescape(s): 348 | return s.replace('~1', '/').replace('~0', '~') 349 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import copy 5 | import doctest 6 | import unittest 7 | 8 | import jsonpointer 9 | from jsonpointer import resolve_pointer, EndOfList, JsonPointerException, \ 10 | JsonPointer, set_pointer 11 | 12 | 13 | class SpecificationTests(unittest.TestCase): 14 | """ Tests all examples from the JSON Pointer specification """ 15 | 16 | def test_example(self): 17 | doc = { 18 | "foo": ["bar", "baz"], 19 | "": 0, 20 | "a/b": 1, 21 | "c%d": 2, 22 | "e^f": 3, 23 | "g|h": 4, 24 | "i\\j": 5, 25 | "k\"l": 6, 26 | " ": 7, 27 | "m~n": 8 28 | } 29 | 30 | self.assertEqual(resolve_pointer(doc, ""), doc) 31 | self.assertEqual(resolve_pointer(doc, "/foo"), ["bar", "baz"]) 32 | self.assertEqual(resolve_pointer(doc, "/foo/0"), "bar") 33 | self.assertEqual(resolve_pointer(doc, "/"), 0) 34 | self.assertEqual(resolve_pointer(doc, "/a~1b"), 1) 35 | self.assertEqual(resolve_pointer(doc, "/c%d"), 2) 36 | self.assertEqual(resolve_pointer(doc, "/e^f"), 3) 37 | self.assertEqual(resolve_pointer(doc, "/g|h"), 4) 38 | self.assertEqual(resolve_pointer(doc, "/i\\j"), 5) 39 | self.assertEqual(resolve_pointer(doc, "/k\"l"), 6) 40 | self.assertEqual(resolve_pointer(doc, "/ "), 7) 41 | self.assertEqual(resolve_pointer(doc, "/m~0n"), 8) 42 | 43 | def test_eol(self): 44 | doc = { 45 | "foo": ["bar", "baz"] 46 | } 47 | 48 | self.assertTrue(isinstance(resolve_pointer(doc, "/foo/-"), EndOfList)) 49 | self.assertRaises(JsonPointerException, resolve_pointer, doc, "/foo/-/1") 50 | 51 | def test_round_trip(self): 52 | paths = [ 53 | "", 54 | "/foo", 55 | "/foo/0", 56 | "/", 57 | "/a~1b", 58 | "/c%d", 59 | "/e^f", 60 | "/g|h", 61 | "/i\\j", 62 | "/k\"l", 63 | "/ ", 64 | "/m~0n", 65 | '/\xee', 66 | ] 67 | for path in paths: 68 | ptr = JsonPointer(path) 69 | self.assertEqual(path, ptr.path) 70 | 71 | parts = ptr.get_parts() 72 | self.assertEqual(parts, ptr.parts) 73 | new_ptr = JsonPointer.from_parts(parts) 74 | self.assertEqual(ptr, new_ptr) 75 | 76 | def test_str_and_repr(self): 77 | paths = [ 78 | ("", "", "JsonPointer('')"), 79 | ("/foo", "/foo", "JsonPointer('/foo')"), 80 | ("/foo/0", "/foo/0", "JsonPointer('/foo/0')"), 81 | ("/", "/", "JsonPointer('/')"), 82 | ("/a~1b", "/a~1b", "JsonPointer('/a~1b')"), 83 | ("/c%d", "/c%d", "JsonPointer('/c%d')"), 84 | ("/e^f", "/e^f", "JsonPointer('/e^f')"), 85 | ("/g|h", "/g|h", "JsonPointer('/g|h')"), 86 | ("/i\\j", "/i\\j", "JsonPointer('/i\\\\j')"), 87 | ("/k\"l", "/k\"l", "JsonPointer('/k\"l')"), 88 | ("/ ", "/ ", "JsonPointer('/ ')"), 89 | ("/m~0n", "/m~0n", "JsonPointer('/m~0n')"), 90 | ] 91 | for path, ptr_str, ptr_repr in paths: 92 | ptr = JsonPointer(path) 93 | self.assertEqual(path, ptr.path) 94 | self.assertEqual(ptr_str, str(ptr)) 95 | self.assertEqual(ptr_repr, repr(ptr)) 96 | 97 | path = "/\xee" 98 | ptr_str = "/\xee" 99 | ptr_repr = "JsonPointer('/\xee')" 100 | ptr = JsonPointer(path) 101 | self.assertEqual(path, ptr.path) 102 | self.assertEqual(ptr_str, str(ptr)) 103 | self.assertEqual(ptr_repr, repr(ptr)) 104 | 105 | self.assertIsInstance(str(ptr), str) 106 | self.assertIsInstance(repr(ptr), str) 107 | 108 | def test_parts(self): 109 | paths = [ 110 | ("", []), 111 | ("/foo", ['foo']), 112 | ("/foo/0", ['foo', '0']), 113 | ("/", ['']), 114 | ("/a~1b", ['a/b']), 115 | ("/c%d", ['c%d']), 116 | ("/e^f", ['e^f']), 117 | ("/g|h", ['g|h']), 118 | ("/i\\j", ['i\\j']), 119 | ("/k\"l", ['k"l']), 120 | ("/ ", [' ']), 121 | ("/m~0n", ['m~n']), 122 | ('/\xee', ['\xee']), 123 | ] 124 | for path in paths: 125 | ptr = JsonPointer(path[0]) 126 | self.assertEqual(ptr.get_parts(), path[1]) 127 | 128 | 129 | class ComparisonTests(unittest.TestCase): 130 | 131 | def setUp(self): 132 | self.ptr1 = JsonPointer("/a/b/c") 133 | self.ptr2 = JsonPointer("/a/b") 134 | self.ptr3 = JsonPointer("/b/c") 135 | 136 | def test_eq_hash(self): 137 | p1 = JsonPointer("/something/1/b") 138 | p2 = JsonPointer("/something/1/b") 139 | p3 = JsonPointer("/something/1.0/b") 140 | 141 | self.assertEqual(p1, p2) 142 | self.assertNotEqual(p1, p3) 143 | self.assertNotEqual(p2, p3) 144 | 145 | self.assertEqual(hash(p1), hash(p2)) 146 | self.assertNotEqual(hash(p1), hash(p3)) 147 | self.assertNotEqual(hash(p2), hash(p3)) 148 | 149 | # a pointer compares not-equal to objects of other types 150 | self.assertFalse(p1 == "/something/1/b") 151 | 152 | def test_contains(self): 153 | self.assertTrue(self.ptr1.contains(self.ptr2)) 154 | self.assertTrue(self.ptr1.contains(self.ptr1)) 155 | self.assertFalse(self.ptr1.contains(self.ptr3)) 156 | 157 | def test_contains_magic(self): 158 | self.assertTrue(self.ptr2 in self.ptr1) 159 | self.assertTrue(self.ptr1 in self.ptr1) 160 | self.assertFalse(self.ptr3 in self.ptr1) 161 | 162 | def test_join(self): 163 | ptr12a = self.ptr1.join(self.ptr2) 164 | self.assertEqual(ptr12a.path, "/a/b/c/a/b") 165 | 166 | ptr12b = self.ptr1.join(self.ptr2.parts) 167 | self.assertEqual(ptr12b.path, "/a/b/c/a/b") 168 | 169 | ptr12c = self.ptr1.join(self.ptr2.parts[0:1]) 170 | self.assertEqual(ptr12c.path, "/a/b/c/a") 171 | 172 | ptr12d = self.ptr1.join("/a/b") 173 | self.assertEqual(ptr12d.path, "/a/b/c/a/b") 174 | 175 | ptr12e = self.ptr1.join(["a", "b"]) 176 | self.assertEqual(ptr12e.path, "/a/b/c/a/b") 177 | 178 | self.assertRaises(JsonPointerException, self.ptr1.join, 0) 179 | 180 | def test_join_magic(self): 181 | ptr12a = self.ptr1 / self.ptr2 182 | self.assertEqual(ptr12a.path, "/a/b/c/a/b") 183 | 184 | ptr12b = self.ptr1 / self.ptr2.parts 185 | self.assertEqual(ptr12b.path, "/a/b/c/a/b") 186 | 187 | ptr12c = self.ptr1 / self.ptr2.parts[0:1] 188 | self.assertEqual(ptr12c.path, "/a/b/c/a") 189 | 190 | ptr12d = self.ptr1 / "/a/b" 191 | self.assertEqual(ptr12d.path, "/a/b/c/a/b") 192 | 193 | ptr12e = self.ptr1 / ["a", "b"] 194 | self.assertEqual(ptr12e.path, "/a/b/c/a/b") 195 | 196 | 197 | class WrongInputTests(unittest.TestCase): 198 | 199 | def test_no_start_slash(self): 200 | # an exception is raised when the pointer string does not start with / 201 | self.assertRaises(JsonPointerException, JsonPointer, 'some/thing') 202 | 203 | def test_invalid_index(self): 204 | # 'a' is not a valid list index 205 | doc = [0, 1, 2] 206 | self.assertRaises(JsonPointerException, resolve_pointer, doc, '/a') 207 | 208 | def test_oob(self): 209 | # this list does not have 10 members 210 | doc = [0, 1, 2] 211 | self.assertRaises(JsonPointerException, resolve_pointer, doc, '/10') 212 | 213 | def test_trailing_escape(self): 214 | self.assertRaises(JsonPointerException, JsonPointer, '/foo/bar~') 215 | 216 | def test_invalid_escape(self): 217 | self.assertRaises(JsonPointerException, JsonPointer, '/foo/bar~2') 218 | 219 | 220 | class ToLastTests(unittest.TestCase): 221 | 222 | def test_empty_path(self): 223 | doc = {'a': [1, 2, 3]} 224 | ptr = JsonPointer('') 225 | last, nxt = ptr.to_last(doc) 226 | self.assertEqual(doc, last) 227 | self.assertTrue(nxt is None) 228 | 229 | def test_path(self): 230 | doc = {'a': [{'b': 1, 'c': 2}, 5]} 231 | ptr = JsonPointer('/a/0/b') 232 | last, nxt = ptr.to_last(doc) 233 | self.assertEqual(last, {'b': 1, 'c': 2}) 234 | self.assertEqual(nxt, 'b') 235 | 236 | 237 | class SetTests(unittest.TestCase): 238 | 239 | def test_set(self): 240 | doc = { 241 | "foo": ["bar", "baz"], 242 | "": 0, 243 | "a/b": 1, 244 | "c%d": 2, 245 | "e^f": 3, 246 | "g|h": 4, 247 | "i\\j": 5, 248 | "k\"l": 6, 249 | " ": 7, 250 | "m~n": 8 251 | } 252 | origdoc = copy.deepcopy(doc) 253 | 254 | # inplace=False 255 | newdoc = set_pointer(doc, "/foo/1", "cod", inplace=False) 256 | self.assertEqual(resolve_pointer(newdoc, "/foo/1"), "cod") 257 | 258 | self.assertEqual(len(doc["foo"]), 2) 259 | newdoc = set_pointer(doc, "/foo/-", "xyz", inplace=False) 260 | self.assertEqual(resolve_pointer(newdoc, "/foo/2"), "xyz") 261 | self.assertEqual(len(doc["foo"]), 2) 262 | self.assertEqual(len(newdoc["foo"]), 3) 263 | 264 | newdoc = set_pointer(doc, "/", 9, inplace=False) 265 | self.assertEqual(resolve_pointer(newdoc, "/"), 9) 266 | 267 | newdoc = set_pointer(doc, "/fud", {}, inplace=False) 268 | newdoc = set_pointer(newdoc, "/fud/gaw", [1, 2, 3], inplace=False) 269 | self.assertEqual(resolve_pointer(newdoc, "/fud"), {'gaw': [1, 2, 3]}) 270 | 271 | newdoc = set_pointer(doc, "", 9, inplace=False) 272 | self.assertEqual(newdoc, 9) 273 | 274 | self.assertEqual(doc, origdoc) 275 | 276 | # inplace=True 277 | set_pointer(doc, "/foo/1", "cod") 278 | self.assertEqual(resolve_pointer(doc, "/foo/1"), "cod") 279 | 280 | self.assertEqual(len(doc["foo"]), 2) 281 | set_pointer(doc, "/foo/-", "xyz") 282 | self.assertEqual(resolve_pointer(doc, "/foo/2"), "xyz") 283 | self.assertEqual(len(doc["foo"]), 3) 284 | 285 | set_pointer(doc, "/", 9) 286 | self.assertEqual(resolve_pointer(doc, "/"), 9) 287 | 288 | self.assertRaises(JsonPointerException, set_pointer, doc, "/fud/gaw", 9) 289 | 290 | set_pointer(doc, "/fud", {}) 291 | set_pointer(doc, "/fud/gaw", [1, 2, 3]) 292 | self.assertEqual(resolve_pointer(doc, "/fud"), {'gaw': [1, 2, 3]}) 293 | 294 | self.assertRaises(JsonPointerException, set_pointer, doc, "", 9) 295 | 296 | 297 | class AltTypesTests(unittest.TestCase): 298 | class Node(object): 299 | def __init__(self, name, parent=None): 300 | self.name = name 301 | self.parent = parent 302 | self.left = None 303 | self.right = None 304 | 305 | def set_left(self, node): 306 | node.parent = self 307 | self.left = node 308 | 309 | def set_right(self, node): 310 | node.parent = self 311 | self.right = node 312 | 313 | def __getitem__(self, key): 314 | if key == 'left': 315 | return self.left 316 | if key == 'right': 317 | return self.right 318 | 319 | raise KeyError("Only left and right supported") 320 | 321 | def __setitem__(self, key, val): 322 | if key == 'left': 323 | return self.set_left(val) 324 | if key == 'right': 325 | return self.set_right(val) 326 | 327 | raise KeyError("Only left and right supported: %s" % key) 328 | 329 | class mdict(object): 330 | def __init__(self, d): 331 | self._d = d 332 | 333 | def __getitem__(self, item): 334 | return self._d[item] 335 | 336 | mdict = mdict({'root': {'1': {'2': '3'}}}) 337 | Node = Node 338 | 339 | def test_alttypes(self): 340 | Node = self.Node 341 | 342 | root = Node('root') 343 | root.set_left(Node('a')) 344 | root.left.set_left(Node('aa')) 345 | root.left.set_right(Node('ab')) 346 | root.set_right(Node('b')) 347 | root.right.set_left(Node('ba')) 348 | root.right.set_right(Node('bb')) 349 | 350 | self.assertEqual(resolve_pointer(root, '/left').name, 'a') 351 | self.assertEqual(resolve_pointer(root, '/left/right').name, 'ab') 352 | self.assertEqual(resolve_pointer(root, '/right').name, 'b') 353 | self.assertEqual(resolve_pointer(root, '/right/left').name, 'ba') 354 | 355 | newroot = set_pointer(root, '/left/right', Node('AB'), inplace=False) 356 | self.assertEqual(resolve_pointer(root, '/left/right').name, 'ab') 357 | self.assertEqual(resolve_pointer(newroot, '/left/right').name, 'AB') 358 | 359 | set_pointer(root, '/left/right', Node('AB')) 360 | self.assertEqual(resolve_pointer(root, '/left/right').name, 'AB') 361 | 362 | def test_mock_dict_sanity(self): 363 | doc = self.mdict 364 | default = None 365 | 366 | # TODO: Generate this automatically for any given object 367 | path_to_expected_value = { 368 | '/root/1': {'2': '3'}, 369 | '/root': {'1': {'2': '3'}}, 370 | '/root/1/2': '3', 371 | } 372 | 373 | for path, expected_value in iter(path_to_expected_value.items()): 374 | self.assertEqual(resolve_pointer(doc, path, default), expected_value) 375 | 376 | def test_mock_dict_returns_default(self): 377 | doc = self.mdict 378 | default = None 379 | 380 | path_to_expected_value = { 381 | '/foo': default, 382 | '/x/y/z/d': default 383 | } 384 | 385 | for path, expected_value in iter(path_to_expected_value.items()): 386 | self.assertEqual(resolve_pointer(doc, path, default), expected_value) 387 | 388 | def test_mock_dict_raises_key_error(self): 389 | doc = self.mdict 390 | self.assertRaises(JsonPointerException, resolve_pointer, doc, '/foo') 391 | self.assertRaises(JsonPointerException, resolve_pointer, doc, '/root/1/2/3/4') 392 | 393 | 394 | def load_tests(loader, tests, ignore): 395 | tests.addTests(doctest.DocTestSuite(jsonpointer)) 396 | return tests 397 | --------------------------------------------------------------------------------