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 index fa172c1..7ec5232 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -39,7 +39,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] steps: - uses: actions/checkout@v4 diff --git a/bin/jsonpointer b/bin/jsonpointer index ba2117c..c9b0477 100755 --- a/bin/jsonpointer +++ b/bin/jsonpointer @@ -1,59 +1,71 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - import argparse import json +import os import sys 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) - -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, - help='Indent output by n spaces') -parser.add_argument('-v', '--version', action='version', - version='%(prog)s ' + jsonpointer.__version__) - - def main(): + # First handle backward compatibility for v1 command line syntax: + # jsonpointer ptr.json file.json + if len(sys.argv) >= 3 and not sys.argv[1].startswith('-'): + # Check if the first argument is a file path + if os.path.isfile(sys.argv[1]): + # Insert -f option before the first argument + sys.argv.insert(1, '-f') + + parser = argparse.ArgumentParser( + description='Resolve a JSON pointer on JSON files') + + # Use mutually exclusive group for pointer specification + pointer_group = parser.add_mutually_exclusive_group(required=True) + pointer_group.add_argument('-e', '--expression', + help='A JSON pointer expression (e.g. "/foo/bar")') + pointer_group.add_argument('-f', '--pointer-file', metavar='POINTER_FILE', + help='File containing 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, + help='Indent output by n spaces') + parser.add_argument('-v', '--version', action='version', + version='%(prog)s ' + jsonpointer.__version__) + + args = parser.parse_args() + try: - resolve_files() + resolve_files(args) except KeyboardInterrupt: sys.exit(1) -def parse_pointer(args): - if args.POINTER: - ptr = args.POINTER +def resolve_files(args): + """Resolve a JSON pointer on JSON files""" + # Get pointer from expression or file + if args.expression: + ptr = args.expression elif args.pointer_file: - ptr = args.pointer_file.read().strip() + try: + with open(args.pointer_file) as f: + content = f.read().strip() + # Handle JSON-encoded strings + if content.startswith('"') and content.endswith('"'): + try: + ptr = json.loads(content) + except json.JSONDecodeError: + ptr = content + else: + ptr = content + except FileNotFoundError: + print(f"Error: Pointer file '{args.pointer_file}' not found", file=sys.stderr) + sys.exit(1) else: - parser.print_usage() - sys.exit(1) - - return ptr - - -def resolve_files(): - """ Resolve a JSON pointer on JSON files """ - args = parser.parse_args() - - ptr = parse_pointer(args) + sys.exit(1) # This should never happen because the group is required + # Process each file for f in args.FILE: doc = json.load(f) try: diff --git a/doc/commandline.rst b/doc/commandline.rst index b4a01de..5d15468 100644 --- a/doc/commandline.rst +++ b/doc/commandline.rst @@ -6,18 +6,26 @@ 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] (-e EXPRESSION | -f POINTER_FILE) [--indent INDENT] + [-v] + 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 + 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 + -e EXPRESSION, --expression EXPRESSION + A JSON pointer expression (e.g. "/foo/bar") + -f POINTER_FILE, --pointer-file POINTER_FILE + File containing a JSON pointer expression + --indent INDENT Indent output by n spaces + -v, --version show program's version number and exit + +For backward compatibility, if the first argument is a file path, it is treated as +if `-f` was specified, allowing the command to be used as in previous versions. Example @@ -36,7 +44,17 @@ Example $ cat ptr.json "/a" - # resolve JSON pointer + # resolve JSON pointer (version 1 compatible syntax) $ jsonpointer ptr.json a.json b.json [1, 2, 3] {"b": [1, 3, 4]} + + # resolve with -f option + $ jsonpointer -f ptr.json a.json b.json + [1, 2, 3] + {"b": [1, 3, 4]} + + # resolve with -e option for direct expression + $ jsonpointer -e "/a" a.json b.json + [1, 2, 3] + {"b": [1, 3, 4]} diff --git a/doc/index.rst b/doc/index.rst index 53b1dea..7d079e8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -7,7 +7,7 @@ python-json-pointer =================== *python-json-pointer* is a Python library for resolving JSON pointers (`RFC -6901 `_). Python 2.7, 3.4+ +6901 `_). Python 3.9+ and PyPy are supported. **Contents** diff --git a/setup.py b/setup.py index 3e87a4c..75f5426 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,6 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', @@ -62,5 +60,5 @@ py_modules=MODULES, scripts=['bin/jsonpointer'], classifiers=CLASSIFIERS, - python_requires='>=3.7', + python_requires='>=3.9', ) diff --git a/tests.py b/tests.py index 7b1cdac..668982d 100755 --- a/tests.py +++ b/tests.py @@ -1,11 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import unicode_literals - import copy import doctest -import sys import unittest import jsonpointer @@ -78,45 +75,33 @@ def test_round_trip(self): def test_str_and_repr(self): paths = [ - ("", "", "JsonPointer({u}'')"), - ("/foo", "/foo", "JsonPointer({u}'/foo')"), - ("/foo/0", "/foo/0", "JsonPointer({u}'/foo/0')"), - ("/", "/", "JsonPointer({u}'/')"), - ("/a~1b", "/a~1b", "JsonPointer({u}'/a~1b')"), - ("/c%d", "/c%d", "JsonPointer({u}'/c%d')"), - ("/e^f", "/e^f", "JsonPointer({u}'/e^f')"), - ("/g|h", "/g|h", "JsonPointer({u}'/g|h')"), - ("/i\\j", "/i\\j", "JsonPointer({u}'/i\\\\j')"), - ("/k\"l", "/k\"l", "JsonPointer({u}'/k\"l')"), - ("/ ", "/ ", "JsonPointer({u}'/ ')"), - ("/m~0n", "/m~0n", "JsonPointer({u}'/m~0n')"), + ("", "", "JsonPointer('')"), + ("/foo", "/foo", "JsonPointer('/foo')"), + ("/foo/0", "/foo/0", "JsonPointer('/foo/0')"), + ("/", "/", "JsonPointer('/')"), + ("/a~1b", "/a~1b", "JsonPointer('/a~1b')"), + ("/c%d", "/c%d", "JsonPointer('/c%d')"), + ("/e^f", "/e^f", "JsonPointer('/e^f')"), + ("/g|h", "/g|h", "JsonPointer('/g|h')"), + ("/i\\j", "/i\\j", "JsonPointer('/i\\\\j')"), + ("/k\"l", "/k\"l", "JsonPointer('/k\"l')"), + ("/ ", "/ ", "JsonPointer('/ ')"), + ("/m~0n", "/m~0n", "JsonPointer('/m~0n')"), ] for path, ptr_str, ptr_repr in paths: ptr = JsonPointer(path) self.assertEqual(path, ptr.path) - - if sys.version_info[0] == 2: - u_str = "u" - else: - u_str = "" self.assertEqual(ptr_str, str(ptr)) - self.assertEqual(ptr_repr.format(u=u_str), repr(ptr)) - - if sys.version_info[0] == 2: - path = "/\xee" - ptr_str = b"/\xee" - ptr_repr = "JsonPointer(u'/\\xee')" - else: - path = "/\xee" - ptr_str = "/\xee" - ptr_repr = "JsonPointer('/\xee')" + self.assertEqual(ptr_repr, repr(ptr)) + + path = "/\xee" + ptr_str = "/\xee" + ptr_repr = "JsonPointer('/\xee')" ptr = JsonPointer(path) self.assertEqual(path, ptr.path) - self.assertEqual(ptr_str, str(ptr)) self.assertEqual(ptr_repr, repr(ptr)) - # should not be unicode in Python 2 self.assertIsInstance(str(ptr), str) self.assertIsInstance(repr(ptr), str)