From 5c3b0d478827a7397cc4264d6ce2d3ad0ba2502e Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 6 Nov 2025 17:38:44 +0000 Subject: [PATCH] Commit --- Lib/importlib/_bootstrap_external.py | 13 +++++++++++++ Lib/py_compile.py | 8 ++++++++ Lib/test/test_compileall.py | 3 ++- .../test_importlib/source/test_file_loader.py | 15 +++++++++++++++ Lib/test/test_py_compile.py | 10 ++++++++++ ...2025-11-06-17-37-51.gh-issue-141081.NJtULs.rst | 2 ++ 6 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-11-06-17-37-51.gh-issue-141081.NJtULs.rst diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 035ae0fcae14e8..6cb8874ede268a 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -999,6 +999,19 @@ def set_data(self, path, data, *, _mode=0o666): _bootstrap._verbose_message('could not create {!r}: {!r}', parent, exc) return + + if part == _PYCACHE: + gitignore = _path_join(parent, '.gitignore') + try: + _path_stat(gitignore) + except FileNotFoundError: + gitignore_content = b'# Created by CPython\n*\n' + try: + _write_atomic(gitignore, gitignore_content, _mode) + except OSError: + pass + except OSError: + pass try: _write_atomic(path, data, _mode) _bootstrap._verbose_message('created {!r}', path) diff --git a/Lib/py_compile.py b/Lib/py_compile.py index 43d8ec90ffb6b1..b8324e7256a566 100644 --- a/Lib/py_compile.py +++ b/Lib/py_compile.py @@ -155,6 +155,14 @@ def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1, dirname = os.path.dirname(cfile) if dirname: os.makedirs(dirname) + if os.path.basename(dirname) == '__pycache__': + gitignore = os.path.join(dirname, '.gitignore') + if not os.path.exists(gitignore): + try: + with open(gitignore, 'wb') as f: + f.write(b'# Created by CPython\n*\n') + except OSError: + pass except FileExistsError: pass if invalidation_mode == PycInvalidationMode.TIMESTAMP: diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py index 8384c183dd92dd..7d022f8e385a96 100644 --- a/Lib/test/test_compileall.py +++ b/Lib/test/test_compileall.py @@ -625,7 +625,8 @@ def f(self, ext=ext, switch=switch): ['-m', 'compileall', '-q', self.pkgdir])) # Verify the __pycache__ directory contents. self.assertTrue(os.path.exists(self.pkgdir_cachedir)) - expected = sorted(base.format(sys.implementation.cache_tag, ext) + expected = ['.gitignore'] + \ + sorted(base.format(sys.implementation.cache_tag, ext) for base in ('__init__.{}.{}', 'bar.{}.{}')) self.assertEqual(sorted(os.listdir(self.pkgdir_cachedir)), expected) # Make sure there are no .pyc files in the source directory. diff --git a/Lib/test/test_importlib/source/test_file_loader.py b/Lib/test/test_importlib/source/test_file_loader.py index f35adec1a8e800..f9fb286859d45a 100644 --- a/Lib/test/test_importlib/source/test_file_loader.py +++ b/Lib/test/test_importlib/source/test_file_loader.py @@ -353,6 +353,21 @@ def test_overridden_unchecked_hash_based_pyc(self): data[8:16], ) + @util.writes_bytecode_files + def test_gitignore_in_pycache(self): + with util.create_modules('_temp') as mapping: + source = mapping['_temp'] + loader = self.machinery.SourceFileLoader('_temp', source) + mod = types.ModuleType('_temp') + mod.__spec__ = self.util.spec_from_loader('_temp', loader) + loader.exec_module(mod) + pyc = os.path.dirname(self.util.cache_from_source(source)) + gitignore = os.path.join(pyc, '.gitignore') + self.assertTrue(os.path.exists(gitignore)) + with open(gitignore, 'rb') as f: + t = f.read() + self.assertEqual(t, b'# Created by CPython\n*\n') + (Frozen_SimpleTest, Source_SimpleTest diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py index 64387296e84621..fdfb124c051884 100644 --- a/Lib/test/test_py_compile.py +++ b/Lib/test/test_py_compile.py @@ -207,6 +207,16 @@ def test_quiet(self): with self.assertRaises(py_compile.PyCompileError): py_compile.compile(bad_coding, doraise=True, quiet=1) + def test_gitignore_created(self): + py_compile.compile(self.source_path) + self.assertTrue(os.path.exists(self.cache_path)) + pyc = os.path.dirname(self.cache_path) + gitignore = os.path.join(pyc, '.gitignore') + self.assertTrue(os.path.exists(gitignore)) + with open(gitignore, 'rb') as f: + text = f.read() + self.assertEqual(text, b'# Created by CPython\n*\n') + class PyCompileTestsWithSourceEpoch(PyCompileTestsBase, unittest.TestCase, diff --git a/Misc/NEWS.d/next/Library/2025-11-06-17-37-51.gh-issue-141081.NJtULs.rst b/Misc/NEWS.d/next/Library/2025-11-06-17-37-51.gh-issue-141081.NJtULs.rst new file mode 100644 index 00000000000000..2b64f68f4dfd28 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-06-17-37-51.gh-issue-141081.NJtULs.rst @@ -0,0 +1,2 @@ +When ``__pycache__`` directories are created, they now contain a +``.gitignore`` file that ignores their contents.