Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added multiuser support and associated tests #192

Merged
merged 11 commits into from
Mar 15, 2023
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
Changelog
=========
v3.10.0 (2023-03-15)
-------------------
- Add support for explicit file modes for lockfiles :pr:`192 - by :user:`jahrules`.

v3.9.1 (2023-03-14)
-------------------
- Use ``time.perf_counter`` instead of ``time.monotonic`` for calculating timeouts.
Expand Down
18 changes: 15 additions & 3 deletions src/filelock/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,20 @@ def __exit__(
class BaseFileLock(ABC, contextlib.ContextDecorator):
"""Abstract base class for a file lock object."""

def __init__(self, lock_file: str | os.PathLike[Any], timeout: float = -1) -> None:
def __init__(
self,
lock_file: str | os.PathLike[Any],
timeout: float = -1,
mode: int = 0o644,
gaborbernat marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""
Create a new lock object.

:param lock_file: path to the file
:param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in
the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it
to a negative value. A timeout of 0 means, that there is exactly one attempt to acquire the file lock.
: param mode: file permissions for the lockfile.
"""
# The path to the lock file.
self._lock_file: str = os.fspath(lock_file)
Expand All @@ -58,6 +64,9 @@ def __init__(self, lock_file: str | os.PathLike[Any], timeout: float = -1) -> No
# The default timeout value.
self._timeout: float = timeout

# The mode for the lock files
self._mode: int = mode

# We use this lock primarily for the lock counter.
self._thread_lock: Lock = Lock()

Expand Down Expand Up @@ -170,8 +179,11 @@ def acquire(
with self._thread_lock:
if not self.is_locked:
_LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
self._acquire()

previous_umask = os.umask(0)
try:
self._acquire()
finally:
os.umask(previous_umask) # reset umask to initial value
if self.is_locked:
_LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename)
break
Expand Down
4 changes: 2 additions & 2 deletions src/filelock/_soft.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ class SoftFileLock(BaseFileLock):
def _acquire(self) -> None:
raise_on_exist_ro_file(self._lock_file)
# first check for exists and read-only mode as the open will mask this case as EEXIST
mode = (
flags = (
os.O_WRONLY # open for writing only
| os.O_CREAT
| os.O_EXCL # together with above raise EEXIST if the file specified by filename exists
| os.O_TRUNC # truncate the file to zero byte
)
try:
fd = os.open(self._lock_file, mode)
fd = os.open(self._lock_file, flags, self._mode)
except OSError as exception:
if exception.errno == EEXIST: # expected if cannot lock
pass
Expand Down
4 changes: 2 additions & 2 deletions src/filelock/_unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ class UnixFileLock(BaseFileLock):
"""Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""

def _acquire(self) -> None:
open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
fd = os.open(self._lock_file, open_mode)
open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC
fd = os.open(self._lock_file, open_flags, self._mode)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
Expand Down
4 changes: 2 additions & 2 deletions src/filelock/_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ class WindowsFileLock(BaseFileLock):

def _acquire(self) -> None:
raise_on_exist_ro_file(self._lock_file)
mode = (
flags = (
os.O_RDWR # open for read and write
| os.O_CREAT # create file if not exists
| os.O_TRUNC # truncate file if not empty
)
try:
fd = os.open(self._lock_file, mode)
fd = os.open(self._lock_file, flags, self._mode)
except OSError as exception:
if exception.errno == ENOENT: # No such file or directory
raise
Expand Down
63 changes: 62 additions & 1 deletion tests/test_filelock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import inspect
import logging
import os
import sys
import threading
from contextlib import contextmanager
from inspect import getframeinfo, stack
from pathlib import Path, PurePath
from stat import S_IWGRP, S_IWOTH, S_IWUSR
from stat import S_IWGRP, S_IWOTH, S_IWUSR, filemode
from types import TracebackType
from typing import Callable, Iterator, Tuple, Type, Union

Expand Down Expand Up @@ -418,6 +419,66 @@ def decorated_method() -> None:
assert not lock.is_locked


def test_lock_mode(tmp_path: Path) -> None:
lock_path = tmp_path / "a.lock"
lock = FileLock(str(lock_path), mode=0o666)

lock.acquire()
assert lock.is_locked

mode = filemode(os.stat(lock_path).st_mode)
assert mode == "-rw-rw-rw-"

lock.release()


def test_lock_mode_soft(tmp_path: Path) -> None:
lock_path = tmp_path / "a.lock"
lock = SoftFileLock(str(lock_path), mode=0o666)

lock.acquire()
assert lock.is_locked

mode = filemode(os.stat(lock_path).st_mode)
assert mode == "-rw-rw-rw-"

lock.release()


def test_umask(tmp_path: Path) -> None:
lock_path = tmp_path / "a.lock"
lock = FileLock(str(lock_path), mode=0o666)

initial_umask = os.umask(0)
os.umask(initial_umask)

lock.acquire()
assert lock.is_locked

current_umask = os.umask(0)
os.umask(current_umask)
assert initial_umask == current_umask

lock.release()


def test_umask_soft(tmp_path: Path) -> None:
lock_path = tmp_path / "a.lock"
lock = SoftFileLock(str(lock_path), mode=0o666)

initial_umask = os.umask(0)
os.umask(initial_umask)

lock.acquire()
assert lock.is_locked

current_umask = os.umask(0)
os.umask(current_umask)
assert initial_umask == current_umask

lock.release()


def test_wrong_platform(tmp_path: Path) -> None:
assert not inspect.isabstract(UnixFileLock)
assert not inspect.isabstract(WindowsFileLock)
Expand Down
1 change: 1 addition & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ caplog
eacces
extlinks
filelock
filemode
frameinfo
fspath
getframeinfo
Expand Down