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
8 changes: 4 additions & 4 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
- uses: actions/checkout@v7
- name: Set up Python 3.12
uses: actions/setup-python@v7
with:
python-version: "3.10"
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/tests-no-regex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
with:
submodules: true
- name: Set up Python 3.11
uses: actions/setup-python@v4
- name: Set up Python 3.12
uses: actions/setup-python@v7
with:
python-version: "3.11"
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
exclude:
- os: macos-latest
python-version: "3.10"
Expand All @@ -20,11 +20,11 @@ jobs:
- os: windows-latest
python-version: "3.11"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/typing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
- uses: actions/checkout@v7
- name: Set up Python 3.12
uses: actions/setup-python@v7
with:
python-version: "3.11"
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
109 changes: 74 additions & 35 deletions jsonpath/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
import argparse
import json
import sys
from contextlib import AbstractContextManager
from contextlib import nullcontext
from typing import IO
from typing import Any
from typing import BinaryIO
from typing import Literal
from typing import TextIO
from typing import overload

import jsonpath
from jsonpath.__about__ import __version__
Expand All @@ -27,16 +35,15 @@ def path_sub_command(parser: argparse.ArgumentParser) -> None: # noqa: D103

group.add_argument(
"-r",
"--path-file",
type=argparse.FileType(mode="r"),
"--query-file",
default="-",
help="Text file containing a JSONPath query.",
)

parser.add_argument(
"-f",
"--file",
type=argparse.FileType(mode="rb"),
default=sys.stdin,
default="-",
help=(
"File to read the target JSON document from. "
"Defaults to reading from the standard input stream."
Expand All @@ -46,8 +53,7 @@ def path_sub_command(parser: argparse.ArgumentParser) -> None: # noqa: D103
parser.add_argument(
"-o",
"--output",
type=argparse.FileType(mode="w"),
default=sys.stdout,
default="-",
help=(
"File to write resulting objects to, as a JSON array. "
"Defaults to the standard output stream."
Expand Down Expand Up @@ -83,15 +89,14 @@ def pointer_sub_command(parser: argparse.ArgumentParser) -> None: # noqa: D103
group.add_argument(
"-r",
"--pointer-file",
type=argparse.FileType(mode="r"),
default="-",
help="Text file containing an RFC 6901 formatted JSON Pointer string.",
)

parser.add_argument(
"-f",
"--file",
type=argparse.FileType(mode="rb"),
default=sys.stdin,
default="-",
help=(
"File to read the target JSON document from. "
"Defaults to reading from the standard input stream."
Expand All @@ -101,8 +106,7 @@ def pointer_sub_command(parser: argparse.ArgumentParser) -> None: # noqa: D103
parser.add_argument(
"-o",
"--output",
type=argparse.FileType(mode="w"),
default=sys.stdout,
default="-",
help=(
"File to write the resulting object to, in JSON format. "
"Defaults to the standard output stream."
Expand All @@ -122,16 +126,15 @@ def patch_sub_command(parser: argparse.ArgumentParser) -> None: # noqa: D103

parser.add_argument(
"patch",
type=argparse.FileType(mode="rb"),
default="-",
metavar="PATCH",
help="File containing an RFC 6902 formatted JSON Patch.",
)

parser.add_argument(
"-f",
"--file",
type=argparse.FileType(mode="rb"),
default=sys.stdin,
default="-",
help=(
"File to read the target JSON document from. "
"Defaults to reading from the standard input stream."
Expand All @@ -141,8 +144,7 @@ def patch_sub_command(parser: argparse.ArgumentParser) -> None: # noqa: D103
parser.add_argument(
"-o",
"--output",
type=argparse.FileType(mode="w"),
default=sys.stdout,
default="-",
help=(
"File to write the resulting JSON document to. "
"Defaults to the standard output stream."
Expand Down Expand Up @@ -252,7 +254,8 @@ def handle_path_command(args: argparse.Namespace) -> None: # noqa: PLR0912
if args.query is not None:
query = args.query
else:
query = args.query_file.read().strip()
with open_input(args.query_file, "r") as fd:
query = fd.read().strip()

try:
path = jsonpath.JSONPathEnvironment(
Expand All @@ -277,7 +280,8 @@ def handle_path_command(args: argparse.Namespace) -> None: # noqa: PLR0912
sys.exit(1)

try:
matches = path.findall(args.file)
with open_input(args.file, "rb") as fd:
matches = path.findall(json.load(fd))
except json.JSONDecodeError as err:
if args.debug:
raise
Expand All @@ -291,7 +295,9 @@ def handle_path_command(args: argparse.Namespace) -> None: # noqa: PLR0912
sys.exit(1)

indent = INDENT if args.pretty else None
json.dump(matches, args.output, indent=indent)

with open_output(args.output) as of:
json.dump(matches, of, indent=indent)


def handle_pointer_command(args: argparse.Namespace) -> None:
Expand All @@ -300,15 +306,17 @@ def handle_pointer_command(args: argparse.Namespace) -> None:
if args.pointer is not None:
pointer = args.pointer
else:
pointer = args.pointer_file.read().strip()
with open_input(args.pointer_file, "r") as fd:
pointer = fd.read().strip()

try:
match = jsonpath.pointer.resolve(
pointer,
args.file,
unicode_escape=not args.no_unicode_escape,
uri_decode=args.uri_decode,
)
with open_input(args.file, "rb") as fd:
match = jsonpath.pointer.resolve(
pointer,
json.load(fd),
unicode_escape=not args.no_unicode_escape,
uri_decode=args.uri_decode,
)
except json.JSONDecodeError as err:
if args.debug:
raise
Expand All @@ -321,13 +329,16 @@ def handle_pointer_command(args: argparse.Namespace) -> None:
sys.exit(1)

indent = INDENT if args.pretty else None
json.dump(match, args.output, indent=indent)

with open_output(args.output) as of:
json.dump(match, of, indent=indent)


def handle_patch_command(args: argparse.Namespace) -> None:
"""Handle the `patch` sub command."""
try:
patch = json.load(args.patch)
with open_input(args.patch, "rb") as fd:
patch = json.load(fd)
except json.JSONDecodeError as err:
if args.debug:
raise
Expand All @@ -341,12 +352,13 @@ def handle_patch_command(args: argparse.Namespace) -> None:
sys.exit(1)

try:
patched = jsonpath.patch.apply(
patch,
args.file,
unicode_escape=not args.no_unicode_escape,
uri_decode=args.uri_decode,
)
with open_input(args.file, "rb") as fd:
patched = jsonpath.patch.apply(
patch, # type: ignore
json.load(fd),
unicode_escape=not args.no_unicode_escape,
uri_decode=args.uri_decode,
)
except json.JSONDecodeError as err:
if args.debug:
raise
Expand All @@ -359,7 +371,34 @@ def handle_patch_command(args: argparse.Namespace) -> None:
sys.exit(1)

indent = INDENT if args.pretty else None
json.dump(patched, args.output, indent=indent)

with open_output(args.output) as of:
json.dump(patched, of, indent=indent)


@overload
def open_input(filename: str, mode: Literal["r"]) -> AbstractContextManager[TextIO]: ...


@overload
def open_input(
filename: str, mode: Literal["rb"]
) -> AbstractContextManager[BinaryIO]: ...


def open_input(
filename: str, mode: Literal["r", "rb"]
) -> AbstractContextManager[IO[Any]]:
"""A context manager for opening input files from command line arguments."""
if filename == "-":
stream = sys.stdin if mode == "r" else sys.stdin.buffer
return nullcontext(stream)
return open(filename, mode)


def open_output(filename: str) -> AbstractContextManager[TextIO]:
"""A context manager for opening output files from command line arguments."""
return nullcontext(sys.stdout) if filename == "-" else open(filename, "w")


def main() -> None:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
Expand Down Expand Up @@ -77,7 +78,7 @@ lint = "ruff check ."
typing = "mypy"

[[tool.hatch.envs.test.matrix]]
python = ["38", "39", "310", "311", "312", "313", "314", "pypy38", "pypy39"]
python = ["38", "39", "310", "311", "312", "313", "314", "315", "pypy38", "pypy39"]

[tool.hatch.envs.test.scripts]
test = "pytest {args}"
Expand Down
9 changes: 2 additions & 7 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import json
import pathlib
from typing import Dict

import pytest

Expand All @@ -18,7 +19,7 @@
from jsonpath.exceptions import JSONPointerResolutionError
from jsonpath.patch import JSONPatch

SAMPLE_DATA = {
SAMPLE_DATA: Dict[str, object] = {
"categories": [
{
"name": "footwear",
Expand Down Expand Up @@ -286,7 +287,6 @@ def test_json_path(
)

handle_path_command(args)
args.output.flush()

with open(outfile, "r") as fd:
assert len(json.load(fd)) == 4 # noqa: PLR2004
Expand Down Expand Up @@ -329,7 +329,6 @@ def test_json_path_strict(
)

handle_path_command(args)
args.output.flush()

with open(outfile, "r") as fd:
rv = json.load(fd)
Expand Down Expand Up @@ -400,7 +399,6 @@ def test_json_pointer(
)

handle_pointer_command(args)
args.output.flush()

with open(outfile, "r") as fd:
assert json.load(fd) == "footwear"
Expand All @@ -413,7 +411,6 @@ def test_json_pointer_empty_string(
args = parser.parse_args(["pointer", "-p", "", "-f", sample_target, "-o", outfile])

handle_pointer_command(args)
args.output.flush()

with open(outfile, "r") as fd:
assert json.load(fd) == SAMPLE_DATA
Expand All @@ -435,7 +432,6 @@ def test_read_pointer_from_file(
)

handle_pointer_command(args)
args.output.flush()

with open(outfile, "r") as fd:
assert json.load(fd) == SAMPLE_DATA["price_cap"]
Expand Down Expand Up @@ -589,7 +585,6 @@ def test_json_patch(
)

handle_patch_command(args)
args.output.flush()

with open(outfile, "r") as fd:
patched = json.load(fd)
Expand Down
Loading