diff --git a/mypy/metastore.py b/mypy/metastore.py index c48ac78bdcb7..f7bdd9078ce1 100644 --- a/mypy/metastore.py +++ b/mypy/metastore.py @@ -189,7 +189,12 @@ def __init__( if cache_dir_prefix.startswith(os.devnull): return - os.makedirs(cache_dir_prefix, exist_ok=True) + try: + os.makedirs(cache_dir_prefix, exist_ok=True) + except OSError: + # Prevent failing on read-only filesystem and such. + return + if num_shards <= 1: self.dbs.append( connect_db(os_path_join(cache_dir_prefix, "cache.db"), set_journal_mode) diff --git a/mypy/test/testmetastore.py b/mypy/test/testmetastore.py new file mode 100644 index 000000000000..0edc100a455f --- /dev/null +++ b/mypy/test/testmetastore.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import os +import sys +import tempfile +import unittest + +from mypy.metastore import SqliteMetadataStore + + +@unittest.skipIf( + sys.platform == "win32", + "POSIX chmod semantics: os.chmod(dir, 0o555) does not prevent writes on Windows", +) +class TestSqliteMetadataStore(unittest.TestCase): + def test_init_degrades_to_noop_when_cache_dir_not_creatable(self) -> None: + with tempfile.TemporaryDirectory() as parent: + os.chmod(parent, 0o555) + + cache_dir = os.path.join(parent, "mypy_cache") + + # Must not raise. + store = SqliteMetadataStore(cache_dir) + + # Degraded to no-op state, matching the os.devnull short-circuit + # and FilesystemMetadataStore's behavior on read-only filesystems. + self.assertEqual(store.dbs, []) + self.assertFalse(store.write("foo.meta.json", b"{}")) + with self.assertRaises(FileNotFoundError): + store.read("foo.meta.json") + with self.assertRaises(FileNotFoundError): + store.getmtime("foo.meta.json") + self.assertEqual(list(store.list_all()), []) + # commit/close must be safe on an empty store + store.commit() + store.close() + + +if __name__ == "__main__": + unittest.main()