File: _cli.py

package info (click to toggle)
python-gjson 1.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 348 kB
  • sloc: python: 2,034; makefile: 20
file content (122 lines) | stat: -rw-r--r-- 4,665 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""GJSON module."""
import argparse
import json
import sys
from collections.abc import Sequence
from typing import IO, Any, Optional

from gjson import GJSONError, get


def cli(argv: Optional[Sequence[str]] = None) -> int:  # noqa: PLR0915
    """Command line entry point to run gjson as a CLI tool.

    Arguments:
        argv: a sequence of CLI arguments to parse. If not set they will be read from sys.argv.

    Returns:
        The CLI exit code to use.

    Raises:
        OSError: for system-related errors, including I/O failures.
        json.JSONDecodeError: when the input data is not a valid JSON.
        gjson.GJSONError: for any query-related error raised by gjson.

    """
    parser = get_parser()
    args = parser.parse_args(argv)

    encapsulate = False
    if args.query.startswith('..'):
        args.query = args.query[2:]
        encapsulate = True

    # Use argparse.FileType here instead of putting it as type in the --file argument parsing, to allow to handle the
    # verbosity in case of error and make sure the file is always closed in case other arguments fail the validation.
    try:
        args.file = argparse.FileType(encoding='utf-8', errors='surrogateescape')(args.file)
    except (OSError, argparse.ArgumentTypeError) as ex:
        if args.verbose == 1:
            print(f'{ex.__class__.__name__}: {ex}', file=sys.stderr)
        elif args.verbose >= 2:  # noqa: PLR2004
            raise

        return 1

    # Reconfigure __stdin__ and __stdout__ instead of stdin and stdout because the latters are TextIO and could not
    # have the reconfigure() method if re-assigned, while reconfigure() is part of TextIOWrapper.
    # See also: https://github.com/python/typeshed/pull/8171
    if sys.__stdin__ is not None:
        sys.__stdin__.reconfigure(errors='surrogateescape')
    if sys.__stdout__ is not None:
        sys.__stdout__.reconfigure(errors='surrogateescape')

    def _execute(line: str, file_obj: Optional[IO[Any]]) -> int:
        try:
            if encapsulate:
                if line:
                    input_data = [json.loads(line, strict=False)]
                elif file_obj is not None:
                    input_data = []
                    for input_line in file_obj:
                        if input_line.strip():
                            input_data.append(json.loads(input_line, strict=False))
            elif line:
                input_data = json.loads(line, strict=False)
            elif file_obj is not None:
                input_data = json.load(file_obj, strict=False)

            result = get(input_data, args.query, as_str=True)
            exit_code = 0
        except (json.JSONDecodeError, GJSONError) as ex:
            result = ''
            exit_code = 1
            if args.verbose == 1:
                print(f'{ex.__class__.__name__}: {ex}', file=sys.stderr)
            elif args.verbose >= 2:  # noqa: PLR2004
                raise

        if result:
            print(result)

        return exit_code

    if args.lines:
        exit_code = 0
        for line in args.file:
            data = line.strip()
            if not data:
                continue
            ret = _execute(data, None)
            exit_code = max(exit_code, ret)
    else:
        exit_code = _execute('', args.file)

    return exit_code


def get_parser() -> argparse.ArgumentParser:
    """Get the CLI argument parser.

    Returns:
        the argument parser for the CLI.

    """
    parser = argparse.ArgumentParser(
        prog='gjson',
        description=('A simple way to filter and extract data from JSON-like data structures. Python porting of the '
                     'Go GJSON package.'),
        epilog='See also the full documentation available at https://volans-.github.io/gjson-py/index.html',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument('-v', '--verbose', action='count', default=0,
                        help=('Verbosity level. By default on error no output will be printed. Use -v to get the '
                              'error message to stderr and -vv to get the full traceback.'))
    parser.add_argument('-l', '--lines', action='store_true',
                        help='Treat the input as JSON Lines, parse each line and apply the query to each line.')
    # argparse.FileType is used later to parse this argument.
    parser.add_argument('file', default='-', nargs='?',
                        help='Input JSON file to query. Reads from stdin if the argument is missing or set to "-".')
    parser.add_argument('query', help='A GJSON query to apply to the input data.')

    return parser