From ba9d6dbe3d7eb985ec4b02fa2b9f349d0be96ce7 Mon Sep 17 00:00:00 2001 From: Clay Dugo Date: Mon, 12 Jan 2026 22:11:15 -0500 Subject: [PATCH 1/3] gh-143768: rebuild broken interpreter symlinks --- Lib/test/test_venv.py | 18 ++++++++++++++++++ Lib/venv/__init__.py | 2 ++ 2 files changed, 20 insertions(+) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 68bcf535eada10..c5516b7ce1ff67 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -597,6 +597,24 @@ def test_failed_symlink(self): filepath_regex = r"'[A-Z]:\\\\(?:[^\\\\]+\\\\)*[^\\\\]+'" self.assertRegex(err, rf"Unable to symlink {filepath_regex} to {filepath_regex}") + @requireVenvCreate + @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)) + @requireVenvCreate def test_multiprocessing(self): """ diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index 19eddde700bcf9..cf8d12ec943bde 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: From 332c799a4772c87b6ca2a0088a64c4544f2ef3a3 Mon Sep 17 00:00:00 2001 From: Clay Dugo Date: Mon, 12 Jan 2026 22:18:26 -0500 Subject: [PATCH 2/3] add NEWS entry try again news again --- .../Library/2026-01-12-22-24-28.gh-issue-143768.RbLnkFx.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-12-22-24-28.gh-issue-143768.RbLnkFx.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-12-22-24-28.gh-issue-143768.RbLnkFx.rst b/Misc/NEWS.d/next/Library/2026-01-12-22-24-28.gh-issue-143768.RbLnkFx.rst new file mode 100644 index 00000000000000..ce0f74f0f55a02 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-12-22-24-28.gh-issue-143768.RbLnkFx.rst @@ -0,0 +1,2 @@ +:mod:`venv`: Rebuild broken interpreter symlinks when upgrading a virtual +environment. Fix by Clay Dugo. From 186d2b361b10c7c8065a044e05925581b843324a Mon Sep 17 00:00:00 2001 From: Clay Dugo Date: Mon, 12 Jan 2026 22:50:05 -0500 Subject: [PATCH 3/3] skip windows as its symlinks --- Lib/test/test_venv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index c5516b7ce1ff67..fd71640a04f193 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -598,6 +598,7 @@ def test_failed_symlink(self): 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): """