From 8b51830869f72051211702f4d9bab540dba31e5d Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Sat, 16 May 2026 22:42:28 +0100 Subject: [PATCH 1/2] Fix crash when checking file in a read only directory Only applies to new SqliteMetadataStore, which is the default since 1.20; old FilesystemMetadataStore doesn't crash. Fixes https://github.com/python/mypy/issues/21495 --- mypy/metastore.py | 7 +++++- mypy/test/testmetastore.py | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 mypy/test/testmetastore.py diff --git a/mypy/metastore.py b/mypy/metastore.py index c48ac78bdcb72..f7bdd9078ce18 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 0000000000000..98fb95b623461 --- /dev/null +++ b/mypy/test/testmetastore.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import os +import sys +import tempfile +import unittest +from collections.abc import Iterator +from contextlib import contextmanager + +from mypy.metastore import SqliteMetadataStore + + +@contextmanager +def _read_only_dir(path: str) -> Iterator[str]: + original_mode = os.stat(path).st_mode + os.chmod(path, 0o555) + try: + yield path + finally: + os.chmod(path, original_mode) + + +@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, _read_only_dir(parent): + 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() From 508d09d281b155eb80edfa5de8588003d59465ca Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Sat, 16 May 2026 23:02:27 +0100 Subject: [PATCH 2/2] remove _read_only_dir fixture -- seems like TemporaryDirectory can handle it: https://github.com/python/cpython/blob/03ceb59e385a8c236795bc6e25a0c767e73aff2d/Lib/tempfile.py#L926 --- mypy/test/testmetastore.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/mypy/test/testmetastore.py b/mypy/test/testmetastore.py index 98fb95b623461..0edc100a455fa 100644 --- a/mypy/test/testmetastore.py +++ b/mypy/test/testmetastore.py @@ -4,29 +4,19 @@ import sys import tempfile import unittest -from collections.abc import Iterator -from contextlib import contextmanager from mypy.metastore import SqliteMetadataStore -@contextmanager -def _read_only_dir(path: str) -> Iterator[str]: - original_mode = os.stat(path).st_mode - os.chmod(path, 0o555) - try: - yield path - finally: - os.chmod(path, original_mode) - - @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, _read_only_dir(parent): + with tempfile.TemporaryDirectory() as parent: + os.chmod(parent, 0o555) + cache_dir = os.path.join(parent, "mypy_cache") # Must not raise.