From c0551fe38eca06958bc63d959f295bfd0a61b65a Mon Sep 17 00:00:00 2001 From: Gregory Taeger Date: Thu, 2 Feb 2023 11:50:11 -0500 Subject: [PATCH 01/11] added multiuser support and associated tests --- src/filelock/_api.py | 11 ++++++++++- src/filelock/_unix.py | 10 ++++++++-- src/filelock/_windows.py | 9 +++++++-- tests/test_filelock.py | 29 ++++++++++++++++++++++++++++- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/filelock/_api.py b/src/filelock/_api.py index 5ca63130..52e1e27d 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -39,7 +39,12 @@ 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, + multiuser: bool = False + ) -> None: """ Create a new lock object. @@ -47,6 +52,7 @@ def __init__(self, lock_file: str | os.PathLike[Any], timeout: float = -1) -> No :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 multiuser: if True, make the lock file permissions rw/rw/rw for all users """ # The path to the lock file. self._lock_file: str = os.fspath(lock_file) @@ -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 multiuser flag + self._multiuser: bool = multiuser + # We use this lock primarily for the lock counter. self._thread_lock: Lock = Lock() diff --git a/src/filelock/_unix.py b/src/filelock/_unix.py index 03b612c9..5f3af77a 100644 --- a/src/filelock/_unix.py +++ b/src/filelock/_unix.py @@ -31,8 +31,14 @@ 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 + if self._multiuser is True: + os.umask(0) + open_mode = 0o666 + else: + # This is the default mode + open_mode = 511 + fd = os.open(self._lock_file, open_flags, open_mode) try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: diff --git a/src/filelock/_windows.py b/src/filelock/_windows.py index 60e68cb9..2f0ce4b4 100644 --- a/src/filelock/_windows.py +++ b/src/filelock/_windows.py @@ -16,13 +16,18 @@ 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 ) + if self._multiuser is True: + os.umask(0) + mode = 0o666 + else: + mode = 511 try: - fd = os.open(self._lock_file, mode) + fd = os.open(self._lock_file, mode, flags) except OSError as exception: if exception.errno == ENOENT: # No such file or directory raise diff --git a/tests/test_filelock.py b/tests/test_filelock.py index a6acfb88..69cd8d33 100644 --- a/tests/test_filelock.py +++ b/tests/test_filelock.py @@ -3,11 +3,12 @@ import inspect import logging import sys +import os 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 @@ -418,6 +419,32 @@ def decorated_method() -> None: assert not lock.is_locked +def test_multuser(tmp_path: Path) -> None: + lock_path = tmp_path / "a.lock" + lock = FileLock(str(lock_path), multiuser=True) + + lock.acquire() + assert lock.is_locked + + mode = filemode(os.stat(lock_path).st_mode) + assert mode == "-rw-rw-rw-" + + lock.release() + + +def test_multuser_soft(tmp_path: Path) -> None: + lock_path = tmp_path / "a.lock" + lock = SoftFileLock(str(lock_path), multiuser=True) + + lock.acquire() + assert lock.is_locked + + mode = filemode(os.stat(lock_path).st_mode) + assert mode == "-rwxrwxrwx" + + lock.release() + + def test_wrong_platform(tmp_path: Path) -> None: assert not inspect.isabstract(UnixFileLock) assert not inspect.isabstract(WindowsFileLock) From a5e65179884888cea02eee67f712106101f9d12d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 16:52:43 +0000 Subject: [PATCH 02/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/filelock/_api.py | 2 +- tests/test_filelock.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/filelock/_api.py b/src/filelock/_api.py index 52e1e27d..9a4f635a 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -43,7 +43,7 @@ def __init__( self, lock_file: str | os.PathLike[Any], timeout: float = -1, - multiuser: bool = False + multiuser: bool = False, ) -> None: """ Create a new lock object. diff --git a/tests/test_filelock.py b/tests/test_filelock.py index 69cd8d33..79c3045d 100644 --- a/tests/test_filelock.py +++ b/tests/test_filelock.py @@ -2,8 +2,8 @@ import inspect import logging -import sys import os +import sys import threading from contextlib import contextmanager from inspect import getframeinfo, stack From 080b8d774d1e5812c9c601b8e018fecf333a5ff2 Mon Sep 17 00:00:00 2001 From: Jahrules Date: Mon, 20 Feb 2023 13:05:16 -0500 Subject: [PATCH 03/11] fixed a bug in windows File Perms --- src/filelock/_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/filelock/_windows.py b/src/filelock/_windows.py index 2f0ce4b4..52d449ed 100644 --- a/src/filelock/_windows.py +++ b/src/filelock/_windows.py @@ -25,9 +25,9 @@ def _acquire(self) -> None: os.umask(0) mode = 0o666 else: - mode = 511 + mode = 0o511 try: - fd = os.open(self._lock_file, mode, flags) + fd = os.open(self._lock_file, flags, mode) except OSError as exception: if exception.errno == ENOENT: # No such file or directory raise From a7bae6f62b45f618a266b3f8b7d0af5ad4274c12 Mon Sep 17 00:00:00 2001 From: Jahrules Date: Mon, 20 Feb 2023 13:14:18 -0500 Subject: [PATCH 04/11] flake8 fixes --- src/filelock/_api.py | 6 +++--- src/filelock/_unix.py | 2 +- src/filelock/_windows.py | 2 +- tests/test_filelock.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/filelock/_api.py b/src/filelock/_api.py index 9a4f635a..0a729a18 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -43,7 +43,7 @@ def __init__( self, lock_file: str | os.PathLike[Any], timeout: float = -1, - multiuser: bool = False, + multi_user: bool = False, ) -> None: """ Create a new lock object. @@ -64,8 +64,8 @@ def __init__( # The default timeout value. self._timeout: float = timeout - # The multiuser flag - self._multiuser: bool = multiuser + # The multi_user flag + self._multi_user: bool = multi_user # We use this lock primarily for the lock counter. self._thread_lock: Lock = Lock() diff --git a/src/filelock/_unix.py b/src/filelock/_unix.py index 5f3af77a..9133612f 100644 --- a/src/filelock/_unix.py +++ b/src/filelock/_unix.py @@ -32,7 +32,7 @@ class UnixFileLock(BaseFileLock): def _acquire(self) -> None: open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC - if self._multiuser is True: + if self._multi_user is True: os.umask(0) open_mode = 0o666 else: diff --git a/src/filelock/_windows.py b/src/filelock/_windows.py index 52d449ed..a79fa07b 100644 --- a/src/filelock/_windows.py +++ b/src/filelock/_windows.py @@ -21,7 +21,7 @@ def _acquire(self) -> None: | os.O_CREAT # create file if not exists | os.O_TRUNC # truncate file if not empty ) - if self._multiuser is True: + if self._multi_user is True: os.umask(0) mode = 0o666 else: diff --git a/tests/test_filelock.py b/tests/test_filelock.py index 79c3045d..47df287e 100644 --- a/tests/test_filelock.py +++ b/tests/test_filelock.py @@ -419,9 +419,9 @@ def decorated_method() -> None: assert not lock.is_locked -def test_multuser(tmp_path: Path) -> None: +def test_multi_user(tmp_path: Path) -> None: lock_path = tmp_path / "a.lock" - lock = FileLock(str(lock_path), multiuser=True) + lock = FileLock(str(lock_path), multi_user=True) lock.acquire() assert lock.is_locked @@ -432,9 +432,9 @@ def test_multuser(tmp_path: Path) -> None: lock.release() -def test_multuser_soft(tmp_path: Path) -> None: +def test_multi_user_soft(tmp_path: Path) -> None: lock_path = tmp_path / "a.lock" - lock = SoftFileLock(str(lock_path), multiuser=True) + lock = SoftFileLock(str(lock_path), multi_user=True) lock.acquire() assert lock.is_locked From 98695ddfffb060e53dac935c3097ed2addaac1c7 Mon Sep 17 00:00:00 2001 From: Jahrules Date: Mon, 20 Feb 2023 13:24:05 -0500 Subject: [PATCH 05/11] fixed test for windows condition --- tests/test_filelock.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_filelock.py b/tests/test_filelock.py index 47df287e..759924da 100644 --- a/tests/test_filelock.py +++ b/tests/test_filelock.py @@ -440,7 +440,10 @@ def test_multi_user_soft(tmp_path: Path) -> None: assert lock.is_locked mode = filemode(os.stat(lock_path).st_mode) - assert mode == "-rwxrwxrwx" + if sys.platform == "win32": + assert mode == "-rw-rw-rw-" + else: + assert mode == "-rwxrwxrwx" lock.release() From 6bb1cff0a899e44d54a2c7a60cc513b85da094d0 Mon Sep 17 00:00:00 2001 From: Gregory Taeger Date: Mon, 20 Feb 2023 22:04:54 -0500 Subject: [PATCH 06/11] adding filemode to whitelist --- whitelist.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/whitelist.txt b/whitelist.txt index bd1b90bb..f9126698 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -5,6 +5,7 @@ caplog eacces extlinks filelock +filemode frameinfo fspath getframeinfo From acf338bc626d5615af98199373bc3196f552681b Mon Sep 17 00:00:00 2001 From: Gregory Taeger Date: Mon, 13 Mar 2023 12:53:49 -0400 Subject: [PATCH 07/11] added changelog as requested --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2e220975..f2616c8f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,9 @@ Changelog ========= +v3.10.0 (2023-03-15) +------------------- +- Add support for lockfile file mode :pr:`192 - by :user:`jahrules`. + v3.9.1 (2023-03-14) ------------------- - Use ``time.perf_counter`` instead of ``time.monotonic`` for calculating timeouts. From ea1e7578c2cf9a8d85e1ed4c75fda453453bfef9 Mon Sep 17 00:00:00 2001 From: Gregory Taeger Date: Wed, 15 Mar 2023 14:15:49 -0400 Subject: [PATCH 08/11] updated code based on comments and added tests for os.umask() --- src/filelock/_api.py | 8 ++++--- src/filelock/_soft.py | 4 ++-- src/filelock/_unix.py | 8 +------ src/filelock/_windows.py | 7 +----- tests/test_filelock.py | 47 +++++++++++++++++++++++++++++++++------- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/filelock/_api.py b/src/filelock/_api.py index 0a729a18..80afa42e 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -43,7 +43,7 @@ def __init__( self, lock_file: str | os.PathLike[Any], timeout: float = -1, - multi_user: bool = False, + mode: int = 0o644, ) -> None: """ Create a new lock object. @@ -64,8 +64,8 @@ def __init__( # The default timeout value. self._timeout: float = timeout - # The multi_user flag - self._multi_user: bool = multi_user + # The mode for the lock files + self._mode: int = mode # We use this lock primarily for the lock counter. self._thread_lock: Lock = Lock() @@ -179,7 +179,9 @@ def acquire( with self._thread_lock: if not self.is_locked: _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) + previous_umask = os.umask(0) self._acquire() + os.umask(previous_umask) # reset umask to initial value if self.is_locked: _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) diff --git a/src/filelock/_soft.py b/src/filelock/_soft.py index cb09799a..6d953407 100644 --- a/src/filelock/_soft.py +++ b/src/filelock/_soft.py @@ -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 diff --git a/src/filelock/_unix.py b/src/filelock/_unix.py index 9133612f..6a71d8c4 100644 --- a/src/filelock/_unix.py +++ b/src/filelock/_unix.py @@ -32,13 +32,7 @@ class UnixFileLock(BaseFileLock): def _acquire(self) -> None: open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC - if self._multi_user is True: - os.umask(0) - open_mode = 0o666 - else: - # This is the default mode - open_mode = 511 - fd = os.open(self._lock_file, open_flags, open_mode) + fd = os.open(self._lock_file, open_flags, self._mode) try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: diff --git a/src/filelock/_windows.py b/src/filelock/_windows.py index a79fa07b..cb324c94 100644 --- a/src/filelock/_windows.py +++ b/src/filelock/_windows.py @@ -21,13 +21,8 @@ def _acquire(self) -> None: | os.O_CREAT # create file if not exists | os.O_TRUNC # truncate file if not empty ) - if self._multi_user is True: - os.umask(0) - mode = 0o666 - else: - mode = 0o511 try: - fd = os.open(self._lock_file, flags, mode) + fd = os.open(self._lock_file, flags, self._mode) except OSError as exception: if exception.errno == ENOENT: # No such file or directory raise diff --git a/tests/test_filelock.py b/tests/test_filelock.py index 759924da..27dc433d 100644 --- a/tests/test_filelock.py +++ b/tests/test_filelock.py @@ -419,9 +419,9 @@ def decorated_method() -> None: assert not lock.is_locked -def test_multi_user(tmp_path: Path) -> None: +def test_lock_mode(tmp_path: Path) -> None: lock_path = tmp_path / "a.lock" - lock = FileLock(str(lock_path), multi_user=True) + lock = FileLock(str(lock_path), mode=0o666) lock.acquire() assert lock.is_locked @@ -432,18 +432,49 @@ def test_multi_user(tmp_path: Path) -> None: lock.release() -def test_multi_user_soft(tmp_path: Path) -> None: +def test_lock_mode_soft(tmp_path: Path) -> None: lock_path = tmp_path / "a.lock" - lock = SoftFileLock(str(lock_path), multi_user=True) + lock = SoftFileLock(str(lock_path), mode=0o666) lock.acquire() assert lock.is_locked mode = filemode(os.stat(lock_path).st_mode) - if sys.platform == "win32": - assert mode == "-rw-rw-rw-" - else: - assert mode == "-rwxrwxrwx" + 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() From 31f634a464825a21cba41910e8c3ea7c1a9ceab0 Mon Sep 17 00:00:00 2001 From: Gregory Taeger Date: Wed, 15 Mar 2023 14:44:20 -0400 Subject: [PATCH 09/11] updates as requested --- docs/changelog.rst | 2 +- src/filelock/_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f2616c8f..62e717e4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,7 +2,7 @@ Changelog ========= v3.10.0 (2023-03-15) ------------------- -- Add support for lockfile file mode :pr:`192 - by :user:`jahrules`. +- Add support for explicit file modes for lockfiles :pr:`192 - by :user:`jahrules`. v3.9.1 (2023-03-14) ------------------- diff --git a/src/filelock/_api.py b/src/filelock/_api.py index 80afa42e..74e4d9da 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -52,7 +52,7 @@ def __init__( :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 multiuser: if True, make the lock file permissions rw/rw/rw for all users + : param mode: file permissions for the lockfile to have. default: 0o644 (-rw-r--r--) """ # The path to the lock file. self._lock_file: str = os.fspath(lock_file) From 4259eaefa4bee8be23080be6689c8882fcb7ecb7 Mon Sep 17 00:00:00 2001 From: Gregory Taeger Date: Wed, 15 Mar 2023 15:47:41 -0400 Subject: [PATCH 10/11] changes as requested --- src/filelock/_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/filelock/_api.py b/src/filelock/_api.py index 74e4d9da..ced93795 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -52,7 +52,7 @@ def __init__( :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 to have. default: 0o644 (-rw-r--r--) + : param mode: file permissions for the lockfile. """ # The path to the lock file. self._lock_file: str = os.fspath(lock_file) @@ -182,7 +182,6 @@ def acquire( previous_umask = os.umask(0) self._acquire() 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 From b4d42bb32730971446fcf894b9768a23184fca2c Mon Sep 17 00:00:00 2001 From: Gregory Taeger Date: Wed, 15 Mar 2023 16:32:00 -0400 Subject: [PATCH 11/11] updated os.umask to be inside a try:finally --- src/filelock/_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/filelock/_api.py b/src/filelock/_api.py index ced93795..8018cccb 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -180,8 +180,10 @@ def acquire( if not self.is_locked: _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) previous_umask = os.umask(0) - self._acquire() - os.umask(previous_umask) # reset umask to initial value + 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