diff --git a/CHANGES.md b/CHANGES.md index a80d17c0..6603a5db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.29.0 (2023-xx-xx xx:xx:00 UTC) +* Update filelock 3.9.0 (ce3e891) to 3.11.0 (d3241b9) * Update Msgpack 1.0.4 (b5acfd5) to 1.0.5 (0516c2c) * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) * Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) diff --git a/lib/filelock/__init__.py b/lib/filelock/__init__.py index 36fe7e43..31d2bce1 100644 --- a/lib/filelock/__init__.py +++ b/lib/filelock/__init__.py @@ -30,7 +30,7 @@ else: # pragma: win32 no cover else: _FileLock = SoftFileLock if warnings is not None: - warnings.warn("only soft file lock is available") + warnings.warn("only soft file lock is available", stacklevel=2) #: Alias for the lock, which should be used for the current platform. On Windows, this is an alias for # :class:`WindowsFileLock`, on Unix for :class:`UnixFileLock` and otherwise for :class:`SoftFileLock`. diff --git a/lib/filelock/_api.py b/lib/filelock/_api.py index 273b82e6..958369a6 100644 --- a/lib/filelock/_api.py +++ b/lib/filelock/_api.py @@ -6,7 +6,7 @@ import os import time import warnings from abc import ABC, abstractmethod -from threading import Lock +from threading import local from types import TracebackType from typing import Any @@ -36,10 +36,15 @@ class AcquireReturnProxy: self.lock.release() -class BaseFileLock(ABC, contextlib.ContextDecorator): +class BaseFileLock(ABC, contextlib.ContextDecorator, local): """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, + ) -> None: """ Create a new lock object. @@ -47,6 +52,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): :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) @@ -58,8 +64,8 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): # The default timeout value. self._timeout: float = timeout - # We use this lock primarily for the lock counter. - self._thread_lock: Lock = Lock() + # The mode for the lock files + self._mode: int = mode # The lock counter is used for implementing the nested locking mechanism. Whenever the lock is acquired, the # counter is increased and the lock is only released, when this value is 0 again. @@ -159,26 +165,23 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): poll_interval = poll_intervall # Increment the number right at the beginning. We can still undo it, if something fails. - with self._thread_lock: - self._lock_counter += 1 + self._lock_counter += 1 lock_id = id(self) lock_filename = self._lock_file - start_time = time.monotonic() + start_time = time.perf_counter() try: while True: - with self._thread_lock: - if not self.is_locked: - _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) - self._acquire() - + if not self.is_locked: + _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) + self._acquire() if self.is_locked: _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) break elif blocking is False: _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename) raise Timeout(self._lock_file) - elif 0 <= timeout < time.monotonic() - start_time: + elif 0 <= timeout < time.perf_counter() - start_time: _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename) raise Timeout(self._lock_file) else: @@ -186,8 +189,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): _LOGGER.debug(msg, lock_id, lock_filename, poll_interval) time.sleep(poll_interval) except BaseException: # Something did go wrong, so decrement the counter. - with self._thread_lock: - self._lock_counter = max(0, self._lock_counter - 1) + self._lock_counter = max(0, self._lock_counter - 1) raise return AcquireReturnProxy(lock=self) @@ -198,18 +200,16 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): :param force: If true, the lock counter is ignored and the lock is released in every case/ """ - with self._thread_lock: + if self.is_locked: + self._lock_counter -= 1 - if self.is_locked: - self._lock_counter -= 1 + if self._lock_counter == 0 or force: + lock_id, lock_filename = id(self), self._lock_file - if self._lock_counter == 0 or force: - lock_id, lock_filename = id(self), self._lock_file - - _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename) - self._release() - self._lock_counter = 0 - _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) + _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename) + self._release() + self._lock_counter = 0 + _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) def __enter__(self) -> BaseFileLock: """ diff --git a/lib/filelock/_error.py b/lib/filelock/_error.py index b3885214..e2bd6530 100644 --- a/lib/filelock/_error.py +++ b/lib/filelock/_error.py @@ -1,15 +1,28 @@ from __future__ import annotations +from typing import Any + class Timeout(TimeoutError): """Raised when the lock could not be acquired in *timeout* seconds.""" def __init__(self, lock_file: str) -> None: - #: The path of the file lock. - self.lock_file = lock_file + super().__init__() + self._lock_file = lock_file + + def __reduce__(self) -> str | tuple[Any, ...]: + return self.__class__, (self._lock_file,) # Properly pickle the exception def __str__(self) -> str: - return f"The file lock '{self.lock_file}' could not be acquired." + return f"The file lock '{self._lock_file}' could not be acquired." + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.lock_file!r})" + + @property + def lock_file(self) -> str: + """:return: The path of the file lock.""" + return self._lock_file __all__ = [ diff --git a/lib/filelock/_soft.py b/lib/filelock/_soft.py index cb09799a..697f6c4c 100644 --- a/lib/filelock/_soft.py +++ b/lib/filelock/_soft.py @@ -2,7 +2,7 @@ from __future__ import annotations import os import sys -from errno import EACCES, EEXIST, ENOENT +from errno import EACCES, EEXIST from ._api import BaseFileLock from ._util import raise_on_exist_ro_file @@ -14,24 +14,22 @@ 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) - except OSError as exception: - if exception.errno == EEXIST: # expected if cannot lock - pass - elif exception.errno == ENOENT: # No such file or directory - parent directory is missing + file_handler = os.open(self._lock_file, flags, self._mode) + except OSError as exception: # re-raise unless expected exception + if not ( + exception.errno == EEXIST # lock already exist + or (exception.errno == EACCES and sys.platform == "win32") # has no access to this lock + ): # pragma: win32 no cover raise - elif exception.errno == EACCES and sys.platform != "win32": # pragma: win32 no cover - # Permission denied - parent dir is R/O - raise # note windows does not allow you to make a folder r/o only files else: - self._lock_file_fd = fd + self._lock_file_fd = file_handler def _release(self) -> None: os.close(self._lock_file_fd) # type: ignore # the lock file is definitely not None diff --git a/lib/filelock/_unix.py b/lib/filelock/_unix.py index 03b612c9..4c0a2603 100644 --- a/lib/filelock/_unix.py +++ b/lib/filelock/_unix.py @@ -2,6 +2,7 @@ from __future__ import annotations import os import sys +from errno import ENOSYS from typing import cast from ._api import BaseFileLock @@ -31,12 +32,18 @@ else: # pragma: win32 no cover """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: + os.fchmod(fd, self._mode) + except PermissionError: + pass # This locked is not owned by this UID try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except OSError: + except OSError as exception: os.close(fd) + if exception.errno == ENOSYS: # NotImplemented error + raise NotImplementedError("FileSystem does not appear to support flock; user SoftFileLock instead") else: self._lock_file_fd = fd diff --git a/lib/filelock/_windows.py b/lib/filelock/_windows.py index 60e68cb9..cb324c94 100644 --- a/lib/filelock/_windows.py +++ b/lib/filelock/_windows.py @@ -16,13 +16,13 @@ if sys.platform == "win32": # pragma: win32 cover 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 diff --git a/lib/filelock/version.py b/lib/filelock/version.py index d20e218b..16805e82 100644 --- a/lib/filelock/version.py +++ b/lib/filelock/version.py @@ -1,4 +1,4 @@ # file generated by setuptools_scm # don't change, don't track in version control -__version__ = version = '3.9.0' -__version_tuple__ = version_tuple = (3, 9, 0) +__version__ = version = '3.11.0' +__version_tuple__ = version_tuple = (3, 11, 0)