Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-filelock for openSUSE:Factory
checked in at 2023-04-29 17:27:34
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-filelock (Old)
and /work/SRC/openSUSE:Factory/.python-filelock.new.1533 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-filelock"
Sat Apr 29 17:27:34 2023 rev:12 rq:1083340 version:3.12.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-filelock/python-filelock.changes
2023-04-22 22:01:56.693748952 +0200
+++
/work/SRC/openSUSE:Factory/.python-filelock.new.1533/python-filelock.changes
2023-04-29 17:27:35.646404889 +0200
@@ -1,0 +2,28 @@
+Tue Apr 25 23:29:13 UTC 2023 - John Vandenberg <[email protected]>
+
+- Update to v3.12.0
+ * Make the thread local behaviour something the caller can
+ enable/disable via a flag during the lock creation. on by default.
+ * Better error handling on Windows.
+- from v3.11.0
+ * Make the lock thread local.
+- from v3.10.7
+ * Use fchmod instead of chmod to work around bug in PyPy via Anaconda.
+- from v3.10.6
+ * Enhance the robustness of the try/catch block in _soft.py.
+- from v3.10.5
+ * Add explicit error check as certain UNIX filesystems do not support
+ flock.
+- from v3.10.4
+ * Update os.open to preserve mode= for certain edge cases.
+- from v3.10.3
+ * Fix permission issue
+- from v3.10.2
+ * Bug fix for using filelock with threaded programs causing undesired
+ file permissions
+- from v3.10.1
+ * Handle pickle for :class:`filelock.Timeout`
+- from v3.10.0
+ * Add support for explicit file modes for lockfiles
+
+-------------------------------------------------------------------
Old:
----
filelock-3.9.1.tar.gz
New:
----
filelock-3.12.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-filelock.spec ++++++
--- /var/tmp/diff_new_pack.lAp0tF/_old 2023-04-29 17:27:36.250407417 +0200
+++ /var/tmp/diff_new_pack.lAp0tF/_new 2023-04-29 17:27:36.254407435 +0200
@@ -19,7 +19,7 @@
%{?sle15_python_module_pythons}
Name: python-filelock
-Version: 3.9.1
+Version: 3.12.0
Release: 0
Summary: Platform Independent File Lock in Python
License: Unlicense
@@ -29,6 +29,7 @@
BuildRequires: %{python_module hatchling}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module pytest}
+BuildRequires: %{python_module pytest-mock}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
@@ -51,7 +52,7 @@
%python_expand %fdupes %{buildroot}/%{$python_sitelib}
%check
-%pytest
+%pytest -rs
%files %{python_files}
%doc README.md
++++++ filelock-3.9.1.tar.gz -> filelock-3.12.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/PKG-INFO new/filelock-3.12.0/PKG-INFO
--- old/filelock-3.9.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/filelock-3.12.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: filelock
-Version: 3.9.1
+Version: 3.12.0
Summary: A platform independent file lock.
Project-URL: Documentation, https://py-filelock.readthedocs.io
Project-URL: Homepage, https://github.com/tox-dev/py-filelock
@@ -22,15 +22,17 @@
Classifier: Topic :: System
Requires-Python: >=3.7
Provides-Extra: docs
-Requires-Dist: furo>=2022.12.7; extra == 'docs'
-Requires-Dist: sphinx-autodoc-typehints!=1.23.4,>=1.22; extra == 'docs'
+Requires-Dist: furo>=2023.3.27; extra == 'docs'
+Requires-Dist: sphinx-autodoc-typehints!=1.23.4,>=1.23; extra == 'docs'
Requires-Dist: sphinx>=6.1.3; extra == 'docs'
Provides-Extra: testing
Requires-Dist: covdefaults>=2.3; extra == 'testing'
-Requires-Dist: coverage>=7.2.1; extra == 'testing'
+Requires-Dist: coverage>=7.2.3; extra == 'testing'
+Requires-Dist: diff-cover>=7.5; extra == 'testing'
Requires-Dist: pytest-cov>=4; extra == 'testing'
+Requires-Dist: pytest-mock>=3.10; extra == 'testing'
Requires-Dist: pytest-timeout>=2.1; extra == 'testing'
-Requires-Dist: pytest>=7.2.2; extra == 'testing'
+Requires-Dist: pytest>=7.3.1; extra == 'testing'
Description-Content-Type: text/markdown
# py-filelock
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/pyproject.toml
new/filelock-3.12.0/pyproject.toml
--- old/filelock-3.9.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
+++ new/filelock-3.12.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -2,7 +2,7 @@
build-backend = "hatchling.build"
requires = [
"hatch-vcs>=0.3",
- "hatchling>=1.13",
+ "hatchling>=1.14",
]
[project]
@@ -35,15 +35,17 @@
"version",
]
optional-dependencies.docs = [
- "furo>=2022.12.7",
+ "furo>=2023.3.27",
"sphinx>=6.1.3",
- "sphinx-autodoc-typehints!=1.23.4,>=1.22",
+ "sphinx-autodoc-typehints!=1.23.4,>=1.23",
]
optional-dependencies.testing = [
"covdefaults>=2.3",
- "coverage>=7.2.1",
- "pytest>=7.2.2",
+ "coverage>=7.2.3",
+ "diff-cover>=7.5",
+ "pytest>=7.3.1",
"pytest-cov>=4",
+ "pytest-mock>=3.10",
"pytest-timeout>=2.1",
]
urls.Documentation = "https://py-filelock.readthedocs.io"
@@ -56,6 +58,21 @@
build.targets.sdist.include = ["/src", "/tests"]
version.source = "vcs"
+[tool.black]
+line-length = 120
+
+[tool.isort]
+profile = "black"
+known_first_party = ["filelock"]
+add_imports = ["from __future__ import annotations"]
+
+[tool.flake8]
+max-complexity = 22
+max-line-length = 120
+unused-arguments-ignore-abstract-functions = true
+noqa-require-code = true
+dictionaries = ["en_US", "python", "technical", "django"]
+
[tool.coverage]
html.show_contexts = true
html.skip_covered = false
@@ -65,13 +82,6 @@
run.parallel = true
run.plugins = ["covdefaults"]
-[tool.black]
-line-length = 120
-
-[tool.isort]
-profile = "black"
-known_first_party = ["filelock"]
-
[tool.mypy]
python_version = "3.11"
show_error_codes = true
@@ -80,10 +90,3 @@
[tool.pep8]
max-line-length = "120"
-
-[tool.flake8]
-max-complexity = 22
-max-line-length = 120
-unused-arguments-ignore-abstract-functions = true
-noqa-require-code = true
-dictionaries = ["en_US", "python", "technical", "django"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/src/filelock/__init__.py
new/filelock-3.12.0/src/filelock/__init__.py
--- old/filelock-3.9.1/src/filelock/__init__.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/src/filelock/__init__.py 2020-02-02
01:00:00.000000000 +0100
@@ -32,11 +32,10 @@
if warnings is not None:
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`.
if TYPE_CHECKING:
FileLock = SoftFileLock
else:
+ #: Alias for the lock, which should be used for the current platform.
FileLock = _FileLock
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/src/filelock/_api.py
new/filelock-3.12.0/src/filelock/_api.py
--- old/filelock-3.9.1/src/filelock/_api.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/src/filelock/_api.py 2020-02-02 01:00:00.000000000
+0100
@@ -6,7 +6,8 @@
import time
import warnings
from abc import ABC, abstractmethod
-from threading import Lock
+from dataclasses import dataclass
+from threading import local
from types import TracebackType
from typing import Any
@@ -36,10 +37,47 @@
self.lock.release()
+@dataclass
+class FileLockContext:
+ """
+ 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.
+
+ #: The path to the lock file.
+ lock_file: str
+
+ #: The default timeout value.
+ timeout: float
+
+ #: The mode for the lock files
+ mode: int
+
+ #: 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
+
+ #: The lock counter is used for implementing the nested locking mechanism.
+ lock_counter: int = 0 # When the lock is acquired is increased and the
lock is only released, when this value is 0
+
+
+class ThreadLocalFileContext(FileLockContext, local):
+ """
+ A thread local version of the ``FileLockContext`` class.
+ """
+
+
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,
+ thread_local: bool = True,
+ ) -> None:
"""
Create a new lock object.
@@ -47,28 +85,29 @@
: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.
- """
- # The path to the lock file.
- self._lock_file: str = os.fspath(lock_file)
-
- # The file descriptor for the *_lock_file* as it is returned by the
os.open() function.
- # This file lock is only NOT None, if the object currently holds the
lock.
- self._lock_file_fd: int | None = None
-
- # The default timeout value.
- self._timeout: float = timeout
-
- # We use this lock primarily for the lock counter.
- self._thread_lock: Lock = Lock()
-
- # 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.
- self._lock_counter: int = 0
+ :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.
+ """
+ self._is_thread_local = thread_local
+
+ # 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,
+ }
+ self._context: FileLockContext = (ThreadLocalFileContext if
thread_local else FileLockContext)(**kwargs)
+
+ def is_thread_local(self) -> bool:
+ """:return: a flag indicating if this lock is thread local or not"""
+ return self._is_thread_local
@property
def lock_file(self) -> str:
""":return: path to the lock file"""
- return self._lock_file
+ return self._context.lock_file
@property
def timeout(self) -> float:
@@ -77,7 +116,7 @@
.. versionadded:: 2.0.0
"""
- return self._timeout
+ return self._context.timeout
@timeout.setter
def timeout(self, value: float | str) -> None:
@@ -86,16 +125,16 @@
:param value: the new value, in seconds
"""
- self._timeout = float(value)
+ self._context.timeout = float(value)
@abstractmethod
def _acquire(self) -> None:
- """If the file lock could be acquired, self._lock_file_fd holds the
file descriptor of the lock file."""
+ """If the file lock could be acquired, self._context.lock_file_fd
holds the file descriptor of the lock file."""
raise NotImplementedError
@abstractmethod
def _release(self) -> None:
- """Releases the lock and sets self._lock_file_fd to None."""
+ """Releases the lock and sets self._context.lock_file_fd to None."""
raise NotImplementedError
@property
@@ -108,7 +147,14 @@
This was previously a method and is now a property.
"""
- return self._lock_file_fd is not None
+ return self._context.lock_file_fd is not None
+
+ @property
+ def lock_counter(self) -> int:
+ """
+ :return: The number of times this lock has been acquired (but not yet
released).
+ """
+ return self._context.lock_counter
def acquire(
self,
@@ -126,7 +172,7 @@
:param poll_interval: interval of trying to acquire the lock file
:param poll_intervall: deprecated, kept for backwards compatibility,
use ``poll_interval`` instead
:param blocking: defaults to True. If False, function will return
immediately if it cannot obtain a lock on the
- first attempt. Otherwise this method will block until the timeout
expires or the lock is acquired.
+ first attempt. Otherwise, this method will block until the timeout
expires or the lock is acquired.
:raises Timeout: if fails to acquire lock within the timeout period
:return: a context object that will unlock the file when the context
is exited
@@ -151,7 +197,7 @@
"""
# Use the default timeout, if no timeout is provided.
if timeout is None:
- timeout = self.timeout
+ timeout = self._context.timeout
if poll_intervall is not None:
msg = "use poll_interval instead of poll_intervall"
@@ -159,35 +205,31 @@
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._context.lock_counter += 1
lock_id = id(self)
- lock_filename = self._lock_file
+ lock_filename = self.lock_file
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)
+ raise Timeout(lock_filename)
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)
+ 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)
except BaseException: # Something did go wrong, so decrement the
counter.
- with self._thread_lock:
- self._lock_counter = max(0, self._lock_counter - 1)
+ self._context.lock_counter = max(0, self._context.lock_counter - 1)
raise
return AcquireReturnProxy(lock=self)
@@ -198,17 +240,16 @@
: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._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)
+ if self.is_locked:
+ self._context.lock_counter -= 1
+
+ if self._context.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._context.lock_counter = 0
+ _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)
def __enter__(self) -> BaseFileLock:
"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/src/filelock/_error.py
new/filelock-3.12.0/src/filelock/_error.py
--- old/filelock-3.9.1/src/filelock/_error.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/src/filelock/_error.py 2020-02-02 01:00:00.000000000
+0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/src/filelock/_soft.py
new/filelock-3.12.0/src/filelock/_soft.py
--- old/filelock-3.9.1/src/filelock/_soft.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/src/filelock/_soft.py 2020-02-02 01:00:00.000000000
+0100
@@ -2,42 +2,40 @@
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
+from ._util import raise_on_not_writable_file
class SoftFileLock(BaseFileLock):
"""Simply watches the existence of the lock file."""
def _acquire(self) -> None:
- raise_on_exist_ro_file(self._lock_file)
+ raise_on_not_writable_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._context.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._context.lock_file_fd = file_handler
def _release(self) -> None:
- os.close(self._lock_file_fd) # type: ignore # the lock file is
definitely not None
- self._lock_file_fd = None
+ os.close(self._context.lock_file_fd) # type: ignore # the lock file
is definitely not None
+ self._context.lock_file_fd = None
try:
- os.remove(self._lock_file)
+ os.remove(self.lock_file)
except OSError: # the file is already deleted and that's what we want
pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/src/filelock/_unix.py
new/filelock-3.12.0/src/filelock/_unix.py
--- old/filelock-3.9.1/src/filelock/_unix.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/src/filelock/_unix.py 2020-02-02 01:00:00.000000000
+0100
@@ -2,6 +2,7 @@
import os
import sys
+from errno import ENOSYS
from typing import cast
from ._api import BaseFileLock
@@ -31,21 +32,27 @@
"""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._context.mode)
+ try:
+ 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:
+ 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
+ self._context.lock_file_fd = fd
def _release(self) -> None:
# Do not remove the lockfile:
# https://github.com/tox-dev/py-filelock/issues/31
#
https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition
- fd = cast(int, self._lock_file_fd)
- self._lock_file_fd = None
+ fd = cast(int, self._context.lock_file_fd)
+ self._context.lock_file_fd = None
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/src/filelock/_util.py
new/filelock-3.12.0/src/filelock/_util.py
--- old/filelock-3.9.1/src/filelock/_util.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/src/filelock/_util.py 2020-02-02 01:00:00.000000000
+0100
@@ -2,9 +2,18 @@
import os
import stat
+import sys
+from errno import EACCES, EISDIR
-def raise_on_exist_ro_file(filename: str) -> None:
+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
+ :param filename: file to check
+ :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
except OSError:
@@ -12,9 +21,17 @@
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):
- raise PermissionError(f"Permission denied: {filename!r}")
+ raise PermissionError(EACCES, "Permission denied", filename)
+
+ if stat.S_ISDIR(file_stat.st_mode):
+ if sys.platform == "win32": # pragma: win32 cover
+ # On Windows, this is PermissionError
+ raise PermissionError(EACCES, "Permission denied", filename)
+ else: # pragma: win32 no cover
+ # On linux / macOS, this is IsADirectoryError
+ raise IsADirectoryError(EISDIR, "Is a directory", filename)
__all__ = [
- "raise_on_exist_ro_file",
+ "raise_on_not_writable_file",
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/src/filelock/_windows.py
new/filelock-3.12.0/src/filelock/_windows.py
--- old/filelock-3.9.1/src/filelock/_windows.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/src/filelock/_windows.py 2020-02-02
01:00:00.000000000 +0100
@@ -2,46 +2,48 @@
import os
import sys
-from errno import ENOENT
+from errno import EACCES
from typing import cast
from ._api import BaseFileLock
-from ._util import raise_on_exist_ro_file
+from ._util import raise_on_not_writable_file
if sys.platform == "win32": # pragma: win32 cover
import msvcrt
class WindowsFileLock(BaseFileLock):
- """Uses the :func:`msvcrt.locking` function to hard lock the lock file
on windows systems."""
+ """Uses the :func:`msvcrt.locking` function to hard lock the lock file
on Windows systems."""
def _acquire(self) -> None:
- raise_on_exist_ro_file(self._lock_file)
- mode = (
+ raise_on_not_writable_file(self.lock_file)
+ 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
+ | os.O_TRUNC # truncate file if not empty
)
try:
- fd = os.open(self._lock_file, mode)
+ fd = os.open(self.lock_file, flags, self._context.mode)
except OSError as exception:
- if exception.errno == ENOENT: # No such file or directory
+ if exception.errno != EACCES: # has no access to this lock
raise
else:
try:
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
- except OSError:
- os.close(fd)
+ except OSError as exception:
+ os.close(fd) # close file first
+ if exception.errno != EACCES: # file is already locked
+ raise
else:
- self._lock_file_fd = fd
+ self._context.lock_file_fd = fd
def _release(self) -> None:
- fd = cast(int, self._lock_file_fd)
- self._lock_file_fd = None
+ fd = cast(int, self._context.lock_file_fd)
+ self._context.lock_file_fd = None
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
os.close(fd)
try:
- os.remove(self._lock_file)
+ os.remove(self.lock_file)
# Probably another instance of the application hat acquired the
file lock.
except OSError:
pass
@@ -49,7 +51,7 @@
else: # pragma: win32 no cover
class WindowsFileLock(BaseFileLock):
- """Uses the :func:`msvcrt.locking` function to hard lock the lock file
on windows systems."""
+ """Uses the :func:`msvcrt.locking` function to hard lock the lock file
on Windows systems."""
def _acquire(self) -> None:
raise NotImplementedError
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/src/filelock/version.py
new/filelock-3.12.0/src/filelock/version.py
--- old/filelock-3.9.1/src/filelock/version.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/src/filelock/version.py 2020-02-02 01:00:00.000000000
+0100
@@ -1,4 +1,4 @@
# file generated by setuptools_scm
# don't change, don't track in version control
-__version__ = version = '3.9.1'
-__version_tuple__ = version_tuple = (3, 9, 1)
+__version__ = version = '3.12.0'
+__version_tuple__ = version_tuple = (3, 12, 0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/tests/test_error.py
new/filelock-3.12.0/tests/test_error.py
--- old/filelock-3.9.1/tests/test_error.py 1970-01-01 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/tests/test_error.py 2020-02-02 01:00:00.000000000
+0100
@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+import pickle
+
+from filelock import Timeout
+
+
+def test_timeout_str() -> None:
+ timeout = Timeout("/path/to/lock")
+ assert str(timeout) == "The file lock '/path/to/lock' could not be
acquired."
+
+
+def test_timeout_repr() -> None:
+ timeout = Timeout("/path/to/lock")
+ assert repr(timeout) == "Timeout('/path/to/lock')"
+
+
+def test_timeout_lock_file() -> None:
+ timeout = Timeout("/path/to/lock")
+ assert timeout.lock_file == "/path/to/lock"
+
+
+def test_timeout_pickle() -> None:
+ timeout = Timeout("/path/to/lock")
+ timeout_loaded = pickle.loads(pickle.dumps(timeout))
+
+ assert timeout.__class__ == timeout_loaded.__class__
+ assert str(timeout) == str(timeout_loaded)
+ assert repr(timeout) == repr(timeout_loaded)
+ assert timeout.lock_file == timeout_loaded.lock_file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/filelock-3.9.1/tests/test_filelock.py
new/filelock-3.12.0/tests/test_filelock.py
--- old/filelock-3.9.1/tests/test_filelock.py 2020-02-02 01:00:00.000000000
+0100
+++ new/filelock-3.12.0/tests/test_filelock.py 2020-02-02 01:00:00.000000000
+0100
@@ -2,17 +2,22 @@
import inspect
import logging
+import os
import sys
import threading
+from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
+from errno import ENOSYS
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
+from uuid import uuid4
import pytest
from _pytest.logging import LogCaptureFixture
+from pytest_mock import MockerFixture
from filelock import (
BaseFileLock,
@@ -78,6 +83,10 @@
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have
read only folders")
[email protected](
+ sys.platform != "win32" and os.geteuid() == 0, # noqa: SC200
+ reason="Cannot make a read only file (that the current user: root can't
read)",
+)
def test_ro_folder(lock_type: type[BaseFileLock], tmp_path_ro: Path) -> None:
lock = lock_type(str(tmp_path_ro / "a"))
with pytest.raises(PermissionError, match="Permission denied"):
@@ -93,18 +102,46 @@
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
[email protected](
+ sys.platform != "win32" and os.geteuid() == 0, # noqa: SC200
+ reason="Cannot make a read only file (that the current user: root can't
read)",
+)
def test_ro_file(lock_type: type[BaseFileLock], tmp_file_ro: Path) -> None:
lock = lock_type(str(tmp_file_ro))
with pytest.raises(PermissionError, match="Permission denied"):
lock.acquire()
+WindowsOnly = pytest.mark.skipif(sys.platform != "win32", reason="Windows
only")
+
+
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
-def test_missing_directory(lock_type: type[BaseFileLock], tmp_path_ro: Path)
-> None:
- lock_path = tmp_path_ro / "a" / "b"
- lock = lock_type(str(lock_path))
[email protected](
+ ("expected_error", "match", "bad_lock_file"),
+ [
+ pytest.param(FileNotFoundError, "No such file or directory:", "a/b",
id="non_existent_directory"),
+ pytest.param(FileNotFoundError, "No such file or directory:", "",
id="blank_filename"),
+ pytest.param(ValueError, "embedded null (byte|character)", "\0",
id="null_byte"),
+ pytest.param(
+ PermissionError if sys.platform == "win32" else IsADirectoryError,
+ "Permission denied:" if sys.platform == "win32" else "Is a
directory",
+ ".",
+ id="current_directory",
+ ),
+ ]
+ + [pytest.param(OSError, "Invalid argument", i, id=f"invalid_{i}",
marks=WindowsOnly) for i in '<>:"|?*\a']
+ + [pytest.param(PermissionError, "Permission denied:", i,
id=f"permission_{i}", marks=WindowsOnly) for i in "/\\"],
+)
[email protected](5) # timeout in case of infinite loop
+def test_bad_lock_file(
+ lock_type: type[BaseFileLock],
+ expected_error: type[Exception],
+ match: str,
+ bad_lock_file: str,
+) -> None:
+ lock = lock_type(bad_lock_file)
- with pytest.raises(OSError, match="No such file or directory:"):
+ with pytest.raises(expected_error, match=match):
lock.acquire()
@@ -334,8 +371,8 @@
with lock as lock_1:
assert lock is lock_1
assert lock.is_locked
- raise Exception
- except Exception:
+ raise ValueError
+ except ValueError:
assert not lock.is_locked
@@ -349,8 +386,8 @@
with lock.acquire() as lock_1:
assert lock is lock_1
assert lock.is_locked
- raise Exception
- except Exception:
+ raise ValueError
+ except ValueError:
assert not lock.is_locked
@@ -382,9 +419,8 @@
def test_cleanup_soft_lock(tmp_path: Path) -> None:
# tests if the lock file is removed after use
lock_path = tmp_path / "a"
- lock = SoftFileLock(str(lock_path))
- with lock:
+ with SoftFileLock(lock_path):
assert lock_path.exists()
assert not lock_path.exists()
@@ -396,9 +432,9 @@
with pytest.deprecated_call(match="use poll_interval instead of
poll_intervall") as checker:
lock.acquire(poll_intervall=0.05) # the deprecation warning will be
captured by the checker
- frameinfo = getframeinfo(stack()[0][0]) # get frameinfo of current
file and lineno (+1 than the above lineno)
+ frame_info = getframeinfo(stack()[0][0]) # get frame info of current
file and lineno (+1 than the above lineno)
for warning in checker:
- if warning.filename == frameinfo.filename and warning.lineno + 1
== frameinfo.lineno: # pragma: no cover
+ if warning.filename == frame_info.filename and warning.lineno + 1
== frame_info.lineno: # pragma: no cover
break
else: # pragma: no cover
pytest.fail("No warnings of stacklevel=2 matching.")
@@ -418,15 +454,178 @@
assert not lock.is_locked
+def test_lock_mode(tmp_path: Path) -> None:
+ # test file lock permissions are independent of umask
+ lock_path = tmp_path / "a.lock"
+ lock = FileLock(str(lock_path), mode=0o666)
+
+ # set umask so permissions can be anticipated
+ initial_umask = os.umask(0o022)
+ try:
+ lock.acquire()
+ assert lock.is_locked
+
+ mode = filemode(os.stat(lock_path).st_mode)
+ assert mode == "-rw-rw-rw-"
+ finally:
+ os.umask(initial_umask)
+
+ lock.release()
+
+
+def test_lock_mode_soft(tmp_path: Path) -> None:
+ # test soft lock permissions are dependent of umask
+ lock_path = tmp_path / "a.lock"
+ lock = SoftFileLock(str(lock_path), mode=0o666)
+
+ # set umask so permissions can be anticipated
+ initial_umask = os.umask(0o022)
+ try:
+ 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 == "-rw-r--r--"
+ finally:
+ os.umask(initial_umask)
+
+ 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)
assert inspect.isabstract(BaseFileLock)
lock_type = UnixFileLock if sys.platform == "win32" else WindowsFileLock
- lock = lock_type(str(tmp_path / "lockfile"))
+ lock = lock_type(tmp_path / "lockfile")
with pytest.raises(NotImplementedError):
lock.acquire()
with pytest.raises(NotImplementedError):
lock._release()
+
+
[email protected](sys.platform == "win32", reason="flock not run on windows")
+def test_flock_not_implemented_unix(tmp_path: Path, mocker: MockerFixture) ->
None:
+ mocker.patch("fcntl.flock", side_effect=OSError(ENOSYS, "mock error"))
+ with pytest.raises(NotImplementedError):
+ with FileLock(tmp_path / "a.lock"):
+ pass
+
+
+def test_soft_errors(tmp_path: Path, mocker: MockerFixture) -> None:
+ mocker.patch("os.open", side_effect=OSError(ENOSYS, "mock error"))
+ with pytest.raises(OSError, match="mock error"):
+ SoftFileLock(tmp_path / "a.lock").acquire()
+
+
+def _check_file_read_write(txt_file: Path) -> None:
+ for _ in range(3):
+ uuid = str(uuid4())
+ txt_file.write_text(uuid)
+ assert txt_file.read_text() == uuid
+
+
[email protected]("lock_type", [FileLock, SoftFileLock])
+def test_thrashing_with_thread_pool_passing_lock_to_threads(tmp_path: Path,
lock_type: type[BaseFileLock]) -> None:
+ def mess_with_file(lock_: BaseFileLock) -> None:
+ with lock_:
+ _check_file_read_write(txt_file)
+
+ lock_file, txt_file = tmp_path / "test.txt.lock", tmp_path / "test.txt"
+ lock = lock_type(lock_file)
+ results = []
+ with ThreadPoolExecutor() as executor:
+ for _ in range(100):
+ results.append(executor.submit(mess_with_file, lock))
+
+ assert all(r.result() is None for r in results)
+
+
[email protected]("lock_type", [FileLock, SoftFileLock])
+def test_thrashing_with_thread_pool_global_lock(tmp_path: Path, lock_type:
type[BaseFileLock]) -> None:
+ def mess_with_file() -> None:
+ with lock:
+ _check_file_read_write(txt_file)
+
+ lock_file, txt_file = tmp_path / "test.txt.lock", tmp_path / "test.txt"
+ lock = lock_type(lock_file)
+ results = []
+ with ThreadPoolExecutor() as executor:
+ for _ in range(100):
+ results.append(executor.submit(mess_with_file))
+
+ assert all(r.result() is None for r in results)
+
+
[email protected]("lock_type", [FileLock, SoftFileLock])
+def test_thrashing_with_thread_pool_lock_recreated_in_each_thread(
+ tmp_path: Path,
+ lock_type: type[BaseFileLock],
+) -> None:
+ def mess_with_file() -> None:
+ with lock_type(lock_file):
+ _check_file_read_write(txt_file)
+
+ lock_file, txt_file = tmp_path / "test.txt.lock", tmp_path / "test.txt"
+ results = []
+ with ThreadPoolExecutor() as executor:
+ for _ in range(100):
+ results.append(executor.submit(mess_with_file))
+
+ assert all(r.result() is None for r in results)
+
+
[email protected]("lock_type", [FileLock, SoftFileLock])
+def test_lock_can_be_non_thread_local(
+ tmp_path: Path,
+ lock_type: type[BaseFileLock],
+) -> None:
+ lock = lock_type(tmp_path / "test.lock", thread_local=False)
+
+ for _ in range(2):
+ thread = threading.Thread(target=lock.acquire, kwargs={"timeout": 2})
+ thread.start()
+ thread.join()
+
+ assert lock.lock_counter == 2
+
+ lock.release(force=True)