Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changelog/+missing-read-write-module.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 6 additions & 3 deletions src/filelock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,13 @@ 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:
# 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
Expand Down
43 changes: 43 additions & 0 deletions tests/test_lazy_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,49 @@ def test_lazy_import_resolves_asyncio_submodule() -> None:
assert _run("from filelock import asyncio; print(asyncio.__name__)") == "filelock.asyncio\n"


def _hidden_module_script(blocked: str, missing: str, attribute: str) -> str:
return f"""
import sys
class Hide:
def find_spec(self, name, path=None, target=None):
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(f"error:{{error.name}}")
else:
print("none" if value is None else "resolved")
"""


@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:
assert {filelock.FileLock, filelock.BaseAsyncFileLock} <= set(filelock.AsyncFileLock.__mro__)

Expand Down
Loading