Skip to content
Open
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
53 changes: 41 additions & 12 deletions bin/jsonpointer
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ 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 @@ -36,24 +40,49 @@ def main():


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 """
args = parser.parse_args()

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

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)
Expand Down
46 changes: 32 additions & 14 deletions doc/commandline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,28 @@ 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

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

POINTER A JSON pointer expression (if neither -f nor -p is
given)
FILE Files for which the pointer should be resolved

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``.

Example
^^^^^^^
Expand All @@ -32,11 +41,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]}
84 changes: 84 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,87 @@ 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_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