Update filelock 3.12.4 (c1163ae) → 3.14.0 (8556141).

This commit is contained in:
JackDandy 2024-06-05 09:37:08 +01:00
parent 488feadc88
commit 5715726b22
6 changed files with 114 additions and 26 deletions

View file

@ -1,6 +1,7 @@
### 3.32.0 (2024-xx-xx xx:xx:00 UTC)
* Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2)
* Update filelock 3.12.4 (c1163ae) to 3.14.0 (8556141)
* Update idna library 3.4 (cab054c) to 3.7 (1d365e1)
* Update Requests library 2.31.0 (8812812) to 2.32.3 (0e322af)
* Update urllib3 2.0.7 (56f01e0) to 2.2.1 (54d6edf)

View file

@ -5,6 +5,7 @@ A platform independent file lock that supports the with-statement.
:no-value:
"""
from __future__ import annotations
import sys
@ -32,7 +33,7 @@ else: # pragma: win32 no cover # noqa: PLR5501
if warnings is not None:
warnings.warn("only soft file lock is available", stacklevel=2)
if TYPE_CHECKING: # noqa: SIM108
if TYPE_CHECKING:
FileLock = SoftFileLock
else:
#: Alias for the lock, which should be used for the current platform.
@ -40,12 +41,12 @@ else:
__all__ = [
"__version__",
"AcquireReturnProxy",
"BaseFileLock",
"FileLock",
"SoftFileLock",
"Timeout",
"UnixFileLock",
"WindowsFileLock",
"BaseFileLock",
"AcquireReturnProxy",
"__version__",
]

View file

@ -9,6 +9,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass
from threading import local
from typing import TYPE_CHECKING, Any
from weakref import WeakValueDictionary
from ._error import Timeout
@ -29,7 +30,7 @@ _LOGGER = logging.getLogger("filelock")
# is not called twice when entering the with statement. If we would simply return *self*, the lock would be acquired
# again in the *__enter__* method of the BaseFileLock, but not released again automatically. issue #37 (memory leak)
class AcquireReturnProxy:
"""A context aware object that will release the lock file when exiting."""
"""A context-aware object that will release the lock file when exiting."""
def __init__(self, lock: BaseFileLock) -> None:
self.lock = lock
@ -62,6 +63,9 @@ class FileLockContext:
#: The mode for the lock files
mode: int
#: Whether the lock should be blocking or not
blocking: bool
#: The file descriptor for the *_lock_file* as it is returned by the os.open() function, not None when lock held
lock_file_fd: int | None = None
@ -76,32 +80,74 @@ class ThreadLocalFileContext(FileLockContext, local):
class BaseFileLock(ABC, contextlib.ContextDecorator):
"""Abstract base class for a file lock object."""
def __init__(
_instances: WeakValueDictionary[str, BaseFileLock]
def __new__( # noqa: PLR0913
cls,
lock_file: str | os.PathLike[str],
timeout: float = -1,
mode: int = 0o644,
thread_local: bool = True, # noqa: ARG003, FBT001, FBT002
*,
blocking: bool = True, # noqa: ARG003
is_singleton: bool = False,
**kwargs: dict[str, Any], # capture remaining kwargs for subclasses # noqa: ARG003
) -> Self:
"""Create a new lock object or if specified return the singleton instance for the lock file."""
if not is_singleton:
return super().__new__(cls)
instance = cls._instances.get(str(lock_file))
if not instance:
instance = super().__new__(cls)
cls._instances[str(lock_file)] = instance
elif timeout != instance.timeout or mode != instance.mode:
msg = "Singleton lock instances cannot be initialized with differing arguments"
raise ValueError(msg)
return instance # type: ignore[return-value] # https://github.com/python/mypy/issues/15322
def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None:
"""Setup unique state for lock subclasses."""
super().__init_subclass__(**kwargs)
cls._instances = WeakValueDictionary()
def __init__( # noqa: PLR0913
self,
lock_file: str | os.PathLike[str],
timeout: float = -1,
mode: int = 0o644,
thread_local: bool = True, # noqa: FBT001, FBT002
*,
blocking: bool = True,
is_singleton: bool = False,
) -> 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.
:param thread_local: Whether this object's internal context should be thread local or not.
If this is set to ``False`` then the lock will be reentrant across threads.
: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
:param thread_local: Whether this object's internal context should be thread local or not. If this is set to \
``False`` then the lock will be reentrant across threads.
:param blocking: whether the lock should be blocking or not
:param is_singleton: If this is set to ``True`` then only one instance of this class will be created \
per lock file. This is useful if you want to use the lock object for reentrant locking without needing \
to pass the same object around.
"""
self._is_thread_local = thread_local
self._is_singleton = is_singleton
# Create the context. Note that external code should not work with the context directly and should instead use
# Create the context. Note that external code should not work with the context directly and should instead use
# properties of this class.
kwargs: dict[str, Any] = {
"lock_file": os.fspath(lock_file),
"timeout": timeout,
"mode": mode,
"blocking": blocking,
}
self._context: FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext)(**kwargs)
@ -109,6 +155,11 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
""":return: a flag indicating if this lock is thread local or not"""
return self._is_thread_local
@property
def is_singleton(self) -> bool:
""":return: a flag indicating if this lock is singleton or not"""
return self._is_singleton
@property
def lock_file(self) -> str:
""":return: path to the lock file"""
@ -129,9 +180,30 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
Change the default timeout value.
:param value: the new value, in seconds
"""
self._context.timeout = float(value)
@property
def blocking(self) -> bool:
""":return: whether the locking is blocking or not"""
return self._context.blocking
@blocking.setter
def blocking(self, value: bool) -> None:
"""
Change the default blocking value.
:param value: the new value as bool
"""
self._context.blocking = value
@property
def mode(self) -> int:
""":return: the file permissions for the lockfile"""
return self._context.mode
@abstractmethod
def _acquire(self) -> None:
"""If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file."""
@ -165,7 +237,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
poll_interval: float = 0.05,
*,
poll_intervall: float | None = None,
blocking: bool = True,
blocking: bool | None = None,
) -> AcquireReturnProxy:
"""
Try to acquire the file lock.
@ -202,6 +274,9 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
if timeout is None:
timeout = self._context.timeout
if blocking is None:
blocking = self._context.blocking
if poll_intervall is not None:
msg = "use poll_interval instead of poll_intervall"
warnings.warn(msg, DeprecationWarning, stacklevel=2)
@ -237,10 +312,11 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
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.
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.
:param force: If true, the lock counter is ignored and the lock is released in every case/
"""
if self.is_locked:
self._context.lock_counter -= 1
@ -258,6 +334,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
Acquire the lock.
:return: the lock object
"""
self.acquire()
return self
@ -274,6 +351,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
:param exc_type: the exception type if raised
:param exc_value: the exception value if raised
:param traceback: the exception traceback if raised
"""
self.release()
@ -283,6 +361,6 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
__all__ = [
"BaseFileLock",
"AcquireReturnProxy",
"BaseFileLock",
]

View file

@ -4,6 +4,7 @@ import os
import sys
from contextlib import suppress
from errno import ENOSYS
from pathlib import Path
from typing import cast
from ._api import BaseFileLock
@ -35,7 +36,9 @@ else: # pragma: win32 no cover
def _acquire(self) -> None:
ensure_directory_exists(self.lock_file)
open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC
open_flags = os.O_RDWR | os.O_TRUNC
if not Path(self.lock_file).exists():
open_flags |= os.O_CREAT
fd = os.open(self.lock_file, open_flags, self._context.mode)
with suppress(PermissionError): # This locked is not owned by this UID
os.fchmod(fd, self._context.mode)
@ -44,7 +47,7 @@ else: # pragma: win32 no cover
except OSError as exception:
os.close(fd)
if exception.errno == ENOSYS: # NotImplemented error
msg = "FileSystem does not appear to support flock; user SoftFileLock instead"
msg = "FileSystem does not appear to support flock; use SoftFileLock instead"
raise NotImplementedError(msg) from exception
else:
self._context.lock_file_fd = fd
@ -60,6 +63,6 @@ else: # pragma: win32 no cover
__all__ = [
"has_fcntl",
"UnixFileLock",
"has_fcntl",
]

View file

@ -10,10 +10,13 @@ from pathlib import Path
def raise_on_not_writable_file(filename: str) -> None:
"""
Raise an exception if attempting to open the file for writing would fail.
This is done so files that will never be writable can be separated from
files that are writable but currently locked
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.
"""
try: # use stat to do exists + can write to check without race condition
file_stat = os.stat(filename) # noqa: PTH116
@ -35,13 +38,15 @@ def raise_on_not_writable_file(filename: str) -> None:
def ensure_directory_exists(filename: Path | str) -> None:
"""
Ensure the directory containing the file exists (create it if necessary)
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",
"raise_on_not_writable_file",
]

View file

@ -1,4 +1,4 @@
# file generated by setuptools_scm
# don't change, don't track in version control
__version__ = version = '3.12.4'
__version_tuple__ = version_tuple = (3, 12, 4)
__version__ = version = '3.14.0'
__version_tuple__ = version_tuple = (3, 14, 0)