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
12 changes: 12 additions & 0 deletions jsonpatch_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def patch_files():
args.ORIGINAL.close()
dirname = os.path.abspath(os.path.dirname(args.ORIGINAL.name))

fd = None
try:
# Attempt to replace the file atomically. We do this by
# creating a temporary file in the same directory as the
Expand All @@ -90,6 +91,17 @@ def patch_files():
atomic = True

except OSError:
# mkstemp may have succeeded before fdopen failed. Neither the
# descriptor nor the temporary file belongs to the fallback.
if fd is not None:
try:
os.close(fd)
except OSError:
pass
try:
os.unlink(pathname)
except OSError:
pass
# We failed to create the temporary file for an atomic
# replace, so fall back to non-atomic mode by backing up
# the original (if desired) and writing a new file.
Expand Down
39 changes: 39 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2114,6 +2114,45 @@ def dump(value, stream, **kwargs):
self.assertEqual(self.read(doc), {'a': 1})
self.assertEqual(sorted(os.listdir(self.dir)), ['doc.json', 'patch.json'])

def test_jsonpatch_in_place_cleans_temp_on_fdopen_error(self):
for backup in (False, True):
with self.subTest(backup=backup):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.dir = tmp.name
doc = self.write('doc.json', {'a': 1})
patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}])
parser = jsonpatch_cli.jsonpatch_parser
args = parser.parse_args(['-i', doc, patch] + (['-b'] if backup else []))
self.addCleanup(args.ORIGINAL.close)
self.addCleanup(args.PATCH.close)
descriptors = []

def close_if_open(fd):
try:
os.close(fd)
except OSError:
pass

def fail_fdopen(fd, *args, **kwargs):
descriptors.append(fd)
self.addCleanup(close_if_open, fd)
raise OSError('cannot wrap temporary descriptor')

with mock.patch.object(parser, 'parse_args', return_value=args), \
mock.patch('os.fdopen', side_effect=fail_fdopen):
jsonpatch_cli.patch_files()

self.assertEqual(self.read(doc), {'a': 2})
if backup:
self.assertEqual(self.read(doc + '.orig'), {'a': 1})
with self.assertRaises(OSError):
os.fstat(descriptors[0])
expected = ['doc.json', 'patch.json']
if backup:
expected.append('doc.json.orig')
self.assertEqual(sorted(os.listdir(self.dir)), sorted(expected))

def test_jsonpatch_in_place_preserves_original_on_replace_error(self):
doc = self.write('doc.json', {'a': 1})
patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}])
Expand Down
Loading