diff --git a/src/filelock/_soft.py b/src/filelock/_soft.py index b99912bc..28c67f74 100644 --- a/src/filelock/_soft.py +++ b/src/filelock/_soft.py @@ -7,7 +7,7 @@ from pathlib import Path from ._api import BaseFileLock -from ._util import raise_on_not_writable_file +from ._util import ensure_directory_exists, raise_on_not_writable_file class SoftFileLock(BaseFileLock): @@ -15,6 +15,7 @@ class SoftFileLock(BaseFileLock): def _acquire(self) -> None: raise_on_not_writable_file(self.lock_file) + ensure_directory_exists(self.lock_file) # first check for exists and read-only mode as the open will mask this case as EEXIST flags = ( os.O_WRONLY # open for writing only diff --git a/src/filelock/_unix.py b/src/filelock/_unix.py index 40cec0ab..93ce3be5 100644 --- a/src/filelock/_unix.py +++ b/src/filelock/_unix.py @@ -7,6 +7,7 @@ from typing import cast from ._api import BaseFileLock +from ._util import ensure_directory_exists #: a flag to indicate if the fcntl API is available has_fcntl = False @@ -33,6 +34,7 @@ class UnixFileLock(BaseFileLock): """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" def _acquire(self) -> None: + ensure_directory_exists(self.lock_file) open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC fd = os.open(self.lock_file, open_flags, self._context.mode) with suppress(PermissionError): # This locked is not owned by this UID diff --git a/src/filelock/_util.py b/src/filelock/_util.py index 3d95731f..543c1394 100644 --- a/src/filelock/_util.py +++ b/src/filelock/_util.py @@ -4,6 +4,7 @@ import stat import sys from errno import EACCES, EISDIR +from pathlib import Path def raise_on_not_writable_file(filename: str) -> None: @@ -32,6 +33,15 @@ def raise_on_not_writable_file(filename: str) -> None: raise IsADirectoryError(EISDIR, "Is a directory", filename) +def ensure_directory_exists(filename: Path | str) -> None: + """ + Ensure the directory containing the file exists (create it if necessary) + :param filename: file. + """ + Path(filename).parent.mkdir(parents=True, exist_ok=True) + + __all__ = [ "raise_on_not_writable_file", + "ensure_directory_exists", ] diff --git a/src/filelock/_windows.py b/src/filelock/_windows.py index 41683f48..8db55dcb 100644 --- a/src/filelock/_windows.py +++ b/src/filelock/_windows.py @@ -8,7 +8,7 @@ from typing import cast from ._api import BaseFileLock -from ._util import raise_on_not_writable_file +from ._util import ensure_directory_exists, raise_on_not_writable_file if sys.platform == "win32": # pragma: win32 cover import msvcrt @@ -18,6 +18,7 @@ class WindowsFileLock(BaseFileLock): def _acquire(self) -> None: raise_on_not_writable_file(self.lock_file) + ensure_directory_exists(self.lock_file) flags = ( os.O_RDWR # open for read and write | os.O_CREAT # create file if not exists diff --git a/tests/test_filelock.py b/tests/test_filelock.py index f2c30df1..56373ad2 100644 --- a/tests/test_filelock.py +++ b/tests/test_filelock.py @@ -23,27 +23,20 @@ from pytest_mock import MockerFixture -@pytest.mark.parametrize( - ("lock_type", "path_type"), - [ - (FileLock, str), - (FileLock, PurePath), - (FileLock, Path), - (SoftFileLock, str), - (SoftFileLock, PurePath), - (SoftFileLock, Path), - ], -) +@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) +@pytest.mark.parametrize("path_type", [str, PurePath, Path]) +@pytest.mark.parametrize("filename", ["a", "new/b", "new2/new3/c"]) def test_simple( lock_type: type[BaseFileLock], path_type: type[str] | type[Path], + filename: str, tmp_path: Path, caplog: pytest.LogCaptureFixture, ) -> None: caplog.set_level(logging.DEBUG) # test lock creation by passing a `str` - lock_path = tmp_path / "a" + lock_path = tmp_path / filename lock = lock_type(path_type(lock_path)) with lock as locked: assert lock.is_locked @@ -113,7 +106,6 @@ def test_ro_file(lock_type: type[BaseFileLock], tmp_file_ro: Path) -> None: @pytest.mark.parametrize( ("expected_error", "match", "bad_lock_file"), [ - pytest.param(FileNotFoundError, "No such file or directory:", "a/b", id="non_existent_directory"), pytest.param(FileNotFoundError, "No such file or directory:", "", id="blank_filename"), pytest.param(ValueError, "embedded null (byte|character)", "\0", id="null_byte"), # Should be PermissionError on Windows