From 8b33b6eb9e03e64fd54416186d21bf08150283b5 Mon Sep 17 00:00:00 2001 From: fhgffy <102001626+fhgffy@users.noreply.github.com> Date: Sun, 11 Oct 2026 04:30:40 +0800 Subject: [PATCH] Close input handle before in-place CLI replacement --- bin/jsonpatch | 2 ++ tests.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/bin/jsonpatch b/bin/jsonpatch index a7adf29..2f2a447 100755 --- a/bin/jsonpatch +++ b/bin/jsonpatch @@ -42,6 +42,8 @@ def patch_files(): result = jsonpatch.apply_patch(doc, patch) if args.in_place: + # 2026-10-11: Release the input handle before replacing it on Windows. + args.ORIGINAL.close() dirname = os.path.abspath(os.path.dirname(args.ORIGINAL.name)) try: diff --git a/tests.py b/tests.py index 5454fbf..5b8b080 100755 --- a/tests.py +++ b/tests.py @@ -9,6 +9,7 @@ import jsonpatch import jsonpointer import random +import runpy # 2026-10-11: Exercise the CLI file-handle lifecycle. import sys import os import stat @@ -1898,6 +1899,27 @@ def test_jsonpatch_in_place(self): # the temporary file has been moved over the original self.assertEqual(sorted(os.listdir(self.dir)), ['doc.json', 'patch.json']) + # 2026-10-11: Windows cannot replace the original while its input is open. + def test_jsonpatch_in_place_closes_original_before_rename(self): + doc = self.write('doc.json', {'a': 1}) + patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}]) + here = os.path.dirname(os.path.abspath(__file__)) + script = runpy.run_path(os.path.join(here, 'bin', 'jsonpatch')) + parser = script['parser'] + args = parser.parse_args(['-i', doc, patch]) + self.addCleanup(args.ORIGINAL.close) + self.addCleanup(args.PATCH.close) + rename = os.rename + + def rename_closed_original(source, target): + self.assertTrue(args.ORIGINAL.closed) + return rename(source, target) + + with mock.patch.object(parser, 'parse_args', return_value=args), \ + mock.patch('os.rename', side_effect=rename_closed_original): + script['patch_files']() + self.assertEqual(self.read(doc), {'a': 2}) + @unittest.skipUnless(os.name == 'posix', 'needs POSIX file permissions') def test_jsonpatch_in_place_keeps_permissions(self): doc = self.write('doc.json', {'a': 1})