From 39115f4f03788ce2c986182776d2a66b8887df15 Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Fri, 30 Oct 2026 10:48:14 -0400 Subject: [PATCH] gh-160782: replace symlinked files in the venv directory --- Lib/test/test_venv.py | 23 +++++++++++++++++++ Lib/venv/__init__.py | 8 +++++++ ...-10-30-10-43-07.gh-issue-160782.AHKQl6.rst | 2 ++ 3 files changed, 33 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-30-10-43-07.gh-issue-160782.AHKQl6.rst diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 678615ea172aef..bfec77582897bd 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -663,6 +663,29 @@ def test_unoverwritable_fails(self): self.assertRaises((ValueError, OSError), venv.create, self.env_dir) self.clear_directory(self.env_dir) + @unittest.skipUnless(can_symlink(), 'Needs symlinks') + def test_symlinked_file_replaced(self): + """ + Test that venv creation replaces a symlink where it writes a file. + """ + outside_dir = tempfile.mkdtemp() + self.addCleanup(rmtree, outside_dir) + outside = os.path.join(outside_dir, 'outside') + with open(outside, 'w', encoding='utf-8') as f: + f.write('untouched\n') + links = ['pyvenv.cfg', '.gitignore', + os.path.join(self.bindir, 'activate')] + os.mkdir(os.path.join(self.env_dir, self.bindir)) + for link in links: + os.symlink(outside, self.get_env_file(link)) + venv.create(self.env_dir, scm_ignore_files={'git'}) + for link in links: + with self.subTest(link=link): + self.assertFalse(os.path.islink(self.get_env_file(link))) + self.assertTrue(os.path.isfile(self.get_env_file(link))) + with open(outside, encoding='utf-8') as f: + self.assertEqual(f.read(), 'untouched\n') + def test_upgrade(self): """ Test upgrading an existing environment directory. diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index 620168be16862e..64e76a2860d13a 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -18,6 +18,11 @@ logger = logging.getLogger(__name__) +def _unlink_symlink(path): + if os.path.islink(path): + os.unlink(path) + + class EnvBuilder: """ This class exists to allow virtual environment creation to be @@ -223,6 +228,7 @@ def create_configuration(self, context): being processed. """ context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg') + _unlink_symlink(path) with open(path, 'w', encoding='utf-8') as f: f.write('home = %s\n' % context.python_dir) if self.system_site_packages: @@ -291,6 +297,7 @@ def create_git_ignore_file(self, context): ignored by git. """ gitignore_path = os.path.join(context.env_dir, '.gitignore') + _unlink_symlink(gitignore_path) with open(gitignore_path, 'w', encoding='utf-8') as file: file.write('# Created by venv; ' 'see https://docs.python.org/3/library/venv.html\n') @@ -585,6 +592,7 @@ def skip_file(f): if not os.path.exists(dstdir): os.makedirs(dstdir) dstfile = os.path.join(dstdir, f) + _unlink_symlink(dstfile) if os.name == 'nt' and srcfile.endswith(('.exe', '.pdb')): shutil.copy2(srcfile, dstfile) continue diff --git a/Misc/NEWS.d/next/Library/2026-10-30-10-43-07.gh-issue-160782.AHKQl6.rst b/Misc/NEWS.d/next/Library/2026-10-30-10-43-07.gh-issue-160782.AHKQl6.rst new file mode 100644 index 00000000000000..07f85ab8892e3a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-30-10-43-07.gh-issue-160782.AHKQl6.rst @@ -0,0 +1,2 @@ +:mod:`venv`: a symbolic link in place of ``pyvenv.cfg``, ``.gitignore`` or +an activation script is now replaced instead of written through.