From ec8b8d1132e3d7bf9b7e2378d5b70d2ae6dbee49 Mon Sep 17 00:00:00 2001 From: 6t8k <58048945+6t8k@users.noreply.github.com> Date: Sat, 26 Nov 2022 23:11:08 +0100 Subject: [PATCH 1/4] gh-99203: `shutil.make_archive()`: restore select CPython <= 3.10.5 behavior Restore following CPython <= 3.10.5 behavior of `shutil.make_archive()` that went away as part of gh-93160: Do not create an empty archive if `root_dir` is not a directory, and, in that case, raise `FileNotFoundError` or `NotADirectoryError` regardless of `format` choice. Beyond the brought-back behavior, the function may now also raise these exceptions in `dry_run` mode. --- Lib/shutil.py | 4 ++ Lib/test/test_shutil.py | 42 +++++++++++++++++++ ...2-11-26-22-05-22.gh-issue-99203.j0DUae.rst | 5 +++ 3 files changed, 51 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2022-11-26-22-05-22.gh-issue-99203.j0DUae.rst diff --git a/Lib/shutil.py b/Lib/shutil.py index 867925aa10cc04..c8a2c77b4c7344 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1104,6 +1104,10 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0, supports_root_dir = getattr(func, 'supports_root_dir', False) save_cwd = None if root_dir is not None: + stmd = os.stat(root_dir).st_mode + if not stat.S_ISDIR(stmd): + raise NotADirectoryError(root_dir) + if supports_root_dir: # Support path-like base_name here for backwards-compatibility. base_name = os.fspath(base_name) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 8fe62216ecdca0..0ac64ca18b259b 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1665,6 +1665,48 @@ def test_register_archive_format(self): formats = [name for name, params in get_archive_formats()] self.assertNotIn('xxx', formats) + def _unlink_existing_file(self, path): + try: + os.unlink(path) + except FileNotFoundError: + print(f"File {path} not found") + pass + + def test_make_tarfile_rootdir_nodir(self): + # GH-99203 + self.addCleanup(self._unlink_existing_file, f'{TESTFN}.tar') + for dry_run in (0, True): + tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + os.close(tmp_fd) + with self.assertRaises(NotADirectoryError): + make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) + self.assertFalse(os.path.exists(f'{TESTFN}.tar')) + + tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + os.close(tmp_fd) + os.unlink(tmp_file) + with self.assertRaises(FileNotFoundError): + make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) + self.assertFalse(os.path.exists(f'{TESTFN}.tar')) + + @support.requires_zlib() + def test_make_zipfile_rootdir_nodir(self): + # GH-99203 + self.addCleanup(self._unlink_existing_file, f'{TESTFN}.zip') + for dry_run in (0, True): + tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + os.close(tmp_fd) + with self.assertRaises(NotADirectoryError): + make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) + self.assertFalse(os.path.exists(f'{TESTFN}.zip')) + + tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + os.close(tmp_fd) + os.unlink(tmp_file) + with self.assertRaises(FileNotFoundError): + make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) + self.assertFalse(os.path.exists(f'{TESTFN}.zip')) + ### shutil.unpack_archive def check_unpack_archive(self, format): diff --git a/Misc/NEWS.d/next/Library/2022-11-26-22-05-22.gh-issue-99203.j0DUae.rst b/Misc/NEWS.d/next/Library/2022-11-26-22-05-22.gh-issue-99203.j0DUae.rst new file mode 100644 index 00000000000000..fcfb044d476acc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-11-26-22-05-22.gh-issue-99203.j0DUae.rst @@ -0,0 +1,5 @@ +Restore following CPython <= 3.10.5 behavior of :func:`shutil.make_archive`: +do not create an empty archive if ``root_dir`` is not a directory, and, in that +case, raise :class:`FileNotFoundError` or :class:`NotADirectoryError` +regardless of ``format`` choice. Beyond the brought-back behavior, the function +may now also raise these exceptions in ``dry_run`` mode. From 0cfc858903b9d9014f77b9d85238e9b7eacc6480 Mon Sep 17 00:00:00 2001 From: 6t8k <58048945+6t8k@users.noreply.github.com> Date: Wed, 30 Nov 2022 08:29:24 +0100 Subject: [PATCH 2/4] Make use of `unittest.TestCase.subTest` --- Lib/test/test_shutil.py | 51 +++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 0ac64ca18b259b..a34a17bd651dc5 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1669,43 +1669,44 @@ def _unlink_existing_file(self, path): try: os.unlink(path) except FileNotFoundError: - print(f"File {path} not found") pass def test_make_tarfile_rootdir_nodir(self): # GH-99203 self.addCleanup(self._unlink_existing_file, f'{TESTFN}.tar') for dry_run in (0, True): - tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) - os.close(tmp_fd) - with self.assertRaises(NotADirectoryError): - make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) - self.assertFalse(os.path.exists(f'{TESTFN}.tar')) - - tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) - os.close(tmp_fd) - os.unlink(tmp_file) - with self.assertRaises(FileNotFoundError): - make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) - self.assertFalse(os.path.exists(f'{TESTFN}.tar')) + with self.subTest(dry_run=dry_run): + tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + os.close(tmp_fd) + with self.assertRaises(NotADirectoryError): + make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) + self.assertFalse(os.path.exists(f'{TESTFN}.tar')) + + tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + os.close(tmp_fd) + os.unlink(tmp_file) + with self.assertRaises(FileNotFoundError): + make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) + self.assertFalse(os.path.exists(f'{TESTFN}.tar')) @support.requires_zlib() def test_make_zipfile_rootdir_nodir(self): # GH-99203 self.addCleanup(self._unlink_existing_file, f'{TESTFN}.zip') for dry_run in (0, True): - tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) - os.close(tmp_fd) - with self.assertRaises(NotADirectoryError): - make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) - self.assertFalse(os.path.exists(f'{TESTFN}.zip')) - - tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) - os.close(tmp_fd) - os.unlink(tmp_file) - with self.assertRaises(FileNotFoundError): - make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) - self.assertFalse(os.path.exists(f'{TESTFN}.zip')) + with self.subTest(dry_run=dry_run): + tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + os.close(tmp_fd) + with self.assertRaises(NotADirectoryError): + make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) + self.assertFalse(os.path.exists(f'{TESTFN}.zip')) + + tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + os.close(tmp_fd) + os.unlink(tmp_file) + with self.assertRaises(FileNotFoundError): + make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) + self.assertFalse(os.path.exists(f'{TESTFN}.zip')) ### shutil.unpack_archive From e9a0118d842e72ca48b20cf7783eeb608011311a Mon Sep 17 00:00:00 2001 From: 6t8k <58048945+6t8k@users.noreply.github.com> Date: Mon, 30 Jan 2023 20:55:32 +0100 Subject: [PATCH 3/4] Address code review --- Lib/shutil.py | 2 +- Lib/test/test_shutil.py | 34 ++++++++++++++-------------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index c8a2c77b4c7344..9297f40374d7db 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1106,7 +1106,7 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0, if root_dir is not None: stmd = os.stat(root_dir).st_mode if not stat.S_ISDIR(stmd): - raise NotADirectoryError(root_dir) + raise NotADirectoryError(errno.ENOTDIR, 'Not a directory', root_dir) if supports_root_dir: # Support path-like base_name here for backwards-compatibility. diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index a34a17bd651dc5..f5425a843bd030 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1665,47 +1665,41 @@ def test_register_archive_format(self): formats = [name for name, params in get_archive_formats()] self.assertNotIn('xxx', formats) - def _unlink_existing_file(self, path): - try: - os.unlink(path) - except FileNotFoundError: - pass - def test_make_tarfile_rootdir_nodir(self): # GH-99203 - self.addCleanup(self._unlink_existing_file, f'{TESTFN}.tar') - for dry_run in (0, True): + self.addCleanup(os_helper.unlink, f'{TESTFN}.tar') + for dry_run in (False, True): with self.subTest(dry_run=dry_run): tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) os.close(tmp_fd) - with self.assertRaises(NotADirectoryError): + with self.assertRaises(NotADirectoryError) as cm: make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) + self.assertEqual(cm.exception.errno, errno.ENOTDIR) + self.assertEqual(cm.exception.filename, tmp_file) self.assertFalse(os.path.exists(f'{TESTFN}.tar')) - tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) - os.close(tmp_fd) - os.unlink(tmp_file) + nonexisting_file = os.path.join(self.mkdtemp(), 'nonexisting') with self.assertRaises(FileNotFoundError): - make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) + make_archive(TESTFN, 'tar', nonexisting_file, dry_run=dry_run) self.assertFalse(os.path.exists(f'{TESTFN}.tar')) @support.requires_zlib() def test_make_zipfile_rootdir_nodir(self): # GH-99203 - self.addCleanup(self._unlink_existing_file, f'{TESTFN}.zip') - for dry_run in (0, True): + self.addCleanup(os_helper.unlink, f'{TESTFN}.zip') + for dry_run in (False, True): with self.subTest(dry_run=dry_run): tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) os.close(tmp_fd) - with self.assertRaises(NotADirectoryError): + with self.assertRaises(NotADirectoryError) as cm: make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) + self.assertEqual(cm.exception.errno, errno.ENOTDIR) + self.assertEqual(cm.exception.filename, tmp_file) self.assertFalse(os.path.exists(f'{TESTFN}.zip')) - tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) - os.close(tmp_fd) - os.unlink(tmp_file) + nonexisting_file = os.path.join(self.mkdtemp(), 'nonexisting') with self.assertRaises(FileNotFoundError): - make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) + make_archive(TESTFN, 'zip', nonexisting_file, dry_run=dry_run) self.assertFalse(os.path.exists(f'{TESTFN}.zip')) ### shutil.unpack_archive From 74a7fe4c70c959c543fc391200885b6e5ac4e98f Mon Sep 17 00:00:00 2001 From: 6t8k <58048945+6t8k@users.noreply.github.com> Date: Tue, 31 Jan 2023 06:29:05 +0100 Subject: [PATCH 4/4] Create less tmp dirs, test FileNotFoundError attrs too --- Lib/test/test_shutil.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index f5425a843bd030..72c2f830431d41 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1670,7 +1670,15 @@ def test_make_tarfile_rootdir_nodir(self): self.addCleanup(os_helper.unlink, f'{TESTFN}.tar') for dry_run in (False, True): with self.subTest(dry_run=dry_run): - tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + tmp_dir = self.mkdtemp() + nonexisting_file = os.path.join(tmp_dir, 'nonexisting') + with self.assertRaises(FileNotFoundError) as cm: + make_archive(TESTFN, 'tar', nonexisting_file, dry_run=dry_run) + self.assertEqual(cm.exception.errno, errno.ENOENT) + self.assertEqual(cm.exception.filename, nonexisting_file) + self.assertFalse(os.path.exists(f'{TESTFN}.tar')) + + tmp_fd, tmp_file = tempfile.mkstemp(dir=tmp_dir) os.close(tmp_fd) with self.assertRaises(NotADirectoryError) as cm: make_archive(TESTFN, 'tar', tmp_file, dry_run=dry_run) @@ -1678,18 +1686,21 @@ def test_make_tarfile_rootdir_nodir(self): self.assertEqual(cm.exception.filename, tmp_file) self.assertFalse(os.path.exists(f'{TESTFN}.tar')) - nonexisting_file = os.path.join(self.mkdtemp(), 'nonexisting') - with self.assertRaises(FileNotFoundError): - make_archive(TESTFN, 'tar', nonexisting_file, dry_run=dry_run) - self.assertFalse(os.path.exists(f'{TESTFN}.tar')) - @support.requires_zlib() def test_make_zipfile_rootdir_nodir(self): # GH-99203 self.addCleanup(os_helper.unlink, f'{TESTFN}.zip') for dry_run in (False, True): with self.subTest(dry_run=dry_run): - tmp_fd, tmp_file = tempfile.mkstemp(dir=self.mkdtemp()) + tmp_dir = self.mkdtemp() + nonexisting_file = os.path.join(tmp_dir, 'nonexisting') + with self.assertRaises(FileNotFoundError) as cm: + make_archive(TESTFN, 'zip', nonexisting_file, dry_run=dry_run) + self.assertEqual(cm.exception.errno, errno.ENOENT) + self.assertEqual(cm.exception.filename, nonexisting_file) + self.assertFalse(os.path.exists(f'{TESTFN}.zip')) + + tmp_fd, tmp_file = tempfile.mkstemp(dir=tmp_dir) os.close(tmp_fd) with self.assertRaises(NotADirectoryError) as cm: make_archive(TESTFN, 'zip', tmp_file, dry_run=dry_run) @@ -1697,11 +1708,6 @@ def test_make_zipfile_rootdir_nodir(self): self.assertEqual(cm.exception.filename, tmp_file) self.assertFalse(os.path.exists(f'{TESTFN}.zip')) - nonexisting_file = os.path.join(self.mkdtemp(), 'nonexisting') - with self.assertRaises(FileNotFoundError): - make_archive(TESTFN, 'zip', nonexisting_file, dry_run=dry_run) - self.assertFalse(os.path.exists(f'{TESTFN}.zip')) - ### shutil.unpack_archive def check_unpack_archive(self, format):
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: