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 2024-06-17 19:27:33 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-filelock (Old) and /work/SRC/openSUSE:Factory/.python-filelock.new.19518 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-filelock" Mon Jun 17 19:27:33 2024 rev:20 rq:1181236 version:3.15.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-filelock/python-filelock.changes 2024-05-03 19:44:21.780443991 +0200 +++ /work/SRC/openSUSE:Factory/.python-filelock.new.19518/python-filelock.changes 2024-06-17 19:28:18.219640112 +0200 @@ -1,0 +2,11 @@ +Mon Jun 17 06:00:15 UTC 2024 - Dirk Müller <dmuel...@suse.com> + +- update to 3.15.1: + * Hotfix: Restore __init__ method; more robust initialization + for singleton locks +- update to 3.15.0: + * asyncio support + * Don't initialize BaseFileLock when just returning existing + instance + +------------------------------------------------------------------- Old: ---- filelock-3.14.0.tar.gz New: ---- filelock-3.15.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-filelock.spec ++++++ --- /var/tmp/diff_new_pack.gdXsYO/_old 2024-06-17 19:28:18.887664558 +0200 +++ /var/tmp/diff_new_pack.gdXsYO/_new 2024-06-17 19:28:18.887664558 +0200 @@ -19,15 +19,17 @@ %{?sle15_python_module_pythons} Name: python-filelock -Version: 3.14.0 +Version: 3.15.1 Release: 0 Summary: Platform Independent File Lock in Python License: Unlicense URL: https://github.com/tox-dev/py-filelock Source: https://files.pythonhosted.org/packages/source/f/filelock/filelock-%{version}.tar.gz +BuildRequires: %{python_module asyncio} BuildRequires: %{python_module hatch_vcs} BuildRequires: %{python_module hatchling} BuildRequires: %{python_module pip} +BuildRequires: %{python_module pytest-asyncio} BuildRequires: %{python_module pytest-mock} BuildRequires: %{python_module pytest} BuildRequires: %{python_module wheel} @@ -36,6 +38,7 @@ %if 0%{?python_version_nodots} < 311 Requires: python-typing_extensions >= 4.7.1 %endif +Requires: python-asyncio BuildArch: noarch %python_subpackages ++++++ filelock-3.14.0.tar.gz -> filelock-3.15.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/filelock-3.14.0/PKG-INFO new/filelock-3.15.1/PKG-INFO --- old/filelock-3.14.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/filelock-3.15.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.3 Name: filelock -Version: 3.14.0 +Version: 3.15.1 Summary: A platform independent file lock. Project-URL: Documentation, https://py-filelock.readthedocs.io Project-URL: Homepage, https://github.com/tox-dev/py-filelock @@ -33,6 +33,7 @@ Requires-Dist: covdefaults>=2.3; extra == 'testing' Requires-Dist: coverage>=7.3.2; extra == 'testing' Requires-Dist: diff-cover>=8.0.1; extra == 'testing' +Requires-Dist: pytest-asyncio>=0.21; extra == 'testing' Requires-Dist: pytest-cov>=4.1; extra == 'testing' Requires-Dist: pytest-mock>=3.12; extra == 'testing' Requires-Dist: pytest-timeout>=2.2; extra == 'testing' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/filelock-3.14.0/pyproject.toml new/filelock-3.15.1/pyproject.toml --- old/filelock-3.14.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/filelock-3.15.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -17,7 +17,9 @@ "user", ] license = "Unlicense" -maintainers = [{ name = "Bernát Gábor", email = "gaborjber...@gmail.com" }] +maintainers = [ + { name = "Bernát Gábor", email = "gaborjber...@gmail.com" }, +] requires-python = ">=3.8" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -48,12 +50,13 @@ "coverage>=7.3.2", "diff-cover>=8.0.1", "pytest>=7.4.3", + "pytest-asyncio>=0.21", "pytest-cov>=4.1", "pytest-mock>=3.12", "pytest-timeout>=2.2", ] optional-dependencies.typing = [ - 'typing-extensions>=4.8; python_version < "3.11"', + "typing-extensions>=4.8; python_version<'3.11'", ] urls.Documentation = "https://py-filelock.readthedocs.io" urls.Homepage = "https://github.com/tox-dev/py-filelock" @@ -62,39 +65,48 @@ [tool.hatch] build.hooks.vcs.version-file = "src/filelock/version.py" -build.targets.sdist.include = ["/src", "/tests", "/tox.ini"] +build.targets.sdist.include = [ + "/src", + "/tests", + "/tox.ini", +] version.source = "vcs" [tool.ruff] -line-length = 120 target-version = "py38" -lint.isort = { known-first-party = ["filelock"], required-imports = ["from __future__ import annotations"] } -lint.select = ["ALL"] +line-length = 120 +format.preview = true +format.docstring-code-line-length = 100 +format.docstring-code-format = true +lint.select = [ + "ALL", +] lint.ignore = [ "ANN101", # Missing type annotation for `self` in method - "D301", # Use `r"""` if any backslashes in a docstring - "D205", # 1 blank line required between summary line and description - "D401", # First line of docstring should be in imperative mood + "COM812", # Conflict with formatter + "CPY", # No copyright statements "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible + "D205", # 1 blank line required between summary line and description "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible - "S104", # Possible binding to all interface - "COM812", # Conflict with formatter + "D301", # Use `r"""` if any backslashes in a docstring + "D401", # First line of docstring should be in imperative mood "ISC001", # Conflict with formatter - "CPY", # No copyright statements + "S104", # Possible binding to all interface ] -lint.preview = true -format.preview = true -format.docstring-code-format = true -format.docstring-code-line-length = 100 -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # asserts allowed in tests... +lint.per-file-ignores."tests/**/*.py" = [ + "D", # don"t care about documentation in tests "FBT", # don"t care about booleans as positional arguments in tests "INP001", # no implicit namespace - "D", # don"t care about documentation in tests - "S603", # `subprocess` call: check for execution of untrusted input "PLR2004", # Magic value used in comparison, consider replacing with a constant variable + "S101", # asserts allowed in tests... + "S603", # `subprocess` call: check for execution of untrusted input ] +lint.isort = { known-first-party = [ + "filelock", +], required-imports = [ + "from __future__ import annotations", +] } +lint.preview = true [tool.codespell] builtin = "clear,usage,en-GB_to_en-US" @@ -105,14 +117,31 @@ [tool.coverage] html.show_contexts = true html.skip_covered = false -paths.source = ["src", ".tox/*/lib/*/site-packages", ".tox\\*\\Lib\\site-packages", "**/src", "**\\src"] -paths.other = [".", "*/filelock", "*\\filelock"] +paths.source = [ + "src", + ".tox/*/lib/*/site-packages", + ".tox\\*\\Lib\\site-packages", + "**/src", + "**\\src", +] +paths.other = [ + ".", + "*/filelock", + "*\\filelock", +] report.fail_under = 76 run.parallel = true -run.plugins = ["covdefaults"] +run.plugins = [ + "covdefaults", +] [tool.mypy] python_version = "3.11" show_error_codes = true strict = true -overrides = [{ module = ["appdirs.*", "jnius.*"], ignore_missing_imports = true }] +overrides = [ + { module = [ + "appdirs.*", + "jnius.*", + ], ignore_missing_imports = true }, +] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/filelock-3.14.0/src/filelock/__init__.py new/filelock-3.15.1/src/filelock/__init__.py --- old/filelock-3.14.0/src/filelock/__init__.py 2020-02-02 01:00:00.000000000 +0100 +++ new/filelock-3.15.1/src/filelock/__init__.py 2020-02-02 01:00:00.000000000 +0100 @@ -17,6 +17,13 @@ from ._soft import SoftFileLock from ._unix import UnixFileLock, has_fcntl from ._windows import WindowsFileLock +from .asyncio import ( + AsyncAcquireReturnProxy, + AsyncSoftFileLock, + AsyncUnixFileLock, + AsyncWindowsFileLock, + BaseAsyncFileLock, +) from .version import version #: version of the project as a string @@ -25,23 +32,34 @@ if sys.platform == "win32": # pragma: win32 cover _FileLock: type[BaseFileLock] = WindowsFileLock + _AsyncFileLock: type[BaseAsyncFileLock] = AsyncWindowsFileLock else: # pragma: win32 no cover # noqa: PLR5501 if has_fcntl: _FileLock: type[BaseFileLock] = UnixFileLock + _AsyncFileLock: type[BaseAsyncFileLock] = AsyncUnixFileLock else: _FileLock = SoftFileLock + _AsyncFileLock = AsyncSoftFileLock if warnings is not None: warnings.warn("only soft file lock is available", stacklevel=2) if TYPE_CHECKING: FileLock = SoftFileLock + AsyncFileLock = AsyncSoftFileLock else: #: Alias for the lock, which should be used for the current platform. FileLock = _FileLock + AsyncFileLock = _AsyncFileLock __all__ = [ "AcquireReturnProxy", + "AsyncAcquireReturnProxy", + "AsyncFileLock", + "AsyncSoftFileLock", + "AsyncUnixFileLock", + "AsyncWindowsFileLock", + "BaseAsyncFileLock", "BaseFileLock", "FileLock", "SoftFileLock", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/filelock-3.14.0/src/filelock/_api.py new/filelock-3.15.1/src/filelock/_api.py --- old/filelock-3.14.0/src/filelock/_api.py 2020-02-02 01:00:00.000000000 +0100 +++ new/filelock-3.15.1/src/filelock/_api.py 2020-02-02 01:00:00.000000000 +0100 @@ -80,18 +80,18 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): """Abstract base class for a file lock object.""" - _instances: WeakValueDictionary[str, BaseFileLock] + _instances: WeakValueDictionary[str, Self] 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 + timeout: float = -1, # noqa: ARG003 + mode: int = 0o644, # noqa: ARG003 + thread_local: bool = True, # noqa: FBT001, FBT002, ARG003 *, blocking: bool = True, # noqa: ARG003 is_singleton: bool = False, - **kwargs: dict[str, Any], # capture remaining kwargs for subclasses # noqa: ARG003 + **kwargs: Any, # capture remaining kwargs for subclasses # noqa: ARG003, ANN401 ) -> Self: """Create a new lock object or if specified return the singleton instance for the lock file.""" if not is_singleton: @@ -99,11 +99,9 @@ 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) + self = super().__new__(cls) + cls._instances[str(lock_file)] = self + return self return instance # type: ignore[return-value] # https://github.com/python/mypy/issues/15322 @@ -138,6 +136,34 @@ to pass the same object around. """ + if is_singleton and hasattr(self, "_context"): + # test whether other parameters match existing instance. + if not self.is_singleton: + msg = "__init__ should only be called on initialized object if it is a singleton" + raise RuntimeError(msg) + + params_to_check = { + "thread_local": (thread_local, self.is_thread_local()), + "timeout": (timeout, self.timeout), + "mode": (mode, self.mode), + "blocking": (blocking, self.blocking), + } + + non_matching_params = { + name: (passed_param, set_param) + for name, (passed_param, set_param) in params_to_check.items() + if passed_param != set_param + } + if not non_matching_params: + return # bypass initialization because object is already initialized + + # parameters do not match; raise error + msg = "Singleton lock instances cannot be initialized with differing arguments" + msg += "\nNon-matching arguments: " + for param_name, (passed_param, set_param) in non_matching_params.items(): + msg += f"\n\t{param_name} (existing lock has {set_param} but {passed_param} was passed)" + raise ValueError(msg) + self._is_thread_local = thread_local self._is_singleton = is_singleton diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/filelock-3.14.0/src/filelock/asyncio.py new/filelock-3.15.1/src/filelock/asyncio.py --- old/filelock-3.14.0/src/filelock/asyncio.py 1970-01-01 01:00:00.000000000 +0100 +++ new/filelock-3.15.1/src/filelock/asyncio.py 2020-02-02 01:00:00.000000000 +0100 @@ -0,0 +1,314 @@ +"""An asyncio-based implementation of the file lock.""" + +from __future__ import annotations + +import asyncio +import contextlib +import logging +import os +import time +from dataclasses import dataclass +from threading import local +from typing import TYPE_CHECKING, Any, Callable, NoReturn + +from ._api import BaseFileLock, FileLockContext +from ._error import Timeout +from ._soft import SoftFileLock +from ._unix import UnixFileLock +from ._windows import WindowsFileLock + +if TYPE_CHECKING: + import sys + from concurrent import futures + from types import TracebackType + + if sys.version_info >= (3, 11): # pragma: no cover (py311+) + from typing import Self + else: # pragma: no cover (<py311) + from typing_extensions import Self + + +_LOGGER = logging.getLogger("filelock") + + +@dataclass +class AsyncFileLockContext(FileLockContext): + """A dataclass which holds the context for a ``BaseAsyncFileLock`` object.""" + + #: Whether run in executor + run_in_executor: bool = True + + #: The executor + executor: futures.Executor | None = None + + #: The loop + loop: asyncio.AbstractEventLoop | None = None + + +class AsyncThreadLocalFileContext(AsyncFileLockContext, local): + """A thread local version of the ``FileLockContext`` class.""" + + +class AsyncAcquireReturnProxy: + """A context-aware object that will release the lock file when exiting.""" + + def __init__(self, lock: BaseAsyncFileLock) -> None: # noqa: D107 + self.lock = lock + + async def __aenter__(self) -> BaseAsyncFileLock: # noqa: D105 + return self.lock + + async def __aexit__( # noqa: D105 + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self.lock.release() + + +class BaseAsyncFileLock(BaseFileLock): + """Base class for asynchronous file locks.""" + + def __init__( # noqa: PLR0913 + self, + lock_file: str | os.PathLike[str], + timeout: float = -1, + mode: int = 0o644, + thread_local: bool = False, # noqa: FBT001, FBT002 + *, + blocking: bool = True, + is_singleton: bool = False, + loop: asyncio.AbstractEventLoop | None = None, + run_in_executor: bool = True, + executor: futures.Executor | None = None, + ) -> 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 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. + :param loop: The event loop to use. If not specified, the running event loop will be used. + :param run_in_executor: If this is set to ``True`` then the lock will be acquired in an executor. + :param executor: The executor to use. If not specified, the default executor will be used. + + """ + self._is_thread_local = thread_local + self._is_singleton = is_singleton + if thread_local and run_in_executor: + msg = "run_in_executor is not supported when thread_local is True" + raise ValueError(msg) + + # 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, + "loop": loop, + "run_in_executor": run_in_executor, + "executor": executor, + } + self._context: AsyncFileLockContext = (AsyncThreadLocalFileContext if thread_local else AsyncFileLockContext)( + **kwargs + ) + + @property + def run_in_executor(self) -> bool: + """::return: whether run in executor.""" + return self._context.run_in_executor + + @property + def executor(self) -> futures.Executor | None: + """::return: the executor.""" + return self._context.executor + + @executor.setter + def executor(self, value: futures.Executor | None) -> None: # pragma: no cover + """ + Change the executor. + + :param value: the new executor or ``None`` + :type value: futures.Executor | None + + """ + self._context.executor = value + + @property + def loop(self) -> asyncio.AbstractEventLoop | None: + """::return: the event loop.""" + return self._context.loop + + async def acquire( # type: ignore[override] + self, + timeout: float | None = None, + poll_interval: float = 0.05, + *, + blocking: bool | None = None, + ) -> AsyncAcquireReturnProxy: + """ + Try to acquire the file lock. + + :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default + :attr:`~BaseFileLock.timeout` is and if ``timeout < 0``, there is no timeout and + this method will block until the lock could be acquired + :param poll_interval: interval of trying to acquire the lock file + :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. + :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 + + .. code-block:: python + + # You can use this method in the context manager (recommended) + with lock.acquire(): + pass + + # Or use an equivalent try-finally construct: + lock.acquire() + try: + pass + finally: + lock.release() + + """ + # Use the default timeout, if no timeout is provided. + if timeout is None: + timeout = self._context.timeout + + if blocking is None: + blocking = self._context.blocking + + # Increment the number right at the beginning. We can still undo it, if something fails. + self._context.lock_counter += 1 + + lock_id = id(self) + lock_filename = self.lock_file + start_time = time.perf_counter() + try: + while True: + if not self.is_locked: + _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) + await self._run_internal_method(self._acquire) + if self.is_locked: + _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) + break + if blocking is False: + _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename) + 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) # noqa: TRY301 + msg = "Lock %s not acquired on %s, waiting %s seconds ..." + _LOGGER.debug(msg, lock_id, lock_filename, poll_interval) + await asyncio.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 AsyncAcquireReturnProxy(lock=self) + + async def release(self, force: bool = False) -> None: # type: ignore[override] # 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. + + :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 + + 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) + await self._run_internal_method(self._release) + self._context.lock_counter = 0 + _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) + + async def _run_internal_method(self, method: Callable[[], Any]) -> None: + if asyncio.iscoroutinefunction(method): + await method() + elif self.run_in_executor: + loop = self.loop or asyncio.get_running_loop() + await loop.run_in_executor(self.executor, method) + else: + method() + + def __enter__(self) -> NoReturn: + """ + Replace old __enter__ method to avoid using it. + + NOTE: DO NOT USE `with` FOR ASYNCIO LOCKS, USE `async with` INSTEAD. + + :return: none + :rtype: NoReturn + """ + msg = "Do not use `with` for asyncio locks, use `async with` instead." + raise NotImplementedError(msg) + + async def __aenter__(self) -> Self: + """ + Acquire the lock. + + :return: the lock object + + """ + await self.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """ + Release the lock. + + :param exc_type: the exception type if raised + :param exc_value: the exception value if raised + :param traceback: the exception traceback if raised + + """ + await self.release() + + def __del__(self) -> None: + """Called when the lock object is deleted.""" + with contextlib.suppress(RuntimeError): + loop = self.loop or asyncio.get_running_loop() + if not loop.is_running(): # pragma: no cover + loop.run_until_complete(self.release(force=True)) + else: + loop.create_task(self.release(force=True)) + + +class AsyncSoftFileLock(SoftFileLock, BaseAsyncFileLock): + """Simply watches the existence of the lock file.""" + + +class AsyncUnixFileLock(UnixFileLock, BaseAsyncFileLock): + """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" + + +class AsyncWindowsFileLock(WindowsFileLock, BaseAsyncFileLock): + """Uses the :func:`msvcrt.locking` to hard lock the lock file on windows systems.""" + + +__all__ = [ + "AsyncAcquireReturnProxy", + "AsyncSoftFileLock", + "AsyncUnixFileLock", + "AsyncWindowsFileLock", + "BaseAsyncFileLock", +] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/filelock-3.14.0/src/filelock/version.py new/filelock-3.15.1/src/filelock/version.py --- old/filelock-3.14.0/src/filelock/version.py 2020-02-02 01:00:00.000000000 +0100 +++ new/filelock-3.15.1/src/filelock/version.py 2020-02-02 01:00:00.000000000 +0100 @@ -12,5 +12,5 @@ __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE -__version__ = version = '3.14.0' -__version_tuple__ = version_tuple = (3, 14, 0) +__version__ = version = '3.15.1' +__version_tuple__ = version_tuple = (3, 15, 1) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/filelock-3.14.0/tests/test_async_filelock.py new/filelock-3.15.1/tests/test_async_filelock.py --- old/filelock-3.14.0/tests/test_async_filelock.py 1970-01-01 01:00:00.000000000 +0100 +++ new/filelock-3.15.1/tests/test_async_filelock.py 2020-02-02 01:00:00.000000000 +0100 @@ -0,0 +1,181 @@ +from __future__ import annotations + +import logging +from pathlib import Path, PurePath + +import pytest + +from filelock import AsyncFileLock, AsyncSoftFileLock, BaseAsyncFileLock, Timeout + + +@pytest.mark.parametrize("lock_type", [AsyncFileLock, AsyncSoftFileLock]) +@pytest.mark.parametrize("path_type", [str, PurePath, Path]) +@pytest.mark.parametrize("filename", ["a", "new/b", "new2/new3/c"]) +@pytest.mark.asyncio() +async def test_simple( + lock_type: type[BaseAsyncFileLock], + path_type: type[str | Path], + filename: str, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + caplog.set_level(logging.DEBUG) + + # test lock creation by passing a `str` + lock_path = tmp_path / filename + lock = lock_type(path_type(lock_path)) + async with lock as locked: + assert lock.is_locked + assert lock is locked + assert not lock.is_locked + + assert caplog.messages == [ + f"Attempting to acquire lock {id(lock)} on {lock_path}", + f"Lock {id(lock)} acquired on {lock_path}", + f"Attempting to release lock {id(lock)} on {lock_path}", + f"Lock {id(lock)} released on {lock_path}", + ] + assert [r.levelno for r in caplog.records] == [logging.DEBUG, logging.DEBUG, logging.DEBUG, logging.DEBUG] + assert [r.name for r in caplog.records] == ["filelock", "filelock", "filelock", "filelock"] + assert logging.getLogger("filelock").level == logging.NOTSET + + +@pytest.mark.parametrize("lock_type", [AsyncFileLock, AsyncSoftFileLock]) +@pytest.mark.parametrize("path_type", [str, PurePath, Path]) +@pytest.mark.parametrize("filename", ["a", "new/b", "new2/new3/c"]) +@pytest.mark.asyncio() +async def test_acquire( + lock_type: type[BaseAsyncFileLock], + path_type: type[str | Path], + filename: str, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + caplog.set_level(logging.DEBUG) + + # test lock creation by passing a `str` + lock_path = tmp_path / filename + lock = lock_type(path_type(lock_path)) + async with await lock.acquire() as locked: + assert lock.is_locked + assert lock is locked + assert not lock.is_locked + + assert caplog.messages == [ + f"Attempting to acquire lock {id(lock)} on {lock_path}", + f"Lock {id(lock)} acquired on {lock_path}", + f"Attempting to release lock {id(lock)} on {lock_path}", + f"Lock {id(lock)} released on {lock_path}", + ] + assert [r.levelno for r in caplog.records] == [logging.DEBUG, logging.DEBUG, logging.DEBUG, logging.DEBUG] + assert [r.name for r in caplog.records] == ["filelock", "filelock", "filelock", "filelock"] + assert logging.getLogger("filelock").level == logging.NOTSET + + +@pytest.mark.parametrize("lock_type", [AsyncFileLock, AsyncSoftFileLock]) +@pytest.mark.asyncio() +async def test_non_blocking(lock_type: type[BaseAsyncFileLock], tmp_path: Path) -> None: + # raises Timeout error when the lock cannot be acquired + lock_path = tmp_path / "a" + lock_1, lock_2 = lock_type(str(lock_path)), lock_type(str(lock_path)) + lock_3 = lock_type(str(lock_path), blocking=False) + lock_4 = lock_type(str(lock_path), timeout=0) + lock_5 = lock_type(str(lock_path), blocking=False, timeout=-1) + + # acquire lock 1 + await lock_1.acquire() + assert lock_1.is_locked + assert not lock_2.is_locked + assert not lock_3.is_locked + assert not lock_4.is_locked + assert not lock_5.is_locked + + # try to acquire lock 2 + with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): + await lock_2.acquire(blocking=False) + assert not lock_2.is_locked + assert lock_1.is_locked + + # try to acquire pre-parametrized `blocking=False` lock 3 with `acquire` + with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): + await lock_3.acquire() + assert not lock_3.is_locked + assert lock_1.is_locked + + # try to acquire pre-parametrized `blocking=False` lock 3 with context manager + with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): + async with lock_3: + pass + assert not lock_3.is_locked + assert lock_1.is_locked + + # try to acquire pre-parametrized `timeout=0` lock 4 with `acquire` + with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): + await lock_4.acquire() + assert not lock_4.is_locked + assert lock_1.is_locked + + # try to acquire pre-parametrized `timeout=0` lock 4 with context manager + with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): + async with lock_4: + pass + assert not lock_4.is_locked + assert lock_1.is_locked + + # blocking precedence over timeout + # try to acquire pre-parametrized `timeout=-1,blocking=False` lock 5 with `acquire` + with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): + await lock_5.acquire() + assert not lock_5.is_locked + assert lock_1.is_locked + + # try to acquire pre-parametrized `timeout=-1,blocking=False` lock 5 with context manager + with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): + async with lock_5: + pass + assert not lock_5.is_locked + assert lock_1.is_locked + + # release lock 1 + await lock_1.release() + assert not lock_1.is_locked + assert not lock_2.is_locked + assert not lock_3.is_locked + assert not lock_4.is_locked + assert not lock_5.is_locked + + +@pytest.mark.parametrize("lock_type", [AsyncFileLock, AsyncSoftFileLock]) +@pytest.mark.parametrize("thread_local", [True, False]) +@pytest.mark.asyncio() +async def test_non_executor(lock_type: type[BaseAsyncFileLock], thread_local: bool, tmp_path: Path) -> None: + lock_path = tmp_path / "a" + lock = lock_type(str(lock_path), thread_local=thread_local, run_in_executor=False) + async with lock as locked: + assert lock.is_locked + assert lock is locked + assert not lock.is_locked + + +@pytest.mark.asyncio() +async def test_coroutine_function(tmp_path: Path) -> None: + acquired = released = False + + class AioFileLock(BaseAsyncFileLock): + async def _acquire(self) -> None: # type: ignore[override] + nonlocal acquired + acquired = True + self._context.lock_file_fd = 1 + + async def _release(self) -> None: # type: ignore[override] + nonlocal released + released = True + self._context.lock_file_fd = None + + lock = AioFileLock(str(tmp_path / "a")) + await lock.acquire() + assert acquired + assert not released + await lock.release() + assert acquired + assert released diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/filelock-3.14.0/tests/test_filelock.py new/filelock-3.15.1/tests/test_filelock.py --- old/filelock-3.14.0/tests/test_filelock.py 2020-02-02 01:00:00.000000000 +0100 +++ new/filelock-3.15.1/tests/test_filelock.py 2020-02-02 01:00:00.000000000 +0100 @@ -201,6 +201,23 @@ assert not lock.is_locked +@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) +def test_nested_contruct(lock_type: type[BaseFileLock], tmp_path: Path) -> None: + # lock is re-entrant for a given file even if it is constructed multiple times + lock_path = tmp_path / "a" + + with lock_type(str(lock_path), is_singleton=True, timeout=2) as lock_1: + assert lock_1.is_locked + + with lock_type(str(lock_path), is_singleton=True, timeout=2) as lock_2: + assert lock_2 is lock_1 + assert lock_2.is_locked + + assert lock_1.is_locked + + assert not lock_1.is_locked + + _ExcInfoType = Union[Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None]] @@ -670,9 +687,10 @@ mode: int = 0o644, thread_local: bool = True, my_param: int = 0, - **kwargs: dict[str, Any], + **kwargs: dict[str, Any], # noqa: ARG002 ) -> None: - pass + super().__init__(lock_file, timeout, mode, thread_local, blocking=True, is_singleton=True) + self.my_param = my_param lock_path = tmp_path / "a" MyFileLock(str(lock_path), my_param=1) @@ -685,9 +703,10 @@ mode: int = 0o644, thread_local: bool = True, my_param: int = 0, - **kwargs: dict[str, Any], + **kwargs: dict[str, Any], # noqa: ARG002 ) -> None: - pass + super().__init__(lock_file, timeout, mode, thread_local, blocking=True, is_singleton=True) + self.my_param = my_param MySoftFileLock(str(lock_path), my_param=1) @@ -725,12 +744,19 @@ @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_singleton_locks_must_be_initialized_with_the_same_args(lock_type: type[BaseFileLock], tmp_path: Path) -> None: lock_path = tmp_path / "a" - lock = lock_type(str(lock_path), is_singleton=True) # noqa: F841 + args: dict[str, Any] = {"timeout": -1, "mode": 0o644, "thread_local": True, "blocking": True} + alternate_args: dict[str, Any] = {"timeout": 10, "mode": 0, "thread_local": False, "blocking": False} + + lock = lock_type(str(lock_path), is_singleton=True, **args) - with pytest.raises(ValueError, match="Singleton lock instances cannot be initialized with differing arguments"): - lock_type(str(lock_path), timeout=10, is_singleton=True) - with pytest.raises(ValueError, match="Singleton lock instances cannot be initialized with differing arguments"): - lock_type(str(lock_path), mode=0, is_singleton=True) + for arg_name in args: + general_msg = "Singleton lock instances cannot be initialized with differing arguments" + altered_args = args.copy() + altered_args[arg_name] = alternate_args[arg_name] + with pytest.raises(ValueError, match=general_msg) as exc_info: + lock_type(str(lock_path), is_singleton=True, **altered_args) + exc_info.match(arg_name) # ensure specific non-matching argument is included in exception text + del lock, exc_info @pytest.mark.skipif(hasattr(sys, "pypy_version_info"), reason="del() does not trigger GC in PyPy")