Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 53 additions & 15 deletions bin/jsonpointer
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ import jsonpointer
parser = argparse.ArgumentParser(
description='Resolve a JSON pointer on JSON files')

# Accept pointer as argument or as file
ptr_group = parser.add_mutually_exclusive_group(required=True)
# Accept pointer as option, as file or as first positional argument
ptr_group = parser.add_mutually_exclusive_group()

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('-p', '--pointer', type=str,
help='A JSON pointer expression')

parser.add_argument('FILE', type=argparse.FileType('r'), nargs='+',
parser.add_argument('POINTER', type=str, nargs='?',
help='A JSON pointer expression (if neither -f nor -p '
'is given)')

parser.add_argument('FILE', type=str, nargs='+',
help='Files for which the pointer should be resolved')
parser.add_argument('--indent', type=int, default=None,
help='Indent output by n spaces')
Expand All @@ -30,36 +33,71 @@ parser.add_argument('-v', '--version', action='version',

def main():
try:
resolve_files()
ok = resolve_files()
except KeyboardInterrupt:
sys.exit(1)

if not ok:
sys.exit(1)


def parse_pointer(args):
if args.POINTER:
""" Return the pointer and the list of files to resolve it on

If the pointer is given via -f or -p, a POINTER positional argument is
actually the first file. """
files = args.FILE

if args.pointer is not None or args.pointer_file is not None:
if args.POINTER is not None:
files = [args.POINTER] + files

if args.pointer is not None:
ptr = args.pointer
else:
ptr = args.pointer_file.read().strip()

elif args.POINTER is not None:
ptr = args.POINTER
elif args.pointer_file:
ptr = args.pointer_file.read().strip()

else:
parser.print_usage()
sys.exit(1)
parser.error('a JSON pointer is required (POINTER, -p or -f)')

# Catches e.g. `jsonpointer a.json b.json`, where the pointer was omitted
# and the first file would otherwise be used as the pointer
try:
jsonpointer.JsonPointer(ptr)
except jsonpointer.JsonPointerException as e:
parser.error('invalid JSON pointer %r: %s' % (ptr, e))

return ptr
return ptr, files


def resolve_files():
""" Resolve a JSON pointer on JSON files """
""" Resolve a JSON pointer on JSON files

Returns False if the pointer could not be resolved on at least one
file. """
args = parser.parse_args()

ptr = parse_pointer(args)
ptr, files = parse_pointer(args)
ok = True

for name in files:
try:
f = argparse.FileType('r')(name)
except argparse.ArgumentTypeError as e:
parser.error(str(e))

for f in args.FILE:
doc = json.load(f)
try:
result = jsonpointer.resolve_pointer(doc, ptr)
print(json.dumps(result, indent=args.indent))
except jsonpointer.JsonPointerException as e:
print('Could not resolve pointer: %s' % str(e), file=sys.stderr)
ok = False

return ok


if __name__ == "__main__":
Expand Down
46 changes: 34 additions & 12 deletions doc/commandline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,32 @@ that can be used to resolve a JSON pointers on JSON files.

The program has the following usage ::

usage: jsonpointer [-h] [--indent INDENT] [-v] POINTER FILE [FILE ...]
usage: jsonpointer [-h] [-f POINTER_FILE | -p POINTER] [--indent INDENT] [-v]
[POINTER] FILE [FILE ...]

Resolve a JSON pointer on JSON files

positional arguments:
POINTER File containing a JSON pointer expression
FILE Files for which the pointer should be resolved
POINTER A JSON pointer expression (if neither -f nor -p is
given)
FILE Files for which the pointer should be resolved

optional arguments:
-h, --help show this help message and exit
--indent INDENT Indent output by n spaces
-v, --version show program's version number and exit
options:
-h, --help show this help message and exit
-f POINTER_FILE, --pointer-file POINTER_FILE
File containing a JSON pointer expression
-p POINTER, --pointer POINTER
A JSON pointer expression
--indent INDENT Indent output by n spaces
-v, --version show program's version number and exit

The pointer can be passed as the first positional argument, with ``-p``, or
read from a file with ``-f``.

The exit status is 0 if the pointer could be resolved on all files, 1 if it
could not be resolved on at least one file (the remaining files are still
processed) and 2 on usage errors, such as an invalid pointer or a missing
file.

Example
^^^^^^^
Expand All @@ -32,11 +45,20 @@ Example
$ cat b.json
{ "a": {"b": [1, 3, 4]}, "b": 1 }

# inspect JSON pointer
$ cat ptr.json
"/a"

# resolve JSON pointer
$ jsonpointer ptr.json a.json b.json
$ jsonpointer /a a.json b.json
[1, 2, 3]
{"b": [1, 3, 4]}

# same, using the -p option
$ jsonpointer -p /a a.json b.json
[1, 2, 3]
{"b": [1, 3, 4]}

# same, reading the pointer from a file
$ cat ptr.txt
/a

$ jsonpointer -f ptr.txt a.json b.json
[1, 2, 3]
{"b": [1, 3, 4]}
103 changes: 103 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import copy
import doctest
import os
import subprocess
import sys
import tempfile
import unittest

import jsonpointer
Expand Down Expand Up @@ -468,6 +471,106 @@ def test_mock_dict_raises_key_error(self):
self.assertRaises(JsonPointerException, resolve_pointer, doc, '/root/1/2/3/4')


class CommandLineTests(unittest.TestCase):
""" Tests the jsonpointer command line utility """

ROOT = os.path.dirname(os.path.abspath(__file__))
SCRIPT = os.path.join(ROOT, 'bin', 'jsonpointer')

A_OUT = '[1, 2, 3]\n'
B_OUT = '{"b": [1, 3, 4]}\n'

def setUp(self):
tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(tmpdir.cleanup)
self.dir = tmpdir.name

self._write('a.json', '{ "a": [1, 2, 3] }')
self._write('b.json', '{ "a": {"b": [1, 3, 4]}, "b": 1 }')
self._write('ptr.txt', '/a\n')

def _write(self, name, content):
with open(os.path.join(self.dir, name), 'w') as f:
f.write(content)

def _run(self, *args):
env = dict(os.environ, PYTHONPATH=self.ROOT)
return subprocess.run([sys.executable, self.SCRIPT] + list(args),
cwd=self.dir, env=env, capture_output=True,
text=True)

def test_positional_pointer(self):
proc = self._run('/a', 'a.json', 'b.json')
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)

def test_positional_pointer_single_file(self):
proc = self._run('/a', 'a.json')
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(proc.stdout, self.A_OUT)

def test_pointer_file(self):
proc = self._run('-f', 'ptr.txt', 'a.json', 'b.json')
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)

def test_pointer_file_single_file(self):
proc = self._run('-f', 'ptr.txt', 'a.json')
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(proc.stdout, self.A_OUT)

def test_pointer_option(self):
proc = self._run('-p', '/a', 'a.json', 'b.json')
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(proc.stdout, self.A_OUT + self.B_OUT)

def test_no_pointer(self):
proc = self._run('a.json')
self.assertEqual(proc.returncode, 2)
self.assertIn('a JSON pointer is required', proc.stderr)

def test_missing_pointer_multiple_files(self):
proc = self._run('a.json', 'b.json')
self.assertEqual(proc.returncode, 2)
self.assertIn("invalid JSON pointer 'a.json'", proc.stderr)
self.assertEqual(proc.stdout, '')

def test_invalid_pointer_file(self):
self._write('bad.txt', 'a\n')
proc = self._run('-f', 'bad.txt', 'a.json')
self.assertEqual(proc.returncode, 2)
self.assertIn("invalid JSON pointer 'a'", proc.stderr)

def test_empty_pointer(self):
proc = self._run('', 'a.json')
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(proc.stdout, '{"a": [1, 2, 3]}\n')

def test_unresolvable_pointer(self):
proc = self._run('/x', 'a.json')
self.assertEqual(proc.returncode, 1)
self.assertIn('Could not resolve pointer', proc.stderr)
self.assertEqual(proc.stdout, '')

def test_unresolvable_pointer_in_one_file(self):
# /b resolves in b.json only; a.json is still reported but b.json
# is processed
proc = self._run('/b', 'a.json', 'b.json')
self.assertEqual(proc.returncode, 1)
self.assertIn('Could not resolve pointer', proc.stderr)
self.assertEqual(proc.stdout, '1\n')

def test_pointer_file_requires_value(self):
proc = self._run('-f', '-p', '/a', 'a.json')
self.assertEqual(proc.returncode, 2)
self.assertIn('expected one argument', proc.stderr)

def test_pointer_and_pointer_file_exclusive(self):
proc = self._run('-p', '/a', '-f', 'ptr.txt', 'a.json')
self.assertEqual(proc.returncode, 2)
self.assertIn('not allowed with', proc.stderr)


class VerboseExceptionsTests(unittest.TestCase):

def setUp(self):
Expand Down
Loading