filelock-3.15.4/tox.ini0000644000000000000000000000554313615410400011636 0ustar00[tox] requires = tox>=4.2 env_list = fix py312 py311 py310 py39 py38 py37 type coverage docs readme skip_missing_interpreters = true [testenv] description = run tests with {basepython} package = wheel wheel_build_env = .pkg extras = testing pass_env = PYTEST_ADDOPTS set_env = COVERAGE_FILE = {toxworkdir}{/}.coverage.{envname} commands = pytest {tty:--color=yes} {posargs: \ --junitxml {toxworkdir}{/}junit.{envname}.xml --cov {envsitepackagesdir}{/}filelock --cov {toxinidir}{/}tests \ --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ --cov-report html:{envtmpdir}{/}htmlcov --cov-report xml:{toxworkdir}{/}coverage.{envname}.xml \ tests diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}{/}coverage.{envname}.xml} [testenv:fix] description = format the code base to adhere to our styles, and complain about what we cannot do automatically base_python = python3.10 skip_install = true deps = pre-commit>=3.5 commands = pre-commit run --all-files --show-diff-on-failure python -c 'import pathlib; print("hint: run \{\} install to add checks as pre-commit hook".format(pathlib.Path(r"{envdir}") / "bin" / "pre-commit"))' [testenv:type] description = run type check on code base deps = mypy==1.7.1 set_env = {tty:MYPY_FORCE_COLOR = 1} commands = mypy --strict src/filelock mypy --strict tests [testenv:coverage] description = combine coverage files and generate diff (against DIFF_AGAINST defaulting to origin/main) skip_install = true deps = covdefaults>=2.3 coverage[toml]>=7.3.2 diff-cover>=8.0.1 extras = parallel_show_output = true pass_env = DIFF_AGAINST set_env = COVERAGE_FILE = {toxworkdir}/.coverage commands = coverage combine coverage report --skip-covered --show-missing coverage xml -o {toxworkdir}/coverage.xml coverage html -d {toxworkdir}/htmlcov diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}/coverage.xml depends = py311 py310 py39 py38 py37 [testenv:docs] description = build documentation extras = docs commands = sphinx-build -d "{envtmpdir}{/}doctree" docs "{toxworkdir}{/}docs_out" --color -b html -W {posargs} python -c 'print(r"documentation available under file://{toxworkdir}{/}docs_out{/}index.html")' [testenv:readme] description = check that the long description is valid (need for PyPI) skip_install = true deps = build[virtualenv]>=1.0.3 twine>=4.0.2 extras = commands = pyproject-build -o {envtmpdir} --wheel --sdist . twine check {envtmpdir}/* [testenv:dev] description = generate a DEV environment package = editable extras = docs testing commands = python -m pip list --format=columns python -c 'import sys; print(sys.executable)' uv_seed = true filelock-3.15.4/src/filelock/__init__.py0000644000000000000000000000335113615410400015006 0ustar00""" A platform independent file lock that supports the with-statement. .. autodata:: filelock.__version__ :no-value: """ from __future__ import annotations import sys import warnings from typing import TYPE_CHECKING from ._api import AcquireReturnProxy, BaseFileLock from ._error import Timeout 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 __version__: str = version 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", "Timeout", "UnixFileLock", "WindowsFileLock", "__version__", ] filelock-3.15.4/src/filelock/_api.py0000644000000000000000000003431513615410400014163 0ustar00from __future__ import annotations import contextlib import inspect import logging import os import time import warnings from abc import ABCMeta, abstractmethod from dataclasses import dataclass from threading import local from typing import TYPE_CHECKING, Any, cast from weakref import WeakValueDictionary from ._error import Timeout if TYPE_CHECKING: import sys from types import TracebackType if sys.version_info >= (3, 11): # pragma: no cover (py311+) from typing import Self else: # pragma: no cover ( None: self.lock = lock def __enter__(self) -> BaseFileLock: return self.lock def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: 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 #: Whether the lock should be blocking or not blocking: bool #: The file descriptor for the *_lock_file* as it is returned by the os.open() function, not None when lock held lock_file_fd: int | None = None #: 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 FileLockMeta(ABCMeta): def __call__( # noqa: PLR0913 cls, lock_file: str | os.PathLike[str], timeout: float = -1, mode: int = 0o644, thread_local: bool = True, # noqa: FBT001, FBT002 *, blocking: bool = True, is_singleton: bool = False, **kwargs: Any, # capture remaining kwargs for subclasses # noqa: ANN401 ) -> BaseFileLock: if is_singleton: instance = cls._instances.get(str(lock_file)) # type: ignore[attr-defined] if instance: params_to_check = { "thread_local": (thread_local, instance.is_thread_local()), "timeout": (timeout, instance.timeout), "mode": (mode, instance.mode), "blocking": (blocking, instance.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 cast(BaseFileLock, instance) # 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) # Workaround to make `__init__`'s params optional in subclasses # E.g. virtualenv changes the signature of the `__init__` method in the `BaseFileLock` class descendant # (https://github.com/tox-dev/filelock/pull/340) all_params = { "timeout": timeout, "mode": mode, "thread_local": thread_local, "blocking": blocking, "is_singleton": is_singleton, **kwargs, } present_params = inspect.signature(cls.__init__).parameters # type: ignore[misc] init_params = {key: value for key, value in all_params.items() if key in present_params} instance = super().__call__(lock_file, **init_params) if is_singleton: cls._instances[str(lock_file)] = instance # type: ignore[attr-defined] return cast(BaseFileLock, instance) class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta): """Abstract base class for a file lock object.""" _instances: WeakValueDictionary[str, BaseFileLock] def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None: """Setup unique state for lock subclasses.""" super().__init_subclass__(**kwargs) cls._instances = WeakValueDictionary() def __init__( # noqa: PLR0913 self, lock_file: str | os.PathLike[str], timeout: float = -1, mode: int = 0o644, thread_local: bool = True, # noqa: FBT001, FBT002 *, blocking: bool = True, is_singleton: bool = False, ) -> None: """ Create a new lock object. :param lock_file: path to the file :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in \ the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it \ to a negative value. A timeout of 0 means that there is exactly one attempt to acquire the file lock. :param mode: file permissions for the lockfile :param thread_local: Whether this object's internal context should be thread local or not. If this is set to \ ``False`` then the lock will be reentrant across threads. :param blocking: whether the lock should be blocking or not :param is_singleton: If this is set to ``True`` then only one instance of this class will be created \ per lock file. This is useful if you want to use the lock object for reentrant locking without needing \ to pass the same object around. """ self._is_thread_local = thread_local self._is_singleton = is_singleton # Create the context. Note that external code should not work with the context directly and should instead use # properties of this class. kwargs: dict[str, Any] = { "lock_file": os.fspath(lock_file), "timeout": timeout, "mode": mode, "blocking": blocking, } self._context: FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext)(**kwargs) 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 is_singleton(self) -> bool: """:return: a flag indicating if this lock is singleton or not""" return self._is_singleton @property def lock_file(self) -> str: """:return: path to the lock file""" return self._context.lock_file @property def timeout(self) -> float: """ :return: the default timeout value, in seconds .. versionadded:: 2.0.0 """ return self._context.timeout @timeout.setter def timeout(self, value: float | str) -> None: """ Change the default timeout value. :param value: the new value, in seconds """ self._context.timeout = float(value) @property def blocking(self) -> bool: """:return: whether the locking is blocking or not""" return self._context.blocking @blocking.setter def blocking(self, value: bool) -> None: """ Change the default blocking value. :param value: the new value as bool """ self._context.blocking = value @property def mode(self) -> int: """:return: the file permissions for the lockfile""" return self._context.mode @abstractmethod def _acquire(self) -> None: """If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file.""" raise NotImplementedError @abstractmethod def _release(self) -> None: """Releases the lock and sets self._context.lock_file_fd to None.""" raise NotImplementedError @property def is_locked(self) -> bool: """ :return: A boolean indicating if the lock file is holding the lock currently. .. versionchanged:: 2.0.0 This was previously a method and is now a property. """ 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, timeout: float | None = None, poll_interval: float = 0.05, *, poll_intervall: float | None = None, blocking: bool | None = None, ) -> AcquireReturnProxy: """ Try to acquire the file lock. :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default :attr:`~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 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. :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() .. versionchanged:: 2.0.0 This method returns now a *proxy* object instead of *self*, so that it can be used in a with statement without side effects. """ # 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 if poll_intervall is not None: msg = "use poll_interval instead of poll_intervall" warnings.warn(msg, DeprecationWarning, stacklevel=2) poll_interval = poll_intervall # 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) 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) time.sleep(poll_interval) except BaseException: # Something did go wrong, so decrement the counter. self._context.lock_counter = max(0, self._context.lock_counter - 1) raise return AcquireReturnProxy(lock=self) def release(self, force: bool = False) -> None: # 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) self._release() self._context.lock_counter = 0 _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) def __enter__(self) -> Self: """ Acquire the lock. :return: the lock object """ self.acquire() return self def __exit__( 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 """ self.release() def __del__(self) -> None: """Called when the lock object is deleted.""" self.release(force=True) __all__ = [ "AcquireReturnProxy", "BaseFileLock", ] filelock-3.15.4/src/filelock/_error.py0000644000000000000000000000142313615410400014535 0ustar00from __future__ import annotations from typing import Any class Timeout(TimeoutError): # noqa: N818 """Raised when the lock could not be acquired in *timeout* seconds.""" def __init__(self, lock_file: str) -> None: 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." 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__ = [ "Timeout", ] filelock-3.15.4/src/filelock/_soft.py0000644000000000000000000000325713615410400014366 0ustar00from __future__ import annotations import os import sys from contextlib import suppress from errno import EACCES, EEXIST from pathlib import Path from ._api import BaseFileLock from ._util import ensure_directory_exists, raise_on_not_writable_file class SoftFileLock(BaseFileLock): """Simply watches the existence of the lock file.""" def _acquire(self) -> None: raise_on_not_writable_file(self.lock_file) ensure_directory_exists(self.lock_file) # first check for exists and read-only mode as the open will mask this case as EEXIST flags = ( os.O_WRONLY # open for writing only | 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: 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 else: self._context.lock_file_fd = file_handler def _release(self) -> None: assert self._context.lock_file_fd is not None # noqa: S101 os.close(self._context.lock_file_fd) # the lock file is definitely not None self._context.lock_file_fd = None with suppress(OSError): # the file is already deleted and that's what we want Path(self.lock_file).unlink() __all__ = [ "SoftFileLock", ] filelock-3.15.4/src/filelock/_unix.py0000644000000000000000000000432313615410400014371 0ustar00from __future__ import annotations import os import sys from contextlib import suppress from errno import ENOSYS from pathlib import Path from typing import cast from ._api import BaseFileLock from ._util import ensure_directory_exists #: a flag to indicate if the fcntl API is available has_fcntl = False if sys.platform == "win32": # pragma: win32 cover class UnixFileLock(BaseFileLock): """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" def _acquire(self) -> None: raise NotImplementedError def _release(self) -> None: raise NotImplementedError else: # pragma: win32 no cover try: import fcntl except ImportError: pass else: has_fcntl = True class UnixFileLock(BaseFileLock): """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" def _acquire(self) -> None: ensure_directory_exists(self.lock_file) open_flags = os.O_RDWR | os.O_TRUNC if not Path(self.lock_file).exists(): open_flags |= os.O_CREAT fd = os.open(self.lock_file, open_flags, self._context.mode) with suppress(PermissionError): # This locked is not owned by this UID os.fchmod(fd, self._context.mode) try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError as exception: os.close(fd) if exception.errno == ENOSYS: # NotImplemented error msg = "FileSystem does not appear to support flock; use SoftFileLock instead" raise NotImplementedError(msg) from exception else: 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._context.lock_file_fd) self._context.lock_file_fd = None fcntl.flock(fd, fcntl.LOCK_UN) os.close(fd) __all__ = [ "UnixFileLock", "has_fcntl", ] filelock-3.15.4/src/filelock/_util.py0000644000000000000000000000326313615410400014365 0ustar00from __future__ import annotations import os import stat import sys from errno import EACCES, EISDIR from pathlib import Path def raise_on_not_writable_file(filename: str) -> None: """ Raise an exception if attempting to open the file for writing would fail. This is done so files that will never be writable can be separated from files that are writable but currently locked. :param filename: file to check :raises OSError: as if the file was opened for writing. """ try: # use stat to do exists + can write to check without race condition file_stat = os.stat(filename) # noqa: PTH116 except OSError: return # swallow does not exist or other errors if file_stat.st_mtime != 0: # if os.stat returns but modification is zero that's an invalid os.stat - ignore it if not (file_stat.st_mode & stat.S_IWUSR): 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 # noqa: RET506 # On linux / macOS, this is IsADirectoryError raise IsADirectoryError(EISDIR, "Is a directory", filename) def ensure_directory_exists(filename: Path | str) -> None: """ Ensure the directory containing the file exists (create it if necessary). :param filename: file. """ Path(filename).parent.mkdir(parents=True, exist_ok=True) __all__ = [ "ensure_directory_exists", "raise_on_not_writable_file", ] filelock-3.15.4/src/filelock/_windows.py0000644000000000000000000000420113615410400015073 0ustar00from __future__ import annotations import os import sys from contextlib import suppress from errno import EACCES from pathlib import Path from typing import cast from ._api import BaseFileLock from ._util import ensure_directory_exists, 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.""" def _acquire(self) -> None: raise_on_not_writable_file(self.lock_file) ensure_directory_exists(self.lock_file) flags = ( os.O_RDWR # open for read and write | os.O_CREAT # create file if not exists | os.O_TRUNC # truncate file if not empty ) try: fd = os.open(self.lock_file, flags, self._context.mode) except OSError as exception: if exception.errno != EACCES: # has no access to this lock raise else: try: msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) except OSError as exception: os.close(fd) # close file first if exception.errno != EACCES: # file is already locked raise else: self._context.lock_file_fd = fd def _release(self) -> 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) with suppress(OSError): # Probably another instance of the application hat acquired the file lock. Path(self.lock_file).unlink() else: # pragma: win32 no cover class WindowsFileLock(BaseFileLock): """Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems.""" def _acquire(self) -> None: raise NotImplementedError def _release(self) -> None: raise NotImplementedError __all__ = [ "WindowsFileLock", ] filelock-3.15.4/src/filelock/asyncio.py0000644000000000000000000003024113615410400014712 0ustar00"""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, cast from ._api import BaseFileLock, FileLockContext, FileLockMeta 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 ( 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 AsyncFileLockMeta(FileLockMeta): def __call__( # type: ignore[override] # noqa: PLR0913 cls, # noqa: N805 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, ) -> BaseAsyncFileLock: if thread_local and run_in_executor: msg = "run_in_executor is not supported when thread_local is True" raise ValueError(msg) instance = super().__call__( lock_file=lock_file, timeout=timeout, mode=mode, thread_local=thread_local, blocking=blocking, is_singleton=is_singleton, loop=loop, run_in_executor=run_in_executor, executor=executor, ) return cast(BaseAsyncFileLock, instance) class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta): """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 # 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", ] filelock-3.15.4/src/filelock/py.typed0000644000000000000000000000000013615410400014360 0ustar00filelock-3.15.4/src/filelock/version.py0000644000000000000000000000063513615410400014736 0ustar00# file generated by setuptools_scm # don't change, don't track in version control TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple, Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = '3.15.4' __version_tuple__ = version_tuple = (3, 15, 4) filelock-3.15.4/tests/test_async_filelock.py0000644000000000000000000001476313615410400016067 0ustar00from __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 filelock-3.15.4/tests/test_error.py0000644000000000000000000000153713615410400014226 0ustar00from __future__ import annotations import pickle # noqa: S403 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)) # noqa: S301 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 filelock-3.15.4/tests/test_filelock.py0000644000000000000000000006712413615410400014671 0ustar00from __future__ import annotations 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, filemode from types import TracebackType from typing import TYPE_CHECKING, Any, Callable, Iterator, Tuple, Type, Union from uuid import uuid4 from weakref import WeakValueDictionary import pytest from filelock import BaseFileLock, FileLock, SoftFileLock, Timeout, UnixFileLock, WindowsFileLock if TYPE_CHECKING: from pytest_mock import MockerFixture @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) @pytest.mark.parametrize("path_type", [str, PurePath, Path]) @pytest.mark.parametrize("filename", ["a", "new/b", "new2/new3/c"]) def test_simple( lock_type: type[BaseFileLock], 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)) 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 @contextmanager def make_ro(path: Path) -> Iterator[None]: write = S_IWUSR | S_IWGRP | S_IWOTH path.chmod(path.stat().st_mode & ~write) yield path.chmod(path.stat().st_mode | write) @pytest.fixture() def tmp_path_ro(tmp_path: Path) -> Iterator[Path]: with make_ro(tmp_path): yield tmp_path @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, 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"): lock.acquire() @pytest.fixture() def tmp_file_ro(tmp_path: Path) -> Iterator[Path]: filename = tmp_path / "a" filename.write_text("") with make_ro(filename): yield filename @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) @pytest.mark.skipif( sys.platform != "win32" and os.geteuid() == 0, 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]) @pytest.mark.parametrize( ("expected_error", "match", "bad_lock_file"), [ pytest.param(FileNotFoundError, "No such file or directory:", "", id="blank_filename"), pytest.param(ValueError, "embedded null (byte|character)", "\0", id="null_byte"), # Should be PermissionError on Windows ( pytest.param(PermissionError, "Permission denied:", ".", id="current_directory") if sys.platform == "win32" # Should be IsADirectoryError on MacOS and Linux else ( pytest.param(IsADirectoryError, "Is a directory", ".", id="current_directory") if sys.platform in {"darwin", "linux"} # Should be some type of OSError at least on other operating systems else pytest.param(OSError, None, ".", 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(expected_error, match=match): lock.acquire() @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_nested_context_manager(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # lock is not released before the most outer with statement that locked the lock, is left lock_path = tmp_path / "a" lock = lock_type(str(lock_path)) with lock as lock_1: assert lock.is_locked assert lock is lock_1 with lock as lock_2: assert lock.is_locked assert lock is lock_2 with lock as lock_3: assert lock.is_locked assert lock is lock_3 assert lock.is_locked assert lock.is_locked assert not lock.is_locked @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_nested_acquire(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # lock is not released before the most outer with statement that locked the lock, is left lock_path = tmp_path / "a" lock = lock_type(str(lock_path)) with lock.acquire() as lock_1: assert lock.is_locked assert lock is lock_1 with lock.acquire() as lock_2: assert lock.is_locked assert lock is lock_2 with lock.acquire() as lock_3: assert lock.is_locked assert lock is lock_3 assert lock.is_locked assert lock.is_locked assert not lock.is_locked @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_nested_forced_release(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # acquires the lock using a with-statement and releases the lock before leaving the with-statement lock_path = tmp_path / "a" lock = lock_type(str(lock_path)) with lock: assert lock.is_locked lock.acquire() assert lock.is_locked lock.release(force=True) assert not lock.is_locked 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]] class ExThread(threading.Thread): def __init__(self, target: Callable[[], None], name: str) -> None: super().__init__(target=target, name=name) self.ex: _ExcInfoType | None = None def run(self) -> None: try: super().run() except Exception: # noqa: BLE001 # pragma: no cover self.ex = sys.exc_info() # pragma: no cover def join(self, timeout: float | None = None) -> None: super().join(timeout=timeout) if self.ex is not None: raise RuntimeError from self.ex[1] # pragma: no cover @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_threaded_shared_lock_obj(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # Runs 100 threads, which need the filelock. The lock must be acquired if at least one thread required it and # released, as soon as all threads stopped. lock_path = tmp_path / "a" lock = lock_type(str(lock_path)) def thread_work() -> None: for _ in range(100): with lock: assert lock.is_locked threads = [ExThread(target=thread_work, name=f"t{i}") for i in range(100)] for thread in threads: thread.start() for thread in threads: thread.join() assert not lock.is_locked @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) @pytest.mark.skipif(hasattr(sys, "pypy_version_info") and sys.platform == "win32", reason="deadlocks randomly") def test_threaded_lock_different_lock_obj(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # Runs multiple threads, which acquire the same lock file with a different FileLock object. When thread group 1 # acquired the lock, thread group 2 must not hold their lock. def t_1() -> None: for _ in range(1000): with lock_1: assert lock_1.is_locked assert not lock_2.is_locked def t_2() -> None: for _ in range(1000): with lock_2: assert not lock_1.is_locked assert lock_2.is_locked lock_path = tmp_path / "a" lock_1, lock_2 = lock_type(str(lock_path)), lock_type(str(lock_path)) threads = [(ExThread(t_1, f"t1_{i}"), ExThread(t_2, f"t2_{i}")) for i in range(10)] for thread_1, thread_2 in threads: thread_1.start() thread_2.start() for thread_1, thread_2 in threads: thread_1.join() thread_2.join() assert not lock_1.is_locked assert not lock_2.is_locked @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_timeout(lock_type: type[BaseFileLock], 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)) # acquire lock 1 lock_1.acquire() assert lock_1.is_locked assert not lock_2.is_locked # try to acquire lock 2 with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): lock_2.acquire(timeout=0.1) assert not lock_2.is_locked assert lock_1.is_locked # release lock 1 lock_1.release() assert not lock_1.is_locked assert not lock_2.is_locked @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_non_blocking(lock_type: type[BaseFileLock], 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 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."): 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."): 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."), 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."): 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."), 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."): 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."), lock_5: pass assert not lock_5.is_locked assert lock_1.is_locked # release lock 1 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", [FileLock, SoftFileLock]) def test_default_timeout(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # test if the default timeout parameter works lock_path = tmp_path / "a" lock_1, lock_2 = lock_type(str(lock_path)), lock_type(str(lock_path), timeout=0.1) assert lock_2.timeout == 0.1 # acquire lock 1 lock_1.acquire() assert lock_1.is_locked assert not lock_2.is_locked # try to acquire lock 2 with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): lock_2.acquire() assert not lock_2.is_locked assert lock_1.is_locked lock_2.timeout = 0 assert lock_2.timeout == 0 with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): lock_2.acquire() assert not lock_2.is_locked assert lock_1.is_locked # release lock 1 lock_1.release() assert not lock_1.is_locked assert not lock_2.is_locked @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_context_release_on_exc(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # lock is released when an exception is thrown in a with-statement lock_path = tmp_path / "a" lock = lock_type(str(lock_path)) try: with lock as lock_1: assert lock is lock_1 assert lock.is_locked raise ValueError # noqa: TRY301 except ValueError: assert not lock.is_locked @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_acquire_release_on_exc(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # lock is released when an exception is thrown in a acquire statement lock_path = tmp_path / "a" lock = lock_type(str(lock_path)) try: with lock.acquire() as lock_1: assert lock is lock_1 assert lock.is_locked raise ValueError # noqa: TRY301 except ValueError: assert not lock.is_locked @pytest.mark.skipif(hasattr(sys, "pypy_version_info"), reason="del() does not trigger GC in PyPy") @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_del(lock_type: type[BaseFileLock], tmp_path: Path) -> None: # lock is released when the object is deleted lock_path = tmp_path / "a" lock_1, lock_2 = lock_type(str(lock_path)), lock_type(str(lock_path)) # acquire lock 1 lock_1.acquire() assert lock_1.is_locked assert not lock_2.is_locked # try to acquire lock 2 with pytest.raises(Timeout, match="The file lock '.*' could not be acquired."): lock_2.acquire(timeout=0.1) # delete lock 1 and try to acquire lock 2 again del lock_1 lock_2.acquire() assert lock_2.is_locked lock_2.release() def test_cleanup_soft_lock(tmp_path: Path) -> None: # tests if the lock file is removed after use lock_path = tmp_path / "a" with SoftFileLock(lock_path): assert lock_path.exists() assert not lock_path.exists() @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_poll_intervall_deprecated(lock_type: type[BaseFileLock], tmp_path: Path) -> None: lock_path = tmp_path / "a" lock = lock_type(str(lock_path)) 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 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 == 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.") @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_context_decorator(lock_type: type[BaseFileLock], tmp_path: Path) -> None: lock_path = tmp_path / "a" lock = lock_type(str(lock_path)) @lock def decorated_method() -> None: assert lock.is_locked assert not lock.is_locked decorated_method() 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(lock_path.stat().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(lock_path.stat().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(tmp_path / "lockfile") with pytest.raises(NotImplementedError): lock.acquire() with pytest.raises(NotImplementedError): lock._release() # noqa: SLF001 @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), 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) with ThreadPoolExecutor() as executor: results = [executor.submit(mess_with_file, lock) for _ in range(100)] 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) with ThreadPoolExecutor() as executor: results = [executor.submit(mess_with_file) for _ in range(100)] 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" with ThreadPoolExecutor() as executor: results = [executor.submit(mess_with_file) for _ in range(100)] 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) def test_subclass_compatibility(tmp_path: Path) -> None: class MyFileLock(FileLock): def __init__( # noqa: PLR0913 Too many arguments to function call (6 > 5) self, lock_file: str | os.PathLike[str], timeout: float = -1, mode: int = 0o644, thread_local: bool = True, my_param: int = 0, **kwargs: dict[str, Any], # noqa: ARG002 ) -> None: 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) class MySoftFileLock(SoftFileLock): def __init__( # noqa: PLR0913 Too many arguments to function call (6 > 5) self, lock_file: str | os.PathLike[str], timeout: float = -1, mode: int = 0o644, thread_local: bool = True, my_param: int = 0, **kwargs: dict[str, Any], # noqa: ARG002 ) -> None: 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) @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_singleton_and_non_singleton_locks_are_distinct(lock_type: type[BaseFileLock], tmp_path: Path) -> None: lock_path = tmp_path / "a" lock_1 = lock_type(str(lock_path), is_singleton=False) assert lock_1.is_singleton is False lock_2 = lock_type(str(lock_path), is_singleton=True) assert lock_2.is_singleton is True assert lock_2 is not lock_1 @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_singleton_locks_are_the_same(lock_type: type[BaseFileLock], tmp_path: Path) -> None: lock_path = tmp_path / "a" lock_1 = lock_type(str(lock_path), is_singleton=True) lock_2 = lock_type(str(lock_path), is_singleton=True) assert lock_2 is lock_1 @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_singleton_locks_are_distinct_per_lock_file(lock_type: type[BaseFileLock], tmp_path: Path) -> None: lock_path_1 = tmp_path / "a" lock_1 = lock_type(str(lock_path_1), is_singleton=True) lock_path_2 = tmp_path / "b" lock_2 = lock_type(str(lock_path_2), is_singleton=True) assert lock_1 is not lock_2 @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" 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) 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") @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_singleton_locks_are_deleted_when_no_external_references_exist( lock_type: type[BaseFileLock], tmp_path: Path, ) -> None: lock_path = tmp_path / "a" lock = lock_type(str(lock_path), is_singleton=True) assert lock_type._instances == {str(lock_path): lock} # noqa: SLF001 del lock assert lock_type._instances == {} # noqa: SLF001 @pytest.mark.skipif(hasattr(sys, "pypy_version_info"), reason="del() does not trigger GC in PyPy") @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) def test_singleton_instance_tracking_is_unique_per_subclass(lock_type: type[BaseFileLock]) -> None: class Lock1(lock_type): # type: ignore[valid-type, misc] pass class Lock2(lock_type): # type: ignore[valid-type, misc] pass assert isinstance(Lock1._instances, WeakValueDictionary) # noqa: SLF001 assert isinstance(Lock2._instances, WeakValueDictionary) # noqa: SLF001 assert Lock1._instances is not Lock2._instances # noqa: SLF001 def test_singleton_locks_when_inheriting_init_is_called_once(tmp_path: Path) -> None: init_calls = 0 class MyFileLock(FileLock): def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 super().__init__(*args, **kwargs) nonlocal init_calls init_calls += 1 lock_path = tmp_path / "a" lock1 = MyFileLock(str(lock_path), is_singleton=True) lock2 = MyFileLock(str(lock_path), is_singleton=True) assert lock1 is lock2 assert init_calls == 1 def test_file_lock_positional_argument(tmp_path: Path) -> None: class FilePathLock(FileLock): def __init__(self, file_path: str) -> None: super().__init__(file_path + ".lock") lock_path = tmp_path / "a" lock = FilePathLock(str(lock_path)) assert lock.lock_file == str(lock_path) + ".lock" filelock-3.15.4/tests/test_virtualenv.py0000644000000000000000000000047013615410400015267 0ustar00from __future__ import annotations from typing import TYPE_CHECKING from virtualenv import cli_run # type: ignore[import-untyped] if TYPE_CHECKING: from pathlib import Path def test_virtualenv(tmp_path: Path) -> None: cli_run([str(tmp_path), "--no-pip", "--no-setuptools", "--no-periodic-update"]) filelock-3.15.4/.gitignore0000644000000000000000000000027113615410400012304 0ustar00*.egg-info build dist *.egg .eggs *.py[codz] *$py.class .tox .*_cache .DS_Store .idea .vscode /pip-wheel-metadata /src/filelock/version.py venv* .python-version test.lock test.softlock filelock-3.15.4/LICENSE0000644000000000000000000000227213615410400011324 0ustar00This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to filelock-3.15.4/README.md0000644000000000000000000000153213615410400011574 0ustar00# filelock [![PyPI](https://img.shields.io/pypi/v/filelock)](https://pypi.org/project/filelock/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/filelock.svg)](https://pypi.org/project/filelock/) [![Documentation status](https://readthedocs.org/projects/py-filelock/badge/?version=latest)](https://py-filelock.readthedocs.io/en/latest/?badge=latest) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Downloads](https://static.pepy.tech/badge/filelock/month)](https://pepy.tech/project/filelock) [![check](https://github.com/tox-dev/py-filelock/actions/workflows/check.yml/badge.svg)](https://github.com/tox-dev/py-filelock/actions/workflows/check.yml) For more information checkout the [official documentation](https://py-filelock.readthedocs.io/en/latest/index.html). filelock-3.15.4/pyproject.toml0000644000000000000000000000746413615410400013243 0ustar00[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.4", "hatchling>=1.18", ] [project] name = "filelock" description = "A platform independent file lock." readme = "README.md" keywords = [ "application", "cache", "directory", "log", "user", ] license = "Unlicense" maintainers = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] requires-python = ">=3.8" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: The Unlicense (Unlicense)", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: System", ] dynamic = [ "version", ] optional-dependencies.docs = [ "furo>=2023.9.10", "sphinx>=7.2.6", "sphinx-autodoc-typehints!=1.23.4,>=1.25.2", ] optional-dependencies.testing = [ "covdefaults>=2.3", "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", "virtualenv>=20.26.2", ] optional-dependencies.typing = [ "typing-extensions>=4.8; python_version<'3.11'", ] urls.Documentation = "https://py-filelock.readthedocs.io" urls.Homepage = "https://github.com/tox-dev/py-filelock" urls.Source = "https://github.com/tox-dev/py-filelock" urls.Tracker = "https://github.com/tox-dev/py-filelock/issues" [tool.hatch] build.hooks.vcs.version-file = "src/filelock/version.py" build.targets.sdist.include = [ "/src", "/tests", "/tox.ini", ] version.source = "vcs" [tool.ruff] target-version = "py38" 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 "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 "D301", # Use `r"""` if any backslashes in a docstring "D401", # First line of docstring should be in imperative mood "ISC001", # Conflict with formatter "S104", # Possible binding to all interface ] 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 "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" count = true quiet-level = 3 ignore-words-list = "master" [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", ] report.fail_under = 76 run.parallel = true run.plugins = [ "covdefaults", ] [tool.mypy] python_version = "3.11" show_error_codes = true strict = true overrides = [ { module = [ "appdirs.*", "jnius.*", ], ignore_missing_imports = true }, ] filelock-3.15.4/PKG-INFO0000644000000000000000000000552713615410400011422 0ustar00Metadata-Version: 2.3 Name: filelock Version: 3.15.4 Summary: A platform independent file lock. Project-URL: Documentation, https://py-filelock.readthedocs.io Project-URL: Homepage, https://github.com/tox-dev/py-filelock Project-URL: Source, https://github.com/tox-dev/py-filelock Project-URL: Tracker, https://github.com/tox-dev/py-filelock/issues Maintainer-email: Bernát Gábor License-Expression: Unlicense License-File: LICENSE Keywords: application,cache,directory,log,user Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: The Unlicense (Unlicense) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Internet Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: System Requires-Python: >=3.8 Provides-Extra: docs Requires-Dist: furo>=2023.9.10; extra == 'docs' Requires-Dist: sphinx-autodoc-typehints!=1.23.4,>=1.25.2; extra == 'docs' Requires-Dist: sphinx>=7.2.6; extra == 'docs' Provides-Extra: testing 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' Requires-Dist: pytest>=7.4.3; extra == 'testing' Requires-Dist: virtualenv>=20.26.2; extra == 'testing' Provides-Extra: typing Requires-Dist: typing-extensions>=4.8; (python_version < '3.11') and extra == 'typing' Description-Content-Type: text/markdown # filelock [![PyPI](https://img.shields.io/pypi/v/filelock)](https://pypi.org/project/filelock/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/filelock.svg)](https://pypi.org/project/filelock/) [![Documentation status](https://readthedocs.org/projects/py-filelock/badge/?version=latest)](https://py-filelock.readthedocs.io/en/latest/?badge=latest) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Downloads](https://static.pepy.tech/badge/filelock/month)](https://pepy.tech/project/filelock) [![check](https://github.com/tox-dev/py-filelock/actions/workflows/check.yml/badge.svg)](https://github.com/tox-dev/py-filelock/actions/workflows/check.yml) For more information checkout the [official documentation](https://py-filelock.readthedocs.io/en/latest/index.html).