From 031f6375be6957057394f844d9a8cc6c39d67072 Mon Sep 17 00:00:00 2001 From: Clay Dugo Date: Fri, 5 Jun 2026 16:57:10 -0400 Subject: [PATCH 1/3] gh-143768: Replace a dangling interpreter symlink when creating a venv --- Lib/test/test_venv.py | 27 +++++++++++++++++++ Lib/venv/__init__.py | 2 ++ ...06-05-16-57-03.gh-issue-143768.RbLnkFx.rst | 3 +++ 3 files changed, 32 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 1ff5d7cf0c51dd8..54ac27147925593 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -608,6 +608,33 @@ def test_failed_symlink(self): filepath_regex = r"'[A-Z]:\\\\(?:[^\\\\]+\\\\)*[^\\\\]+'" self.assertRegex(err, rf"Unable to symlink {filepath_regex} to {filepath_regex}") + @requireVenvCreate + @unittest.skipIf(os.name == 'nt', 'not relevant on Windows') + @unittest.skipUnless(can_symlink(), 'Needs symlinks') + def test_broken_symlink_in_existing_venv(self): + """ + Test creating a venv when a stale venv with broken symlinks exists. + """ + bindir = os.path.join(self.env_dir, self.bindir) + os.makedirs(bindir) + python = os.path.join(bindir, 'python3') + os.symlink('/path/to/deleted/conda/env/bin/python3', python) + self.assertTrue(os.path.islink(python)) + self.assertFalse(os.path.exists(python)) + + builder = venv.EnvBuilder(with_pip=False, symlinks=True) + self.run_with_capture(builder.create, self.env_dir) + self.assertTrue(os.path.islink(python)) + self.assertTrue(os.path.exists(python)) + + rmtree(self.env_dir) + os.makedirs(bindir) + os.symlink('/path/to/deleted/conda/env/bin/python3', python) + builder = venv.EnvBuilder(with_pip=False, symlinks=False) + self.run_with_capture(builder.create, self.env_dir) + self.assertFalse(os.path.islink(python)) + self.assertTrue(os.path.exists(python)) + @requireVenvCreate def test_multiprocessing(self): """ diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index bd2762d55ef6961..db083f375eca540 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -267,6 +267,8 @@ def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): switch to a different set of files instead.) """ assert os.name != 'nt' + if os.path.islink(dst) and not os.path.exists(dst): + os.unlink(dst) force_copy = not self.symlinks if not force_copy: try: diff --git a/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst b/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst new file mode 100644 index 000000000000000..1bce93b31961415 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst @@ -0,0 +1,3 @@ +:mod:`venv`: Replace a dangling interpreter symlink in an existing +environment instead of failing or silently leaving it broken. Fix by +Clay Dugo. From a96912ab098da0fb39b18eb6ed45cf0b0d795b3a Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 7 Aug 2026 16:27:35 -0700 Subject: [PATCH 2/3] Apply batched suggestions from code review Co-authored-by: Brett Cannon --- Lib/test/test_venv.py | 4 ++-- .../Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 54ac27147925593..21ec6d386b5cd37 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -618,7 +618,7 @@ def test_broken_symlink_in_existing_venv(self): bindir = os.path.join(self.env_dir, self.bindir) os.makedirs(bindir) python = os.path.join(bindir, 'python3') - os.symlink('/path/to/deleted/conda/env/bin/python3', python) + os.symlink('/path/to/deleted/env/bin/python3', python) self.assertTrue(os.path.islink(python)) self.assertFalse(os.path.exists(python)) @@ -629,7 +629,7 @@ def test_broken_symlink_in_existing_venv(self): rmtree(self.env_dir) os.makedirs(bindir) - os.symlink('/path/to/deleted/conda/env/bin/python3', python) + os.symlink('/path/to/deleted/env/bin/python3', python) builder = venv.EnvBuilder(with_pip=False, symlinks=False) self.run_with_capture(builder.create, self.env_dir) self.assertFalse(os.path.islink(python)) diff --git a/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst b/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst index 1bce93b31961415..1aacaf432ccfb25 100644 --- a/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst +++ b/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst @@ -1,3 +1,3 @@ :mod:`venv`: Replace a dangling interpreter symlink in an existing -environment instead of failing or silently leaving it broken. Fix by -Clay Dugo. +virtual environment instead of failing or silently leaving it broken. +Fix by Clay Dugo. From dc5967628e801bcabcb6bce88ab6a442693d08e1 Mon Sep 17 00:00:00 2001 From: Clay Dugo Date: Sat, 8 Aug 2026 09:56:08 -0400 Subject: [PATCH 3/3] add doc update --- Doc/library/venv.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/library/venv.rst b/Doc/library/venv.rst index 1ad2401fe2f3f31..5cf915ea40d9842 100644 --- a/Doc/library/venv.rst +++ b/Doc/library/venv.rst @@ -448,6 +448,12 @@ creation according to their needs, the :class:`EnvBuilder` class. On POSIX systems, if a specific executable ``python3.x`` was used, symlinks to ``python`` and ``python3`` will be created pointing to that executable, unless files with those names already exist. + On POSIX systems, a broken symlink at a destination path is removed + before the copy or symlink is created. + + .. versionchanged:: next + A broken symlink at a destination path is now removed and replaced. + Previously it was left in place, or it made the copy fail. .. method:: setup_scripts(context)