diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index d537598ac16433..bab99e75c93282 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1234,6 +1234,7 @@ def set_data(self, path, data, *, _mode=0o666): parent, part = _path_split(parent) path_parts.append(part) # Create needed directories. + just_created_cache_dir = False for part in reversed(path_parts): parent = _path_join(parent, part) try: @@ -1247,6 +1248,11 @@ def set_data(self, path, data, *, _mode=0o666): _bootstrap._verbose_message('could not create {!r}: {!r}', parent, exc) return + else: + just_created_cache_dir = True + # To avoid polluting backups with __pycache__ directories and .pyc files + if just_created_cache_dir: + _exclude_directory_from_backups(parent) try: _write_atomic(path, data, _mode) _bootstrap._verbose_message('created {!r}', path) @@ -1256,6 +1262,21 @@ def set_data(self, path, data, *, _mode=0o666): exc) +def _exclude_directory_from_backups(directory): + """Exclude the directory from backups using a CACHEDIR.TAG file. + + If the file exists or there's a permission error this is a no-op. + """ + try: + with _io.FileIO(_path_join(directory, 'CACHEDIR.TAG'), 'x') as f: + f.write(b"""Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by CPython. +# For information about cache directory tags, see: https://bford.info/cachedir/ +""") + except (FileExistsError, PermissionError): + pass + + class SourcelessFileLoader(FileLoader, _LoaderBasics): """Loader which handles sourceless file imports.""" diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 48c0a43f29e27f..6b4f314bf4e955 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -1144,6 +1144,21 @@ def test_import_pyc_path(self): 'bytecode file {!r} for {!r} does not ' 'exist'.format(pyc_path, TESTFN)) + @skip_if_dont_write_bytecode + def test_cachedir_tag_exists(self): + self.assertFalse(os.path.exists('__pycache__')) + __import__(TESTFN) + self.assertTrue(os.path.exists('__pycache__')) + cachedir_tag = os.path.join('__pycache__', 'CACHEDIR.TAG') + self.assertTrue(os.path.exists(cachedir_tag), + f'CACHEDIR.TAG file {cachedir_tag} for {TESTFN} does not exist') + with open(cachedir_tag) as f: + cachedir_tag_content = f.read() + self.assertTrue( + cachedir_tag_content.startswith('Signature: 8a477f597d28d172789f06886806bc55'), + f'CACHEDIR.TAG file {cachedir_tag} for {TESTFN} does not contain the right signature', + ) + @unittest.skipUnless(os.name == 'posix', "test meaningful only on posix systems") @skip_if_dont_write_bytecode