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 <jay...@gmail.com>
+
+- 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")
+@pytest.mark.skipif(
+    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])
+@pytest.mark.skipif(
+    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))
+@pytest.mark.parametrize(
+    ("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 "/\\"],
+)
+@pytest.mark.timeout(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()
+
+
+@pytest.mark.skipif(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
+
+
+@pytest.mark.parametrize("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)
+
+
+@pytest.mark.parametrize("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)
+
+
+@pytest.mark.parametrize("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)
+
+
+@pytest.mark.parametrize("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)

Reply via email to