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):