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

133 statements  

1"""An asyncio-based implementation of the file lock.""" 

2 

3from __future__ import annotations 

4 

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 

14 

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 

20 

21if TYPE_CHECKING: 

22 import sys 

23 from collections.abc import Callable 

24 from concurrent import futures 

25 from types import TracebackType 

26 

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 

31 

32 

33_LOGGER = logging.getLogger("filelock") 

34 

35 

36@dataclass 

37class AsyncFileLockContext(FileLockContext): 

38 """A dataclass which holds the context for a ``BaseAsyncFileLock`` object.""" 

39 

40 #: Whether run in executor 

41 run_in_executor: bool = True 

42 

43 #: The executor 

44 executor: futures.Executor | None = None 

45 

46 #: The loop 

47 loop: asyncio.AbstractEventLoop | None = None 

48 

49 

50class AsyncThreadLocalFileContext(AsyncFileLockContext, local): 

51 """A thread local version of the ``FileLockContext`` class.""" 

52 

53 

54class AsyncAcquireReturnProxy: 

55 """A context-aware object that will release the lock file when exiting.""" 

56 

57 def __init__(self, lock: BaseAsyncFileLock) -> None: # noqa: D107 

58 self.lock = lock 

59 

60 async def __aenter__(self) -> BaseAsyncFileLock: # noqa: D105 

61 return self.lock 

62 

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() 

70 

71 

72_AT = TypeVar("_AT", bound="BaseAsyncFileLock") 

73 

74 

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 ) 

107 

108 

109class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta): 

110 """ 

111 Base class for asynchronous file locks. 

112 

113 .. versionadded:: 3.15.0 

114 

115 """ 

116 

117 _deadlock_holder_desc: str = "BaseAsyncFileLock instance in this task" 

118 

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. 

136 

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. 

162 

163 """ 

164 self._is_thread_local = thread_local 

165 self._is_singleton = is_singleton 

166 

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 ) 

183 

184 @property 

185 def run_in_executor(self) -> bool: 

186 """Whether run in executor.""" 

187 return self._context.run_in_executor 

188 

189 @property 

190 def executor(self) -> futures.Executor | None: 

191 """The executor.""" 

192 return self._context.executor 

193 

194 @executor.setter 

195 def executor(self, value: futures.Executor | None) -> None: # pragma: no cover 

196 """ 

197 Change the executor. 

198 

199 :param futures.Executor | None value: the new executor or ``None`` 

200 

201 """ 

202 self._context.executor = value 

203 

204 @property 

205 def loop(self) -> asyncio.AbstractEventLoop | None: 

206 """The event loop.""" 

207 return self._context.loop 

208 

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. 

219 

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. 

229 

230 :returns: a context object that will unlock the file when the context is exited 

231 

232 :raises Timeout: if fails to acquire lock within the timeout period 

233 

234 .. code-block:: python 

235 

236 # You can use this method in the context manager (recommended) 

237 with lock.acquire(): 

238 pass 

239 

240 # Or use an equivalent try-finally construct: 

241 lock.acquire() 

242 try: 

243 pass 

244 finally: 

245 lock.release() 

246 

247 """ 

248 # Use the default timeout, if no timeout is provided. 

249 if timeout is None: 

250 timeout = self._context.timeout 

251 

252 if blocking is None: 

253 blocking = self._context.blocking 

254 

255 if poll_interval is None: 

256 poll_interval = self._context.poll_interval 

257 

258 # Increment the number right at the beginning. We can still undo it, if something fails. 

259 self._context.lock_counter += 1 

260 

261 canonical = _canonical(self.lock_file) 

262 self._raise_if_would_deadlock(canonical, timeout=timeout, blocking=blocking) 

263 

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) 

278 

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) 

310 

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. 

315 

316 :param force: If true, the lock counter is ignored and the lock is released in every case. 

317 

318 """ 

319 if self.is_locked: 

320 self._context.lock_counter -= 1 

321 

322 if self._context.lock_counter == 0 or force: 

323 lock_id, lock_filename = id(self), self.lock_file 

324 

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) 

330 

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() 

338 

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) 

343 

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) 

353 

354 async def __aenter__(self) -> Self: 

355 """ 

356 Acquire the lock. 

357 

358 :returns: the lock object 

359 

360 """ 

361 await self.acquire() 

362 return self 

363 

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. 

372 

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 

376 

377 """ 

378 await self.release() 

379 

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)) 

394 

395 

396class AsyncSoftFileLock(SoftFileLock, BaseAsyncFileLock): 

397 """Simply watches the existence of the lock file.""" 

398 

399 

400class AsyncUnixFileLock(UnixFileLock, BaseAsyncFileLock): 

401 """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" 

402 

403 

404class AsyncWindowsFileLock(WindowsFileLock, BaseAsyncFileLock): 

405 """Uses the :func:`msvcrt.locking` to hard lock the lock file on windows systems.""" 

406 

407 

408__all__ = [ 

409 "AsyncAcquireReturnProxy", 

410 "AsyncSoftFileLock", 

411 "AsyncUnixFileLock", 

412 "AsyncWindowsFileLock", 

413 "BaseAsyncFileLock", 

414]