Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/asyncio.py: 41%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""An asyncio-based implementation of the file lock."""
3from __future__ import annotations
5import asyncio
6import contextlib
7import logging
8import os
9import time
10from dataclasses import dataclass
11from inspect import iscoroutinefunction
12from threading import local
13from typing import TYPE_CHECKING, Any, NoReturn, TypeVar
15from ._api import _UNSET_FILE_MODE, BaseFileLock, FileLockContext, FileLockMeta, _canonical
16from ._error import Timeout
17from ._soft import SoftFileLock
18from ._unix import UnixFileLock
19from ._windows import WindowsFileLock
21if TYPE_CHECKING:
22 import sys
23 from collections.abc import Callable
24 from concurrent import futures
25 from types import TracebackType
27 if sys.version_info >= (3, 11): # pragma: no cover (py311+)
28 from typing import Self
29 else: # pragma: no cover (<py311)
30 from typing_extensions import Self
33_LOGGER = logging.getLogger("filelock")
36@dataclass
37class AsyncFileLockContext(FileLockContext):
38 """A dataclass which holds the context for a ``BaseAsyncFileLock`` object."""
40 #: Whether run in executor
41 run_in_executor: bool = True
43 #: The executor
44 executor: futures.Executor | None = None
46 #: The loop
47 loop: asyncio.AbstractEventLoop | None = None
50class AsyncThreadLocalFileContext(AsyncFileLockContext, local):
51 """A thread local version of the ``FileLockContext`` class."""
54class AsyncAcquireReturnProxy:
55 """A context-aware object that will release the lock file when exiting."""
57 def __init__(self, lock: BaseAsyncFileLock) -> None: # noqa: D107
58 self.lock = lock
60 async def __aenter__(self) -> BaseAsyncFileLock: # noqa: D105
61 return self.lock
63 async def __aexit__( # noqa: D105
64 self,
65 exc_type: type[BaseException] | None,
66 exc_value: BaseException | None,
67 traceback: TracebackType | None,
68 ) -> None:
69 await self.lock.release()
72_AT = TypeVar("_AT", bound="BaseAsyncFileLock")
75class AsyncFileLockMeta(FileLockMeta):
76 def __call__( # ty: ignore[invalid-method-override] # noqa: PLR0913
77 cls: type[_AT], # noqa: N805
78 lock_file: str | os.PathLike[str],
79 timeout: float = -1,
80 mode: int = _UNSET_FILE_MODE,
81 thread_local: bool = False, # noqa: FBT001, FBT002
82 *,
83 blocking: bool = True,
84 is_singleton: bool = False,
85 poll_interval: float = 0.05,
86 lifetime: float | None = None,
87 loop: asyncio.AbstractEventLoop | None = None,
88 run_in_executor: bool = True,
89 executor: futures.Executor | None = None,
90 ) -> _AT:
91 if thread_local and run_in_executor:
92 msg = "run_in_executor is not supported when thread_local is True"
93 raise ValueError(msg)
94 return super().__call__(
95 lock_file=lock_file,
96 timeout=timeout,
97 mode=mode,
98 thread_local=thread_local,
99 blocking=blocking,
100 is_singleton=is_singleton,
101 poll_interval=poll_interval,
102 lifetime=lifetime,
103 loop=loop,
104 run_in_executor=run_in_executor,
105 executor=executor,
106 )
109class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta):
110 """
111 Base class for asynchronous file locks.
113 .. versionadded:: 3.15.0
115 """
117 _deadlock_holder_desc: str = "BaseAsyncFileLock instance in this task"
119 def __init__( # noqa: PLR0913
120 self,
121 lock_file: str | os.PathLike[str],
122 timeout: float = -1,
123 mode: int = _UNSET_FILE_MODE,
124 thread_local: bool = False, # noqa: FBT001, FBT002
125 *,
126 blocking: bool = True,
127 is_singleton: bool = False,
128 poll_interval: float = 0.05,
129 lifetime: float | None = None,
130 loop: asyncio.AbstractEventLoop | None = None,
131 run_in_executor: bool = True,
132 executor: futures.Executor | None = None,
133 ) -> None:
134 """
135 Create a new lock object.
137 :param lock_file: path to the file
138 :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in the
139 acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it to a
140 negative value. A timeout of 0 means that there is exactly one attempt to acquire the file lock.
141 :param mode: file permissions for the lockfile. When not specified, the OS controls permissions via umask and
142 default ACLs, preserving POSIX default ACL inheritance in shared directories.
143 :param thread_local: Whether this object's internal context should be thread local or not. If this is set to
144 ``False`` then the lock will be reentrant across threads. When ``True`` (the default), **all fields of the
145 lock's internal context are per-thread**, including the configuration values ``poll_interval``, ``timeout``,
146 ``blocking``, ``mode``, and ``lifetime``. Setting one of these properties from one thread does not change
147 the value seen by another thread; threads that did not perform the write continue to see the value supplied
148 at construction time. If you need configuration values to be visible across threads, construct the lock
149 with ``thread_local=False``.
150 :param blocking: whether the lock should be blocking or not
151 :param is_singleton: If this is set to ``True`` then only one instance of this class will be created per lock
152 file. This is useful if you want to use the lock object for reentrant locking without needing to pass the
153 same object around.
154 :param poll_interval: default interval for polling the lock file, in seconds. It will be used as fallback value
155 in the acquire method, if no poll_interval value (``None``) is given.
156 :param lifetime: maximum time in seconds a lock can be held before it is considered expired. When set, a waiting
157 process will break a lock whose file modification time is older than ``lifetime`` seconds. ``None`` (the
158 default) means locks never expire.
159 :param loop: The event loop to use. If not specified, the running event loop will be used.
160 :param run_in_executor: If this is set to ``True`` then the lock will be acquired in an executor.
161 :param executor: The executor to use. If not specified, the default executor will be used.
163 """
164 self._is_thread_local = thread_local
165 self._is_singleton = is_singleton
167 # Create the context. Note that external code should not work with the context directly and should instead use
168 # properties of this class.
169 kwargs: dict[str, Any] = {
170 "lock_file": os.fspath(lock_file),
171 "timeout": timeout,
172 "mode": mode,
173 "blocking": blocking,
174 "poll_interval": poll_interval,
175 "lifetime": lifetime,
176 "loop": loop,
177 "run_in_executor": run_in_executor,
178 "executor": executor,
179 }
180 self._context: AsyncFileLockContext = (AsyncThreadLocalFileContext if thread_local else AsyncFileLockContext)(
181 **kwargs
182 )
184 @property
185 def run_in_executor(self) -> bool:
186 """Whether run in executor."""
187 return self._context.run_in_executor
189 @property
190 def executor(self) -> futures.Executor | None:
191 """The executor."""
192 return self._context.executor
194 @executor.setter
195 def executor(self, value: futures.Executor | None) -> None: # pragma: no cover
196 """
197 Change the executor.
199 :param futures.Executor | None value: the new executor or ``None``
201 """
202 self._context.executor = value
204 @property
205 def loop(self) -> asyncio.AbstractEventLoop | None:
206 """The event loop."""
207 return self._context.loop
209 async def acquire( # ty: ignore[invalid-method-override]
210 self,
211 timeout: float | None = None,
212 poll_interval: float | None = None,
213 *,
214 blocking: bool | None = None,
215 cancel_check: Callable[[], bool] | None = None,
216 ) -> AsyncAcquireReturnProxy:
217 """
218 Try to acquire the file lock.
220 :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default
221 :attr:`~BaseFileLock.timeout` is and if ``timeout < 0``, there is no timeout and this method will block
222 until the lock could be acquired
223 :param poll_interval: interval of trying to acquire the lock file, ``None`` means use the default
224 :attr:`~BaseFileLock.poll_interval`
225 :param blocking: defaults to True. If False, function will return immediately if it cannot obtain a lock on the
226 first attempt. Otherwise, this method will block until the timeout expires or the lock is acquired.
227 :param cancel_check: a callable returning ``True`` when the acquisition should be canceled. Checked on each poll
228 iteration. When triggered, raises :class:`~Timeout` just like an expired timeout.
230 :returns: a context object that will unlock the file when the context is exited
232 :raises Timeout: if fails to acquire lock within the timeout period
234 .. code-block:: python
236 # You can use this method in the context manager (recommended)
237 with lock.acquire():
238 pass
240 # Or use an equivalent try-finally construct:
241 lock.acquire()
242 try:
243 pass
244 finally:
245 lock.release()
247 """
248 # Use the default timeout, if no timeout is provided.
249 if timeout is None:
250 timeout = self._context.timeout
252 if blocking is None:
253 blocking = self._context.blocking
255 if poll_interval is None:
256 poll_interval = self._context.poll_interval
258 # Increment the number right at the beginning. We can still undo it, if something fails.
259 self._context.lock_counter += 1
261 canonical = _canonical(self.lock_file)
262 self._raise_if_would_deadlock(canonical, timeout=timeout, blocking=blocking)
264 start_time = time.perf_counter()
265 try:
266 await self._async_poll_until_acquired(
267 blocking=blocking,
268 cancel_check=cancel_check,
269 timeout=timeout,
270 poll_interval=poll_interval,
271 start_time=start_time,
272 )
273 except BaseException: # Something did go wrong, so decrement the counter.
274 self._undo_acquire(canonical)
275 raise
276 self._commit_acquire(canonical)
277 return AsyncAcquireReturnProxy(lock=self)
279 async def _async_poll_until_acquired(
280 self,
281 *,
282 blocking: bool,
283 cancel_check: Callable[[], bool] | None,
284 timeout: float,
285 poll_interval: float,
286 start_time: float,
287 ) -> None:
288 lock_id = id(self)
289 lock_filename = self.lock_file
290 while True:
291 if not self.is_locked:
292 self._try_break_expired_lock()
293 _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
294 await self._run_internal_method(self._acquire)
295 if self.is_locked:
296 _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename)
297 return
298 if self._check_give_up(
299 lock_id,
300 lock_filename,
301 blocking=blocking,
302 cancel_check=cancel_check,
303 timeout=timeout,
304 start_time=start_time,
305 ):
306 raise Timeout(lock_filename)
307 msg = "Lock %s not acquired on %s, waiting %s seconds ..."
308 _LOGGER.debug(msg, lock_id, lock_filename, poll_interval)
309 await asyncio.sleep(poll_interval)
311 async def release(self, force: bool = False) -> None: # ty: ignore[invalid-method-override] # noqa: FBT001, FBT002
312 """
313 Release the file lock. The lock is only completely released when the lock counter reaches 0. The lock file
314 itself may be deleted automatically, the behavior is platform-specific.
316 :param force: If true, the lock counter is ignored and the lock is released in every case.
318 """
319 if self.is_locked:
320 self._context.lock_counter -= 1
322 if self._context.lock_counter == 0 or force:
323 lock_id, lock_filename = id(self), self.lock_file
325 _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename)
326 await self._run_internal_method(self._release)
327 self._context.lock_counter = 0
328 self._drop_registry_entry()
329 _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)
331 async def _run_internal_method(self, method: Callable[[], Any]) -> None:
332 if iscoroutinefunction(method):
333 await method()
334 elif self.run_in_executor:
335 await asyncio.get_running_loop().run_in_executor(self.executor, method)
336 else:
337 method()
339 def __enter__(self) -> NoReturn:
340 """Sync context manager entry is not supported because lock acquisition is a coroutine."""
341 msg = "Use `async with` — acquire/release are coroutines and cannot be awaited in a sync context manager."
342 raise NotImplementedError(msg)
344 def __exit__(
345 self,
346 exc_type: type[BaseException] | None,
347 exc_value: BaseException | None,
348 traceback: object,
349 ) -> None:
350 """Sync context manager exit is not supported because lock release is a coroutine."""
351 msg = "Use `async with` — acquire/release are coroutines and cannot be awaited in a sync context manager."
352 raise NotImplementedError(msg)
354 async def __aenter__(self) -> Self:
355 """
356 Acquire the lock.
358 :returns: the lock object
360 """
361 await self.acquire()
362 return self
364 async def __aexit__(
365 self,
366 exc_type: type[BaseException] | None,
367 exc_value: BaseException | None,
368 traceback: TracebackType | None,
369 ) -> None:
370 """
371 Release the lock.
373 :param exc_type: the exception type if raised
374 :param exc_value: the exception value if raised
375 :param traceback: the exception traceback if raised
377 """
378 await self.release()
380 def __del__(self) -> None:
381 """Release on deletion — safe to call during GC even when no event loop is running."""
382 with contextlib.suppress(Exception):
383 try:
384 loop = asyncio.get_running_loop()
385 except RuntimeError:
386 # No running loop — try stored loop or create one
387 loop = self._context.loop if self._context.loop and not self._context.loop.is_closed() else None
388 if loop is None:
389 return
390 if not loop.is_running(): # pragma: no cover
391 loop.run_until_complete(self.release(force=True))
392 else:
393 loop.create_task(self.release(force=True))
396class AsyncSoftFileLock(SoftFileLock, BaseAsyncFileLock):
397 """Simply watches the existence of the lock file."""
400class AsyncUnixFileLock(UnixFileLock, BaseAsyncFileLock):
401 """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
404class AsyncWindowsFileLock(WindowsFileLock, BaseAsyncFileLock):
405 """Uses the :func:`msvcrt.locking` to hard lock the lock file on windows systems."""
408__all__ = [
409 "AsyncAcquireReturnProxy",
410 "AsyncSoftFileLock",
411 "AsyncUnixFileLock",
412 "AsyncWindowsFileLock",
413 "BaseAsyncFileLock",
414]