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

122 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, cast 

14 

15from ._api import _UNSET_FILE_MODE, BaseFileLock, FileLockContext, FileLockMeta 

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 

72class AsyncFileLockMeta(FileLockMeta): 

73 def __call__( # ty: ignore[invalid-method-override] # noqa: PLR0913 

74 cls, # noqa: N805 

75 lock_file: str | os.PathLike[str], 

76 timeout: float = -1, 

77 mode: int = _UNSET_FILE_MODE, 

78 thread_local: bool = False, # noqa: FBT001, FBT002 

79 *, 

80 blocking: bool = True, 

81 is_singleton: bool = False, 

82 poll_interval: float = 0.05, 

83 loop: asyncio.AbstractEventLoop | None = None, 

84 run_in_executor: bool = True, 

85 executor: futures.Executor | None = None, 

86 ) -> BaseAsyncFileLock: 

87 if thread_local and run_in_executor: 

88 msg = "run_in_executor is not supported when thread_local is True" 

89 raise ValueError(msg) 

90 instance = super().__call__( 

91 lock_file=lock_file, 

92 timeout=timeout, 

93 mode=mode, 

94 thread_local=thread_local, 

95 blocking=blocking, 

96 is_singleton=is_singleton, 

97 poll_interval=poll_interval, 

98 loop=loop, 

99 run_in_executor=run_in_executor, 

100 executor=executor, 

101 ) 

102 return cast("BaseAsyncFileLock", instance) 

103 

104 

105class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta): 

106 """ 

107 Base class for asynchronous file locks. 

108 

109 .. versionadded:: 3.15.0 

110 """ 

111 

112 def __init__( # noqa: PLR0913 

113 self, 

114 lock_file: str | os.PathLike[str], 

115 timeout: float = -1, 

116 mode: int = _UNSET_FILE_MODE, 

117 thread_local: bool = False, # noqa: FBT001, FBT002 

118 *, 

119 blocking: bool = True, 

120 is_singleton: bool = False, 

121 poll_interval: float = 0.05, 

122 loop: asyncio.AbstractEventLoop | None = None, 

123 run_in_executor: bool = True, 

124 executor: futures.Executor | None = None, 

125 ) -> None: 

126 """ 

127 Create a new lock object. 

128 

129 :param lock_file: path to the file 

130 :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in 

131 the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it 

132 to a negative value. A timeout of 0 means that there is exactly one attempt to acquire the file lock. 

133 :param mode: file permissions for the lockfile. When not specified, the OS controls permissions via umask 

134 and default ACLs, preserving POSIX default ACL inheritance in shared directories. 

135 :param thread_local: Whether this object's internal context should be thread local or not. If this is set 

136 to ``False`` then the lock will be reentrant across threads. 

137 :param blocking: whether the lock should be blocking or not 

138 :param is_singleton: If this is set to ``True`` then only one instance of this class will be created per 

139 lock file. This is useful if you want to use the lock object for reentrant locking without needing to 

140 pass the same object around. 

141 :param poll_interval: default interval for polling the lock file, in seconds. It will be used as fallback 

142 value in the acquire method, if no poll_interval value (``None``) is given. 

143 :param loop: The event loop to use. If not specified, the running event loop will be used. 

144 :param run_in_executor: If this is set to ``True`` then the lock will be acquired in an executor. 

145 :param executor: The executor to use. If not specified, the default executor will be used. 

146 

147 """ 

148 self._is_thread_local = thread_local 

149 self._is_singleton = is_singleton 

150 

151 # Create the context. Note that external code should not work with the context directly and should instead use 

152 # properties of this class. 

153 kwargs: dict[str, Any] = { 

154 "lock_file": os.fspath(lock_file), 

155 "timeout": timeout, 

156 "mode": mode, 

157 "blocking": blocking, 

158 "poll_interval": poll_interval, 

159 "loop": loop, 

160 "run_in_executor": run_in_executor, 

161 "executor": executor, 

162 } 

163 self._context: AsyncFileLockContext = (AsyncThreadLocalFileContext if thread_local else AsyncFileLockContext)( 

164 **kwargs 

165 ) 

166 

167 @property 

168 def run_in_executor(self) -> bool: 

169 """:return: whether run in executor.""" 

170 return self._context.run_in_executor 

171 

172 @property 

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

174 """:return: the executor.""" 

175 return self._context.executor 

176 

177 @executor.setter 

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

179 """ 

180 Change the executor. 

181 

182 :param value: the new executor or ``None`` 

183 :type value: futures.Executor | None 

184 

185 """ 

186 self._context.executor = value 

187 

188 @property 

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

190 """:return: the event loop.""" 

191 return self._context.loop 

192 

193 async def acquire( # ty: ignore[invalid-method-override] 

194 self, 

195 timeout: float | None = None, 

196 poll_interval: float | None = None, 

197 *, 

198 blocking: bool | None = None, 

199 ) -> AsyncAcquireReturnProxy: 

200 """ 

201 Try to acquire the file lock. 

202 

203 :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default 

204 :attr:`~BaseFileLock.timeout` is and if ``timeout < 0``, there is no timeout and this method will 

205 block until the lock could be acquired 

206 :param poll_interval: interval of trying to acquire the lock file, ``None`` means use the default 

207 :attr:`~BaseFileLock.poll_interval` 

208 :param blocking: defaults to True. If False, function will return immediately if it cannot obtain a lock on 

209 the first attempt. Otherwise, this method will block until the timeout expires or the lock is acquired. 

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

211 :return: a context object that will unlock the file when the context is exited 

212 

213 .. code-block:: python 

214 

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

216 with lock.acquire(): 

217 pass 

218 

219 # Or use an equivalent try-finally construct: 

220 lock.acquire() 

221 try: 

222 pass 

223 finally: 

224 lock.release() 

225 

226 """ 

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

228 if timeout is None: 

229 timeout = self._context.timeout 

230 

231 if blocking is None: 

232 blocking = self._context.blocking 

233 

234 if poll_interval is None: 

235 poll_interval = self._context.poll_interval 

236 

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

238 self._context.lock_counter += 1 

239 

240 lock_id = id(self) 

241 lock_filename = self.lock_file 

242 start_time = time.perf_counter() 

243 try: 

244 while True: 

245 if not self.is_locked: 

246 _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) 

247 await self._run_internal_method(self._acquire) 

248 if self.is_locked: 

249 _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) 

250 break 

251 if blocking is False: 

252 _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename) 

253 raise Timeout(lock_filename) # noqa: TRY301 

254 if 0 <= timeout < time.perf_counter() - start_time: 

255 _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename) 

256 raise Timeout(lock_filename) # noqa: TRY301 

257 msg = "Lock %s not acquired on %s, waiting %s seconds ..." 

258 _LOGGER.debug(msg, lock_id, lock_filename, poll_interval) 

259 await asyncio.sleep(poll_interval) 

260 except BaseException: # Something did go wrong, so decrement the counter. 

261 self._context.lock_counter = max(0, self._context.lock_counter - 1) 

262 raise 

263 return AsyncAcquireReturnProxy(lock=self) 

264 

265 async def release(self, force: bool = False) -> None: # ty: ignore[invalid-method-override] # noqa: FBT001, FBT002 

266 """ 

267 Releases the file lock. Please note, that the lock is only completely released, if the lock counter is 0. 

268 Also note, that the lock file itself is not automatically deleted. 

269 

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

271 

272 """ 

273 if self.is_locked: 

274 self._context.lock_counter -= 1 

275 

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

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

278 

279 _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename) 

280 await self._run_internal_method(self._release) 

281 self._context.lock_counter = 0 

282 _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) 

283 

284 async def _run_internal_method(self, method: Callable[[], Any]) -> None: 

285 if iscoroutinefunction(method): 

286 await method() 

287 elif self.run_in_executor: 

288 loop = self.loop or asyncio.get_running_loop() 

289 await loop.run_in_executor(self.executor, method) 

290 else: 

291 method() 

292 

293 def __enter__(self) -> NoReturn: 

294 """ 

295 Replace old __enter__ method to avoid using it. 

296 

297 NOTE: DO NOT USE `with` FOR ASYNCIO LOCKS, USE `async with` INSTEAD. 

298 

299 :return: none 

300 :rtype: NoReturn 

301 """ 

302 msg = "Do not use `with` for asyncio locks, use `async with` instead." 

303 raise NotImplementedError(msg) 

304 

305 async def __aenter__(self) -> Self: 

306 """ 

307 Acquire the lock. 

308 

309 :return: the lock object 

310 

311 """ 

312 await self.acquire() 

313 return self 

314 

315 async def __aexit__( 

316 self, 

317 exc_type: type[BaseException] | None, 

318 exc_value: BaseException | None, 

319 traceback: TracebackType | None, 

320 ) -> None: 

321 """ 

322 Release the lock. 

323 

324 :param exc_type: the exception type if raised 

325 :param exc_value: the exception value if raised 

326 :param traceback: the exception traceback if raised 

327 

328 """ 

329 await self.release() 

330 

331 def __del__(self) -> None: 

332 """Called when the lock object is deleted.""" 

333 with contextlib.suppress(RuntimeError): 

334 loop = self.loop or asyncio.get_running_loop() 

335 if not loop.is_running(): # pragma: no cover 

336 loop.run_until_complete(self.release(force=True)) 

337 else: 

338 loop.create_task(self.release(force=True)) 

339 

340 

341class AsyncSoftFileLock(SoftFileLock, BaseAsyncFileLock): 

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

343 

344 

345class AsyncUnixFileLock(UnixFileLock, BaseAsyncFileLock): 

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

347 

348 

349class AsyncWindowsFileLock(WindowsFileLock, BaseAsyncFileLock): 

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

351 

352 

353__all__ = [ 

354 "AsyncAcquireReturnProxy", 

355 "AsyncSoftFileLock", 

356 "AsyncUnixFileLock", 

357 "AsyncWindowsFileLock", 

358 "BaseAsyncFileLock", 

359]