From c4bf6c13d116d13e431559807f79ca57ac3650c9 Mon Sep 17 00:00:00 2001 From: Matt Walker Date: Wed, 15 Oct 2025 19:22:32 -0400 Subject: [PATCH 1/2] Add In Place Editing Option Adds the --in-place command line option to save back edits to the input file without needing to name all the input files twice using --output. We had to restructure the CLI file flow quite a bit because we don't want to touch the output file if running in-place and if the contents haven't changed. (And because argparse.FileType is now deprecated.) To prove that the file hasn't changed we keep the input string around, but this will cost considerable memory for large files. Potential alternatives would be to SHA the file contents at the cost of computation cycles and the small risk of collision, or to write out to a temporary file and do the compare before swapping. --- README.md | 5 ++-- src/compact_json/_compact_json.py | 50 ++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ac904aa..6c35921 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ When installed from `pip` a command-line utility `compact-json` is installed whi ``` shell usage: compact-json [-h] [-V] - [--output-filename [OUTPUT_FILENAME ...]] + [--output [OUTPUT_FILENAME ...]] [--crlf] [--max-inline-length N] [--max-inline-complexity N] [--max-compact-list-complexity N] @@ -140,10 +140,11 @@ positional arguments: optional arguments: -h, --help show this help message and exit -V, --version - --output-filename [OUTPUT_FILENAME ...], -out [OUTPUT_FILENAME ...] + --output [OUTPUT_FILENAME ...], -out [OUTPUT_FILENAME ...] The output file name(s). If empty, no new JSON file(s) will be saved. If provided, the number of output file names must match that of the input files. + --in-place Save any edits back to the input file --crlf Use Windows-style CRLF line endings --max-inline-length N Limit inline elements to N chars, excluding diff --git a/src/compact_json/_compact_json.py b/src/compact_json/_compact_json.py index 4416b0e..38c7f1e 100644 --- a/src/compact_json/_compact_json.py +++ b/src/compact_json/_compact_json.py @@ -15,13 +15,21 @@ def command_line_parser() -> argparse.ArgumentParser: ) parser.add_argument("-V", "--version", action="store_true") - parser.add_argument( + out_group = parser.add_mutually_exclusive_group() + out_group.add_argument( "--output", "-o", action="append", help="The output file name(s). The number of output file names must match " "the number of input files.", ) + out_group.add_argument( + "--in-place", + action="store_true", + default=False, + help="Save any edits back to the input file", + ) + parser.add_argument( "--align-expanded-property-names", action="store_true", @@ -161,7 +169,6 @@ def command_line_parser() -> argparse.ArgumentParser: parser.add_argument( "json", nargs="*", - type=argparse.FileType("r"), help='JSON file(s) to parse (or stdin with "-")', ) return parser @@ -222,21 +229,36 @@ def die(message: str) -> None: line_ending = "\r\n" if args.crlf else "\n" in_files = args.json - out_files = args.output - if out_files is None: - for fh in args.json: - obj = json.load(fh) - json_string = formatter.serialize(obj) - print(json_string, end=line_ending) - return + if args.in_place: + out_files = args.json + elif args.output is None: + out_files = [None] * len(in_files) + else: + if len(in_files) != len(args.output): + die("the numbers of input and output file names do not match") + out_files = args.output + + for fn_in, fn_out in zip(in_files, out_files): + if fn_in == "-": + in_json_string = sys.stdin.read() + else: + try: + in_json_string = open(fn_in, "r").read() + except FileNotFoundError as ex: + die(ex) + + try: + obj = json.loads(in_json_string) + except Exception as ex: + die("While reading {}: {}".format(fn_in, ex)) - if len(in_files) != len(out_files): - die("the numbers of input and output file names do not match") + out_json_string = formatter.serialize(obj) + line_ending - for fn_in, fn_out in zip(args.json, args.output): - obj = json.load(fn_in) - json_string = formatter.dump(obj, output_file=fn_out) + if fn_out is None: + sys.stdout.write(out_json_string) + elif not args.in_place or in_json_string != out_json_string: + open(fn_out, "w").write(out_json_string) if __name__ == "__main__": # pragma: no cover From 869edd2fc320640713fa70dfc8432407007a4ee8 Mon Sep 17 00:00:00 2001 From: Matt Walker Date: Tue, 21 Oct 2025 19:36:18 -0400 Subject: [PATCH 2/2] Add helper note to contributing --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a395129..6671368 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,7 @@ Testing and configuration is based on [Poetry](https://python-poetry.org) and is You should also verify that existing [tests](./tests) are still working and you are encouraged to add new ones. You are also encouraged to add tests that at least maintain the current level of code coverage. You can run the tests using the following commands from the root folder: ```bash +poetry install # Only required the first time poetry run pytest ```