Skip to content

Commit 15c9d90

Browse files
StanFromIrelandhugovkbrettcannon
authored
gh-141081: Add a .gitignore file to __pycache__ folders (#141162)
Co-authored-by: Hugo van Kemenade <[email protected]> Co-authored-by: Brett Cannon <[email protected]>
1 parent 19c72d2 commit 15c9d90

File tree

7 files changed

+60
-2
lines changed

7 files changed

+60
-2
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ Summary -- Release highlights
7474
* :pep:`782`: :ref:`A new PyBytesWriter C API to create a Python bytes object
7575
<whatsnew315-pep782>`
7676
* :ref:`Improved error messages <whatsnew315-improved-error-messages>`
77+
* :ref:`__pycache__ directories now contain a .gitignore file
78+
<whatsnew315-pycache-gitignore>`
7779

7880

7981
New features
@@ -397,6 +399,12 @@ Other language changes
397399
for any class.
398400
(Contributed by Serhiy Storchaka in :gh:`41779`.)
399401

402+
.. _whatsnew315-pycache-gitignore:
403+
404+
* :file:`__pycache__` directories now contain a :file:`.gitignore` file for Git
405+
that ignores their contents.
406+
(Contributed by Stan Ulbrych in :gh:`141081`.)
407+
400408

401409
New modules
402410
===========

Lib/importlib/_bootstrap_external.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,19 @@ def set_data(self, path, data, *, _mode=0o666):
967967
_bootstrap._verbose_message('could not create {!r}: {!r}',
968968
parent, exc)
969969
return
970+
971+
if part == _PYCACHE:
972+
gitignore = _path_join(parent, '.gitignore')
973+
try:
974+
_path_stat(gitignore)
975+
except FileNotFoundError:
976+
gitignore_content = b'# Created by CPython\n*\n'
977+
try:
978+
_write_atomic(gitignore, gitignore_content, _mode)
979+
except OSError:
980+
pass
981+
except OSError:
982+
pass
970983
try:
971984
_write_atomic(path, data, _mode)
972985
_bootstrap._verbose_message('created {!r}', path)

Lib/py_compile.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1,
155155
dirname = os.path.dirname(cfile)
156156
if dirname:
157157
os.makedirs(dirname)
158+
if os.path.basename(dirname) == '__pycache__':
159+
gitignore = os.path.join(dirname, '.gitignore')
160+
if not os.path.exists(gitignore):
161+
try:
162+
with open(gitignore, 'wb') as f:
163+
f.write(b'# Created by CPython\n*\n')
164+
except OSError:
165+
pass
158166
except FileExistsError:
159167
pass
160168
if invalidation_mode == PycInvalidationMode.TIMESTAMP:

Lib/test/test_compileall.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -625,8 +625,10 @@ def f(self, ext=ext, switch=switch):
625625
['-m', 'compileall', '-q', self.pkgdir]))
626626
# Verify the __pycache__ directory contents.
627627
self.assertTrue(os.path.exists(self.pkgdir_cachedir))
628-
expected = sorted(base.format(sys.implementation.cache_tag, ext)
629-
for base in ('__init__.{}.{}', 'bar.{}.{}'))
628+
expected = ['.gitignore'] + sorted(
629+
base.format(sys.implementation.cache_tag, ext)
630+
for base in ('__init__.{}.{}', 'bar.{}.{}')
631+
)
630632
self.assertEqual(sorted(os.listdir(self.pkgdir_cachedir)), expected)
631633
# Make sure there are no .pyc files in the source directory.
632634
self.assertFalse([fn for fn in os.listdir(self.pkgdir)

Lib/test/test_importlib/source/test_file_loader.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,21 @@ def test_overridden_unchecked_hash_based_pyc(self):
180180
data[8:16],
181181
)
182182

183+
@util.writes_bytecode_files
184+
def test_gitignore_in_pycache(self):
185+
with util.create_modules('_temp') as mapping:
186+
source = mapping['_temp']
187+
loader = self.machinery.SourceFileLoader('_temp', source)
188+
mod = types.ModuleType('_temp')
189+
mod.__spec__ = self.util.spec_from_loader('_temp', loader)
190+
loader.exec_module(mod)
191+
pyc = os.path.dirname(self.util.cache_from_source(source))
192+
gitignore = os.path.join(pyc, '.gitignore')
193+
self.assertTrue(os.path.exists(gitignore))
194+
with open(gitignore, 'rb') as f:
195+
t = f.read()
196+
self.assertEqual(t, b'# Created by CPython\n*\n')
197+
183198

184199
(Frozen_SimpleTest,
185200
Source_SimpleTest

Lib/test/test_py_compile.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ def test_quiet(self):
207207
with self.assertRaises(py_compile.PyCompileError):
208208
py_compile.compile(bad_coding, doraise=True, quiet=1)
209209

210+
def test_gitignore_created(self):
211+
py_compile.compile(self.source_path)
212+
self.assertTrue(os.path.exists(self.cache_path))
213+
pyc = os.path.dirname(self.cache_path)
214+
gitignore = os.path.join(pyc, '.gitignore')
215+
self.assertTrue(os.path.exists(gitignore))
216+
with open(gitignore, 'rb') as f:
217+
text = f.read()
218+
self.assertEqual(text, b'# Created by CPython\n*\n')
219+
210220

211221
class PyCompileTestsWithSourceEpoch(PyCompileTestsBase,
212222
unittest.TestCase,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
When ``__pycache__`` directories are created, they now contain a
2+
``.gitignore`` file that ignores their contents.

0 commit comments

Comments
 (0)