diff --git a/CHANGES.md b/CHANGES.md index 3587d865..5b5d7825 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * Update certifi 2023.05.07 to 2023.07.22 * Update CacheControl 0.12.11 (c05ef9e) to 0.13.1 (783a338) * Update feedparser 6.0.10 (859ac57) to 6.0.10 (9865dec) +* Update filelock 3.12.0 (b4713c9) to 3.12.4 (c1163ae) * Update Msgpack 1.0.5 (0516c2c) to 1.0.6 (e1d3d5d) * Update package resource API 67.5.1 (f51eccd) to 68.1.2 (1ef36f2) * Update soupsieve 2.3.2.post1 (792d566) to 2.4.1 (2e66beb) diff --git a/lib/filelock/__init__.py b/lib/filelock/__init__.py index c7492ba5..0b8c1d2f 100644 --- a/lib/filelock/__init__.py +++ b/lib/filelock/__init__.py @@ -24,7 +24,7 @@ __version__: str = version if sys.platform == "win32": # pragma: win32 cover _FileLock: type[BaseFileLock] = WindowsFileLock -else: # pragma: win32 no cover +else: # pragma: win32 no cover # noqa: PLR5501 if has_fcntl: _FileLock: type[BaseFileLock] = UnixFileLock else: @@ -32,7 +32,7 @@ else: # pragma: win32 no cover if warnings is not None: warnings.warn("only soft file lock is available", stacklevel=2) -if TYPE_CHECKING: +if TYPE_CHECKING: # noqa: SIM108 FileLock = SoftFileLock else: #: Alias for the lock, which should be used for the current platform. diff --git a/lib/filelock/_api.py b/lib/filelock/_api.py index 66710cc5..8a40ccd0 100644 --- a/lib/filelock/_api.py +++ b/lib/filelock/_api.py @@ -8,11 +8,20 @@ import warnings from abc import ABC, abstractmethod from dataclasses import dataclass from threading import local -from types import TracebackType -from typing import Any +from typing import TYPE_CHECKING, Any from ._error import Timeout +if TYPE_CHECKING: + import sys + from types import TracebackType + + if sys.version_info >= (3, 11): # pragma: no cover (py311+) + from typing import Self + else: # pragma: no cover ( None: self.lock.release() @dataclass class FileLockContext: - """ - A dataclass which holds the context for a ``BaseFileLock`` object. - """ + """A dataclass which holds the context for a ``BaseFileLock`` object.""" # The context is held in a separate class to allow optional use of thread local storage via the # ThreadLocalFileContext class. @@ -63,9 +70,7 @@ class FileLockContext: class ThreadLocalFileContext(FileLockContext, local): - """ - A thread local version of the ``FileLockContext`` class. - """ + """A thread local version of the ``FileLockContext`` class.""" class BaseFileLock(ABC, contextlib.ContextDecorator): @@ -73,10 +78,10 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): def __init__( self, - lock_file: str | os.PathLike[Any], + lock_file: str | os.PathLike[str], timeout: float = -1, mode: int = 0o644, - thread_local: bool = True, + thread_local: bool = True, # noqa: FBT001, FBT002 ) -> None: """ Create a new lock object. @@ -151,9 +156,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): @property def lock_counter(self) -> int: - """ - :return: The number of times this lock has been acquired (but not yet released). - """ + """:return: The number of times this lock has been acquired (but not yet released).""" return self._context.lock_counter def acquire( @@ -218,22 +221,21 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): if self.is_locked: _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) break - elif blocking is False: + if blocking is False: _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename) - raise Timeout(lock_filename) - elif 0 <= timeout < time.perf_counter() - start_time: + raise Timeout(lock_filename) # noqa: TRY301 + if 0 <= timeout < time.perf_counter() - start_time: _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename) - raise Timeout(lock_filename) - else: - msg = "Lock %s not acquired on %s, waiting %s seconds ..." - _LOGGER.debug(msg, lock_id, lock_filename, poll_interval) - time.sleep(poll_interval) + raise Timeout(lock_filename) # noqa: TRY301 + msg = "Lock %s not acquired on %s, waiting %s seconds ..." + _LOGGER.debug(msg, lock_id, lock_filename, poll_interval) + time.sleep(poll_interval) except BaseException: # Something did go wrong, so decrement the counter. self._context.lock_counter = max(0, self._context.lock_counter - 1) raise return AcquireReturnProxy(lock=self) - def release(self, force: bool = False) -> None: + def release(self, force: bool = False) -> None: # noqa: FBT001, FBT002 """ Releases the file lock. Please note, that the lock is only completely released, if the lock counter is 0. Also note, that the lock file itself is not automatically deleted. @@ -251,7 +253,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): self._context.lock_counter = 0 _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) - def __enter__(self) -> BaseFileLock: + def __enter__(self) -> Self: """ Acquire the lock. @@ -262,9 +264,9 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): def __exit__( self, - exc_type: type[BaseException] | None, # noqa: U100 - exc_value: BaseException | None, # noqa: U100 - traceback: TracebackType | None, # noqa: U100 + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: """ Release the lock. diff --git a/lib/filelock/_error.py b/lib/filelock/_error.py index e2bd6530..f7ff08c0 100644 --- a/lib/filelock/_error.py +++ b/lib/filelock/_error.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -class Timeout(TimeoutError): +class Timeout(TimeoutError): # noqa: N818 """Raised when the lock could not be acquired in *timeout* seconds.""" def __init__(self, lock_file: str) -> None: diff --git a/lib/filelock/_soft.py b/lib/filelock/_soft.py index 57a6c04d..28c67f74 100644 --- a/lib/filelock/_soft.py +++ b/lib/filelock/_soft.py @@ -2,10 +2,12 @@ from __future__ import annotations import os import sys +from contextlib import suppress from errno import EACCES, EEXIST +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): @@ -13,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 @@ -32,12 +35,11 @@ class SoftFileLock(BaseFileLock): self._context.lock_file_fd = file_handler def _release(self) -> None: - os.close(self._context.lock_file_fd) # type: ignore # the lock file is definitely not None + assert self._context.lock_file_fd is not None # noqa: S101 + os.close(self._context.lock_file_fd) # the lock file is definitely not None self._context.lock_file_fd = None - try: - os.remove(self.lock_file) - except OSError: # the file is already deleted and that's what we want - pass + with suppress(OSError): # the file is already deleted and that's what we want + Path(self.lock_file).unlink() __all__ = [ diff --git a/lib/filelock/_unix.py b/lib/filelock/_unix.py index 641bd8d9..93ce3be5 100644 --- a/lib/filelock/_unix.py +++ b/lib/filelock/_unix.py @@ -2,10 +2,12 @@ from __future__ import annotations import os import sys +from contextlib import suppress from errno import ENOSYS 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 @@ -32,18 +34,18 @@ else: # pragma: win32 no cover """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) - try: + with suppress(PermissionError): # This locked is not owned by this UID os.fchmod(fd, self._context.mode) - except PermissionError: - pass # This locked is not owned by this UID try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 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") + msg = "FileSystem does not appear to support flock; user SoftFileLock instead" + raise NotImplementedError(msg) from exception else: self._context.lock_file_fd = fd diff --git a/lib/filelock/_util.py b/lib/filelock/_util.py index 81cbeaf2..543c1394 100644 --- a/lib/filelock/_util.py +++ b/lib/filelock/_util.py @@ -4,6 +4,7 @@ import os import stat import sys from errno import EACCES, EISDIR +from pathlib import Path def raise_on_not_writable_file(filename: str) -> None: @@ -12,12 +13,12 @@ def raise_on_not_writable_file(filename: str) -> None: This is done so files that will never be writable can be separated from files that are writable but currently locked :param filename: file to check - :raises OSError: as if the file was opened for writing + :raises OSError: as if the file was opened for writing. """ - try: - file_stat = os.stat(filename) # use stat to do exists + can write to check without race condition + try: # use stat to do exists + can write to check without race condition + file_stat = os.stat(filename) # noqa: PTH116 except OSError: - return None # swallow does not exist or other errors + return # swallow does not exist or other errors if file_stat.st_mtime != 0: # if os.stat returns but modification is zero that's an invalid os.stat - ignore it if not (file_stat.st_mode & stat.S_IWUSR): @@ -27,11 +28,20 @@ def raise_on_not_writable_file(filename: str) -> None: if sys.platform == "win32": # pragma: win32 cover # On Windows, this is PermissionError raise PermissionError(EACCES, "Permission denied", filename) - else: # pragma: win32 no cover + else: # pragma: win32 no cover # noqa: RET506 # On linux / macOS, this is IsADirectoryError 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/lib/filelock/_windows.py b/lib/filelock/_windows.py index 799644c8..8db55dcb 100644 --- a/lib/filelock/_windows.py +++ b/lib/filelock/_windows.py @@ -2,11 +2,13 @@ from __future__ import annotations import os import sys +from contextlib import suppress from errno import EACCES +from pathlib import Path 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 @@ -16,6 +18,7 @@ if sys.platform == "win32": # pragma: win32 cover 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 @@ -42,11 +45,8 @@ if sys.platform == "win32": # pragma: win32 cover msvcrt.locking(fd, msvcrt.LK_UNLCK, 1) os.close(fd) - try: - os.remove(self.lock_file) - # Probably another instance of the application hat acquired the file lock. - except OSError: - pass + with suppress(OSError): # Probably another instance of the application hat acquired the file lock. + Path(self.lock_file).unlink() else: # pragma: win32 no cover diff --git a/lib/filelock/version.py b/lib/filelock/version.py index f8e7b3ea..e6e96d53 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.12.0' -__version_tuple__ = version_tuple = (3, 12, 0) +__version__ = version = '3.12.4' +__version_tuple__ = version_tuple = (3, 12, 4)