From ce90014d1012e2391909cc7314a08b8a6f51f0b7 Mon Sep 17 00:00:00 2001 From: Pintu Kumar <64580363+pintu544@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:42:01 +0530 Subject: [PATCH 1/2] fix: surface missing read-write lock modules --- docs/changelog/880.bugfix.rst | 1 + src/filelock/__init__.py | 4 ++-- tests/test_lazy_import.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/880.bugfix.rst diff --git a/docs/changelog/880.bugfix.rst b/docs/changelog/880.bugfix.rst new file mode 100644 index 00000000..cde3b2f6 --- /dev/null +++ b/docs/changelog/880.bugfix.rst @@ -0,0 +1 @@ +When the optional read/write lock modules cannot be imported, filelock now treats the failure as an unavailable SQLite dependency only when the missing module is ``sqlite3`` or ``_sqlite3``. Other import errors are raised immediately instead of returning ``None``. diff --git a/src/filelock/__init__.py b/src/filelock/__init__.py index 15a6a55e..af6dbaff 100644 --- a/src/filelock/__init__.py +++ b/src/filelock/__init__.py @@ -112,8 +112,8 @@ def __getattr__(name: str) -> object: raise AttributeError(msg) try: module = import_module(module_name, __name__) - except ImportError: # pragma: lacks sqlite3 - if module_name not in {"._read_write", "._async_read_write"}: + except ImportError as error: # pragma: lacks sqlite3 + if module_name not in {"._read_write", "._async_read_write"} or error.name not in {"sqlite3", "_sqlite3"}: raise value = None else: diff --git a/tests/test_lazy_import.py b/tests/test_lazy_import.py index c597ec1a..6a8bb54c 100644 --- a/tests/test_lazy_import.py +++ b/tests/test_lazy_import.py @@ -65,6 +65,40 @@ def test_lazy_import_resolves_asyncio_submodule() -> None: assert _run("from filelock import asyncio; print(asyncio.__name__)") == "filelock.asyncio\n" +@pytest.mark.parametrize( + ("missing_module", "attribute", "expected"), + [ + pytest.param("filelock._read_write", "ReadWriteLock", "error:filelock._read_write\n", id="sync-module"), + pytest.param( + "filelock._async_read_write", "AsyncReadWriteLock", "error:filelock._async_read_write\n", id="async-module" + ), + pytest.param("sqlite3", "ReadWriteLock", "none\n", id="sync-sqlite-module"), + pytest.param("sqlite3", "AsyncReadWriteLock", "none\n", id="async-sqlite-module"), + pytest.param("_sqlite3", "ReadWriteLock", "none\n", id="sync-sqlite"), + pytest.param("_sqlite3", "AsyncReadWriteLock", "none\n", id="async-sqlite"), + pytest.param("filelock._soft", "SoftFileLock", "error:filelock._soft\n", id="other-lazy-module"), + ], +) +def test_lazy_import_only_uses_none_for_missing_sqlite(missing_module: str, attribute: str, expected: str) -> None: + script = f""" + import sys + class NotBundled: + def find_spec(self, name, path=None, target=None): + if name == {missing_module!r}: + raise ModuleNotFoundError("No module named " + repr(name), name=name) + return None + sys.meta_path.insert(0, NotBundled()) + import filelock + try: + value = getattr(filelock, {attribute!r}) + except ImportError as error: + print("error:" + str(error.name)) + else: + print("none" if value is None else "resolved") + """ + assert _run(script, measured=False) == expected + + def test_lazy_import_async_file_lock_subclasses_file_lock() -> None: assert {filelock.FileLock, filelock.BaseAsyncFileLock} <= set(filelock.AsyncFileLock.__mro__) From 6627aa88ee2939a91ce87782f1f34e0e799feb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Tue, 27 Oct 2026 19:15:44 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20fix(import):=20accept=20any?= =?UTF-8?q?=20missing=20sqlite3=20binding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fallback compared the failing import's name against sqlite3 and _sqlite3, the CPython chain only. PyPy's _sqlite3 is pure Python and imports the binding _sqlite3_cffi, which its build leaves out when the SQLite headers are missing, so ReadWriteLock would have raised there where it had been None. The check now accepts any name in the sqlite3 or _sqlite3 family. Coverage now measures the raise path instead of excluding it: the pragma moved from the except clause to the None assignment, the one line a runtime with sqlite3 cannot reach, and the missing-module cases run under coverage. --- .../+missing-read-write-module.bugfix.rst | 3 + docs/changelog/880.bugfix.rst | 1 - src/filelock/__init__.py | 9 ++- tests/test_lazy_import.py | 55 +++++++++++-------- 4 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 docs/changelog/+missing-read-write-module.bugfix.rst delete mode 100644 docs/changelog/880.bugfix.rst diff --git a/docs/changelog/+missing-read-write-module.bugfix.rst b/docs/changelog/+missing-read-write-module.bugfix.rst new file mode 100644 index 00000000..97701c08 --- /dev/null +++ b/docs/changelog/+missing-read-write-module.bugfix.rst @@ -0,0 +1,3 @@ +Resolving :class:`~filelock.ReadWriteLock` or :class:`~filelock.AsyncReadWriteLock` raises the :exc:`ImportError` when a +module other than Python's :mod:`sqlite3` is missing, as in a frozen application that left out a filelock submodule. The +names are ``None`` only without SQLite. diff --git a/docs/changelog/880.bugfix.rst b/docs/changelog/880.bugfix.rst deleted file mode 100644 index cde3b2f6..00000000 --- a/docs/changelog/880.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -When the optional read/write lock modules cannot be imported, filelock now treats the failure as an unavailable SQLite dependency only when the missing module is ``sqlite3`` or ``_sqlite3``. Other import errors are raised immediately instead of returning ``None``. diff --git a/src/filelock/__init__.py b/src/filelock/__init__.py index af6dbaff..45332e7c 100644 --- a/src/filelock/__init__.py +++ b/src/filelock/__init__.py @@ -112,10 +112,13 @@ def __getattr__(name: str) -> object: raise AttributeError(msg) try: module = import_module(module_name, __name__) - except ImportError as error: # pragma: lacks sqlite3 - if module_name not in {"._read_write", "._async_read_write"} or error.name not in {"sqlite3", "_sqlite3"}: + except ImportError as error: + # Only a missing sqlite3 module (the package, CPython's _sqlite3, PyPy's _sqlite3_cffi) leaves the read-write + # locks None; a frozen application that left out a filelock module gets the error that names it. + sqlite_missing = (error.name or "").startswith(("sqlite3", "_sqlite3")) + if module_name not in {"._read_write", "._async_read_write"} or not sqlite_missing: raise - value = None + value = None # pragma: lacks sqlite3 else: if name == "asyncio": value = module diff --git a/tests/test_lazy_import.py b/tests/test_lazy_import.py index 6a8bb54c..2988109d 100644 --- a/tests/test_lazy_import.py +++ b/tests/test_lazy_import.py @@ -65,38 +65,47 @@ def test_lazy_import_resolves_asyncio_submodule() -> None: assert _run("from filelock import asyncio; print(asyncio.__name__)") == "filelock.asyncio\n" -@pytest.mark.parametrize( - ("missing_module", "attribute", "expected"), - [ - pytest.param("filelock._read_write", "ReadWriteLock", "error:filelock._read_write\n", id="sync-module"), - pytest.param( - "filelock._async_read_write", "AsyncReadWriteLock", "error:filelock._async_read_write\n", id="async-module" - ), - pytest.param("sqlite3", "ReadWriteLock", "none\n", id="sync-sqlite-module"), - pytest.param("sqlite3", "AsyncReadWriteLock", "none\n", id="async-sqlite-module"), - pytest.param("_sqlite3", "ReadWriteLock", "none\n", id="sync-sqlite"), - pytest.param("_sqlite3", "AsyncReadWriteLock", "none\n", id="async-sqlite"), - pytest.param("filelock._soft", "SoftFileLock", "error:filelock._soft\n", id="other-lazy-module"), - ], -) -def test_lazy_import_only_uses_none_for_missing_sqlite(missing_module: str, attribute: str, expected: str) -> None: - script = f""" +def _hidden_module_script(blocked: str, missing: str, attribute: str) -> str: + return f""" import sys - class NotBundled: + class Hide: def find_spec(self, name, path=None, target=None): - if name == {missing_module!r}: - raise ModuleNotFoundError("No module named " + repr(name), name=name) - return None - sys.meta_path.insert(0, NotBundled()) + if name == {blocked!r}: + raise ModuleNotFoundError(name={missing!r}) + sys.meta_path.insert(0, Hide()) import filelock try: value = getattr(filelock, {attribute!r}) except ImportError as error: - print("error:" + str(error.name)) + print(f"error:{{error.name}}") else: print("none" if value is None else "resolved") """ - assert _run(script, measured=False) == expected + + +@pytest.mark.parametrize( + ("blocked", "missing", "attribute"), + [ + pytest.param("sqlite3", "sqlite3", "ReadWriteLock", id="package"), + pytest.param("_sqlite3", "_sqlite3", "AsyncReadWriteLock", id="cpython-binding"), + pytest.param("_sqlite3", "_sqlite3_cffi", "ReadWriteLock", id="pypy-binding"), + ], +) +def test_lazy_import_missing_sqlite3_leaves_read_write_lock_none(blocked: str, missing: str, attribute: str) -> None: + # coverage loads sqlite3 before filelock, so only an unmeasured child can miss it + assert _run(_hidden_module_script(blocked, missing, attribute), measured=False) == "none\n" + + +@pytest.mark.parametrize( + ("module", "attribute"), + [ + pytest.param("filelock._read_write", "ReadWriteLock", id="sync-read-write"), + pytest.param("filelock._async_read_write", "AsyncReadWriteLock", id="async-read-write"), + pytest.param("filelock._soft", "SoftFileLock", id="other-lazy-module"), + ], +) +def test_lazy_import_missing_filelock_module_raises(module: str, attribute: str) -> None: + assert _run(_hidden_module_script(module, module, attribute)) == f"error:{module}\n" def test_lazy_import_async_file_lock_subclasses_file_lock() -> None: