No more allow mode 'w'/'x'

- File is not truncated in mode 'w'/'x', which results non-shrinked file.
- This cannot be simply resolved by adding truncation for mode 'w'/'x', which may be used on an unseekable file buffer and truncation is not allowed.
This commit is contained in:
Danny Lin 2025-05-22 00:13:34 +08:00
parent 602b909b5a
commit bb90f48f60
3 changed files with 24 additions and 19 deletions

View file

@ -523,7 +523,7 @@ ZipFile Objects
Removes a member from the archive. *zinfo_or_arcname* is either the full Removes a member from the archive. *zinfo_or_arcname* is either the full
path of the member, or a :class:`ZipInfo` instance. path of the member, or a :class:`ZipInfo` instance.
The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'``. The archive must be opened with mode ``'a'``.
Calling :meth:`remove` on a closed ZipFile will raise a :exc:`ValueError`. Calling :meth:`remove` on a closed ZipFile will raise a :exc:`ValueError`.

View file

@ -1503,6 +1503,23 @@ def test_verify(self):
zh.remove(file) zh.remove(file)
mock_fn.assert_not_called() mock_fn.assert_not_called()
# mode 'w': error and do nothing
with zipfile.ZipFile(TESTFN, 'w', self.compression) as zh:
zh.writestr(file, data)
with mock.patch('zipfile.ZipFile._remove_members') as mock_fn:
with self.assertRaises(ValueError):
zh.remove(file)
mock_fn.assert_not_called()
# mode 'x': error and do nothing
os.remove(TESTFN)
with zipfile.ZipFile(TESTFN, 'x', self.compression) as zh:
zh.writestr(file, data)
with mock.patch('zipfile.ZipFile._remove_members') as mock_fn:
with self.assertRaises(ValueError):
zh.remove(file)
mock_fn.assert_not_called()
# mode 'a': the most general use case # mode 'a': the most general use case
with zipfile.ZipFile(TESTFN, 'w', self.compression) as zh: with zipfile.ZipFile(TESTFN, 'w', self.compression) as zh:
zh.writestr(file, data) zh.writestr(file, data)
@ -1531,21 +1548,6 @@ def test_verify(self):
zh.remove(zinfo) zh.remove(zinfo)
mock_fn.assert_not_called() mock_fn.assert_not_called()
# mode 'w': like 'a'; allows removing a just written member
with zipfile.ZipFile(TESTFN, 'w', self.compression) as zh:
zh.writestr(file, data)
with mock.patch('zipfile.ZipFile._remove_members') as mock_fn:
zh.remove(file)
mock_fn.assert_called_once_with({zh.getinfo(file)})
# mode 'x': like 'w'
os.remove(TESTFN)
with zipfile.ZipFile(TESTFN, 'x', self.compression) as zh:
zh.writestr(file, data)
with mock.patch('zipfile.ZipFile._remove_members') as mock_fn:
zh.remove(file)
mock_fn.assert_called_once_with({zh.getinfo(file)})
def test_zip64(self): def test_zip64(self):
"""Test if members use zip64.""" """Test if members use zip64."""
file = 'datafile.txt' file = 'datafile.txt'

View file

@ -1867,10 +1867,13 @@ def extractall(self, path=None, members=None, pwd=None):
self._extract_member(zipinfo, path, pwd) self._extract_member(zipinfo, path, pwd)
def remove(self, zinfo_or_arcname): def remove(self, zinfo_or_arcname):
"""Remove a member from the archive.""" """Remove a member from the archive.
if self.mode not in ('w', 'x', 'a'): The archive must be open with mode 'a', since mode 'w'/'x' may be used
raise ValueError("remove() requires mode 'w', 'x', or 'a'") on an unseekable file buffer, which disallows truncation."""
if self.mode != 'a':
raise ValueError("remove() requires mode 'a'")
if not self.fp: if not self.fp:
raise ValueError( raise ValueError(
"Attempt to write to ZIP archive that was already closed") "Attempt to write to ZIP archive that was already closed")