Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_api.py: 51%

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

1135 statements  

1from __future__ import annotations 

2 

3__lazy_modules__ = [ 

4 "atexit", 

5 "decimal", 

6 "filelock._error", 

7 "filelock._util", 

8 "inspect", 

9 "math", 

10 "numbers", 

11 "secrets", 

12 "types", 

13 "warnings", 

14] 

15 

16import atexit 

17import contextlib 

18import inspect 

19import logging 

20import math 

21import os 

22import secrets 

23import stat 

24import sys 

25import time 

26import types 

27import warnings 

28from abc import ABCMeta, abstractmethod 

29from collections.abc import Callable, Hashable 

30from contextlib import contextmanager 

31from dataclasses import dataclass, fields 

32from decimal import Decimal 

33from functools import cache, partial 

34from itertools import count, starmap 

35from numbers import Real 

36from threading import TIMEOUT_MAX, Condition, RLock, Thread, current_thread, get_ident, local 

37from typing import TYPE_CHECKING, Final, Generic, Literal, NoReturn, TypedDict, TypeVar, cast 

38from weakref import WeakKeyDictionary, WeakValueDictionary, ref 

39 

40from filelock._error import SoftFileLockLifetimeWarning, Timeout 

41from filelock._util import ensure_directory_exists 

42 

43#: No explicit file permission mode was passed. Lock files then open with 0o666 so umask and default ACLs pick 

44#: the final permissions, and fchmod is skipped to preserve POSIX default ACL inheritance. 

45_UNSET_FILE_MODE: Final[int] = -1 

46 

47_PERMISSION_BITS: Final[int] = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO 

48 

49#: Ceiling on the retry counter used as a power of two, so a long contended wait cannot overflow the backoff multiply. 

50_MAX_BACKOFF_EXPONENT: Final[int] = 20 

51 

52#: How a context manager reconciles a body failure with a release failure on exit (see the property of this name). 

53ContextErrorPolicy = Literal["chain", "group"] 

54 

55#: What a descriptor-owning backend does with an ``os.close`` failure after relinquishing ownership (see the property). 

56CloseErrorPolicy = Literal["default", "raise", "suppress"] 

57 

58if TYPE_CHECKING: 

59 from collections.abc import Generator, Mapping 

60 from threading import Lock 

61 from types import FrameType, TracebackType 

62 from typing import Protocol 

63 

64 from _typeshed import Unused 

65 

66 from filelock._lease import _LeaseClaim 

67 from filelock._read_write import ReadWriteLock 

68 from filelock._soft_rw._sync import SoftReadWriteLock 

69 

70 class _ForkResettable(Protocol): 

71 def _reset_after_fork_in_child(self) -> None: ... 

72 

73 class _ForkDescriptorOwner(Protocol): 

74 def _descriptors_for_fork(self) -> tuple[tuple[int, tuple[int, int] | None], ...]: ... 

75 

76 # Matched against the class object itself rather than `type[...]` of it. A metaclass supplies this method to the 

77 # class while leaving instances without it, so a `type[_ForkResettableClass]` bound rejects `ReadWriteLock`. 

78 class _ForkResettableClass(Protocol): 

79 def _reset_class_after_fork(self) -> None: ... 

80 

81 class _RegisterAtFork(Protocol): 

82 def __call__( 

83 self, 

84 *, 

85 before: Callable[[], None] | None = None, 

86 after_in_parent: Callable[[], None] | None = None, 

87 after_in_child: Callable[[], None] | None = None, 

88 ) -> None: ... 

89 

90 from typing import Self 

91 

92_LOGGER: Final[logging.Logger] = logging.getLogger("filelock") 

93_REGISTER_AT_FORK: Final[_RegisterAtFork | None] = cast("_RegisterAtFork | None", getattr(os, "register_at_fork", None)) 

94_HAS_REGISTER_AT_FORK: Final[bool] = _REGISTER_AT_FORK is not None 

95 

96_ExtraValue = TypeVar("_ExtraValue") 

97_MarkerValue = TypeVar("_MarkerValue") 

98_SubclassValue = TypeVar("_SubclassValue") 

99_LockInitValue = float | int | bool | str | None | Callable[[int], None] 

100 

101 

102class LockOptions(TypedDict, total=False): 

103 """ 

104 Every option the metaclass forwards, so a subclass adding its own can still type what it passes through. 

105 

106 :ivar timeout: default acquire timeout in seconds; a negative value or ``inf`` waits without a limit 

107 :ivar mode: file permissions for the lock file, as in :class:`BaseFileLock` 

108 :ivar thread_local: whether the lock's internal context is per thread, so a reentrant lock is not shared 

109 :ivar blocking: whether acquiring the lock waits for it 

110 :ivar is_singleton: whether one instance is shared per lock file 

111 :ivar poll_interval: default interval in seconds for polling the lock file 

112 

113 :ivar lifetime: for :class:`SoftFileLock`, the age in seconds after which a waiting process may delete the marker 

114 :ivar context_error_policy: how a context manager reconciles a body failure with a release failure 

115 :ivar close_error_policy: what to do with an ``os.close`` failure after the descriptor is relinquished 

116 :ivar fallback_to_soft: for :class:`UnixFileLock`, whether ``ENOSYS`` falls back to :class:`SoftFileLock` 

117 :ivar preserve_lock_file: for native locks, whether the lock pathname is kept on release 

118 :ivar on_acquired: for native locks, a callable invoked with the borrowed lock descriptor on each acquisition 

119 """ 

120 

121 timeout: float 

122 mode: int 

123 thread_local: bool 

124 blocking: bool 

125 is_singleton: bool 

126 poll_interval: float 

127 lifetime: float | None 

128 context_error_policy: ContextErrorPolicy 

129 close_error_policy: CloseErrorPolicy 

130 fallback_to_soft: bool 

131 preserve_lock_file: bool 

132 on_acquired: Callable[[int], None] | None 

133 

134 

135def _is_exception_group(error: BaseException) -> bool: 

136 return isinstance(error, BaseExceptionGroup) 

137 

138 

139def _raise_grouped_errors( 

140 message: str, 

141 first_error: BaseException, 

142 second_error: BaseException, 

143 *additional_errors: BaseException, 

144 marker: tuple[str, _MarkerValue] | None = None, 

145) -> NoReturn: 

146 errors = (first_error, second_error, *additional_errors) 

147 _detach_grouped_contexts(errors) 

148 group: Final = BaseExceptionGroup(message, errors) 

149 if marker is not None: 

150 setattr(group, marker[0], marker[1]) 

151 raise group from None 

152 

153 

154def _detach_grouped_contexts(errors: tuple[BaseException, ...]) -> None: 

155 seen: set[int] = set() 

156 pending = list(errors) 

157 while pending: 

158 error = pending.pop() 

159 if id(error) in seen: 

160 continue 

161 seen.add(id(error)) 

162 if (context := error.__context__) is not None and ( 

163 context is error 

164 or _same_exception_tree(error, context) 

165 or any(context is root or _contains_exception(root, context) for root in errors) 

166 ): 

167 error.__context__ = None 

168 elif context is not None: 

169 pending.append(context) 

170 if error.__cause__ is not None: 

171 pending.append(error.__cause__) 

172 if _is_exception_group(error): 

173 pending.extend(cast("_ExceptionGroupProtocol", error).exceptions) 

174 

175 

176def _same_exception_tree(first: BaseException, second: BaseException) -> bool: 

177 pending = [(first, second)] 

178 seen: set[tuple[int, int]] = set() 

179 while pending: 

180 first_error, second_error = pending.pop() 

181 if first_error is second_error: 

182 continue 

183 if (pair := (id(first_error), id(second_error))) in seen: 

184 continue 

185 seen.add(pair) 

186 if ( 

187 type(first_error) is not type(second_error) 

188 or not _is_exception_group(first_error) 

189 or not _is_exception_group(second_error) 

190 ): 

191 return False 

192 first_group = cast("_ExceptionGroupProtocol", first_error) 

193 second_group = cast("_ExceptionGroupProtocol", second_error) 

194 if first_group.message != second_group.message or len(first_group.exceptions) != len(second_group.exceptions): 

195 return False 

196 pending.extend(zip(first_group.exceptions, second_group.exceptions, strict=True)) 

197 return True 

198 

199 

200def _contains_exception(error: BaseException, target: BaseException | None) -> bool: 

201 if target is None or not _is_exception_group(error): 

202 return False 

203 pending = list(cast("_ExceptionGroupProtocol", error).exceptions) 

204 seen: set[int] = set() 

205 while pending: 

206 child = pending.pop() 

207 if child is target: 

208 return True 

209 if id(child) in seen: 

210 continue 

211 seen.add(id(child)) 

212 if _is_exception_group(child): 

213 pending.extend(cast("_ExceptionGroupProtocol", child).exceptions) 

214 return False 

215 

216 

217def _grouped_errors( 

218 error: BaseException, message: str, marker: tuple[str, _MarkerValue] 

219) -> tuple[BaseException, ...] | None: 

220 if not _is_exception_group(error): 

221 return None 

222 group = cast("_ExceptionGroupProtocol", error) 

223 return group.exceptions if group.message == message and getattr(group, marker[0], None) is marker[1] else None 

224 

225 

226if TYPE_CHECKING: 

227 

228 class _ExceptionGroupProtocol(Protocol): 

229 @property 

230 def message(self) -> str: ... 

231 

232 @property 

233 def exceptions(self) -> tuple[BaseException, ...]: ... 

234 

235 

236def _raise_chained_errors(first_error: BaseException, second_error: BaseException | None = None) -> NoReturn: 

237 if second_error is None: 

238 first_context = first_error.__context__ 

239 try: 

240 raise first_error # ruff:ignore[raise-within-try] # the handler restores caller-supplied context before propagation 

241 except BaseException: 

242 first_error.__context__ = first_context 

243 raise 

244 if (second_context := second_error.__context__) is not None and second_context is not first_error: 

245 _detach_exception_context(second_context, first_error) 

246 _append_exception_context(first_error, second_context) 

247 first_context = first_error.__context__ 

248 try: 

249 raise first_error # ruff:ignore[raise-within-try] # the second raise needs this error as implicit context 

250 except BaseException: # ruff:ignore[blind-except] # first_error may be a control-flow exception 

251 first_error.__context__ = first_context 

252 try: 

253 raise second_error # ruff:ignore[raise-within-try] # the handler makes the chain interpreter-independent 

254 except BaseException: 

255 second_error.__context__ = first_error 

256 first_error.__context__ = first_context 

257 raise 

258 

259 

260def _detach_exception_context(error: BaseException, target: BaseException) -> None: 

261 pending = [error] 

262 seen: set[int] = set() 

263 while pending: 

264 current = pending.pop() 

265 if id(current) in seen: 

266 continue 

267 seen.add(id(current)) 

268 if current.__context__ is target: 

269 current.__context__ = None 

270 elif current.__context__ is not None: 

271 pending.append(current.__context__) 

272 if current.__cause__ is not None: 

273 pending.append(current.__cause__) 

274 if _is_exception_group(current): 

275 pending.extend(cast("_ExceptionGroupProtocol", current).exceptions) 

276 

277 

278def _append_exception_context(error: BaseException, context: BaseException) -> None: 

279 if _exception_graph_contains(error, context) or _exception_graph_contains(context, error): 

280 return 

281 if error.__context__ is None: 

282 error.__context__ = context 

283 return 

284 tail = error 

285 seen: set[int] = set() 

286 while id(tail) not in seen: 

287 seen.add(id(tail)) 

288 if (next_error := tail.__cause__ if tail.__cause__ is not None else tail.__context__) is None: 

289 tail.__context__ = context 

290 return 

291 tail = next_error 

292 

293 

294def _exception_graph_contains(error: BaseException, target: BaseException) -> bool: 

295 pending = [error] 

296 seen: set[int] = set() 

297 while pending: 

298 current = pending.pop() 

299 if current is target: 

300 return True 

301 if id(current) in seen: # pragma: no cover - arbitrary caller exceptions can contain cycles 

302 continue 

303 seen.add(id(current)) 

304 if current.__cause__ is not None: 

305 pending.append(current.__cause__) 

306 if current.__context__ is not None: 

307 pending.append(current.__context__) 

308 if _is_exception_group(current): 

309 pending.extend(cast("_ExceptionGroupProtocol", current).exceptions) 

310 return False 

311 

312 

313def _raise_body_and_release(body_error: BaseException, release_error: BaseException) -> NoReturn: 

314 # Group mode: surface the body failure and the release failure as sibling leaves instead of letting one hide in the 

315 # other's __context__. BaseExceptionGroup returns a plain ExceptionGroup when both leaves subclass Exception, so 

316 # ``except*`` and ``except Exception`` still catch them; a BaseException leaf (KeyboardInterrupt, CancelledError) 

317 # keeps the group outside ordinary handlers. ``from None`` stops the group itself gaining a redundant __context__. 

318 _raise_grouped_errors("lock body and release both failed", body_error, release_error) 

319 

320 

321def _raise_cleanup_errors( 

322 message: str, 

323 primary_error: BaseException, 

324 *cleanup_errors: BaseException | None, 

325) -> NoReturn: 

326 _raise_grouped_errors( 

327 message, 

328 primary_error, 

329 *(error for error in cleanup_errors if error is not None), 

330 ) 

331 

332 

333# On Windows os.path.realpath calls CreateFileW with share_mode=0, which blocks concurrent DeleteFileW and causes 

334# livelocks under threaded contention with SoftFileLock. os.path.abspath is purely string-based and avoids this. 

335_RESOLVE_DIR: Final[Callable[[str], str]] = os.path.abspath if sys.platform == "win32" else os.path.realpath 

336 

337 

338def _canonical(path: str | os.PathLike[str]) -> str: 

339 # Return one stable key for *path*, collapsing equivalent spellings without following a final symlink. 

340 # 

341 # Relative, absolute, and ``./`` spellings of one lock file must map to a single singleton instance, 

342 # deadlock-registry entry, and removal key. Resolving the whole path with ``realpath`` would follow a final symlink 

343 # and alias a lock target the backend deliberately rejects, so the registry identity would differ from the 

344 # backend's. Resolving only the parent directory and re-appending the literal final component collapses the 

345 # equivalent spellings while keeping a final symlink a distinct key. On Windows the parent is resolved with 

346 # ``abspath`` so junctions and reparse points are not followed either. 

347 parent, name = os.path.split(os.fspath(path)) 

348 return os.path.join(_RESOLVE_DIR(parent or os.curdir), name) # ruff:ignore[os-path-join] # string join matches abspath/realpath 

349 

350 

351class _ThreadLocalRegistry(local): 

352 def __init__(self) -> None: 

353 super().__init__() 

354 self.held: dict[Hashable, int] = {} 

355 

356 

357_REGISTRY: Final[_ThreadLocalRegistry] = _ThreadLocalRegistry() 

358 

359 

360_T = TypeVar("_T", bound="BaseFileLock") 

361_LockT = TypeVar("_LockT", bound="BaseFileLock | ReadWriteLock | SoftReadWriteLock") 

362 

363 

364class FileLockMeta(ABCMeta): 

365 _instances: WeakValueDictionary[str, BaseFileLock] 

366 _instances_lock: RLock 

367 _instances_under_construction: set[str] 

368 

369 def __call__( # ruff:ignore[too-many-arguments] # forwards the public constructor's documented parameters 

370 cls: type[_T], 

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

372 timeout: float = -1, 

373 mode: int = _UNSET_FILE_MODE, 

374 thread_local: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility 

375 *, 

376 blocking: bool = True, 

377 is_singleton: bool = False, 

378 poll_interval: float = 0.05, 

379 lifetime: float | None = None, 

380 context_error_policy: ContextErrorPolicy = "chain", 

381 close_error_policy: CloseErrorPolicy = "default", 

382 fallback_to_soft: bool = True, 

383 preserve_lock_file: bool = False, 

384 on_acquired: Callable[[int], None] | None = None, 

385 **kwargs: _ExtraValue, 

386 ) -> _T: 

387 _ensure_current_process() 

388 _resolve_lazy_imports_before_shutdown() 

389 lifetime = _resolve_lifetime(lifetime, cls, stacklevel=cls._constructor_lifetime_warning_stacklevel) 

390 poll_interval = _resolve_poll_interval(poll_interval) 

391 # Validate before building the instance: a raise inside __init__ would leave a half-constructed object whose 

392 # __del__ then trips over the missing context. 

393 _check_blocking(blocking) 

394 # nan is left to acquire, which knows whether the call blocks; a non-blocking call ignores the timeout. 

395 timeout = _seconds("timeout", timeout) 

396 _check_mode(mode, cls.__name__) 

397 context_error_policy = _resolve_context_error_policy(context_error_policy) 

398 close_error_policy = _resolve_close_error_policy(close_error_policy) 

399 preserve_lock_file = _resolve_preserve_lock_file( 

400 preserve=preserve_lock_file, supported=cls._preserve_lock_file_supported, cls_name=cls.__name__ 

401 ) 

402 on_acquired = _resolve_on_acquired(on_acquired, supported=cls._on_acquired_supported, cls_name=cls.__name__) 

403 # Checked before the singleton lookup so a cache hit rejects a misspelled option just like a fresh build does. 

404 params: Final = _supported_init_params( 

405 cls, 

406 { 

407 "timeout": timeout, 

408 "mode": mode, 

409 "thread_local": thread_local, 

410 "blocking": blocking, 

411 "is_singleton": is_singleton, 

412 "poll_interval": poll_interval, 

413 "lifetime": lifetime, 

414 "context_error_policy": context_error_policy, 

415 "close_error_policy": close_error_policy, 

416 "fallback_to_soft": fallback_to_soft, 

417 "preserve_lock_file": preserve_lock_file, 

418 "on_acquired": on_acquired, 

419 **kwargs, 

420 }, 

421 ) 

422 if not is_singleton: 

423 return super().__call__(lock_file, **params) 

424 

425 # Look up, build and store under one lock. Without it two threads racing the first construction for a 

426 # path both miss the cache and each build their own instance, so callers relying on is_singleton for 

427 # reentrant locking across instances end up with two "singletons" and acquire()'s deadlock check then 

428 # rejects a legitimate reentrant acquire; the unguarded writes to the WeakValueDictionary are a data 

429 # race besides. ReadWriteLock and SoftReadWriteLock already guard their singleton caches this way. 

430 # Key the cache on the canonical form so equivalent spellings of one path share a singleton, and it matches the 

431 # deadlock-registry key acquire() uses. 

432 singleton_key = _canonical(lock_file) 

433 with cls._instances_lock: 

434 # A cached lock resolves its relative path at each acquisition and pins it for the hold. After a chdir, or 

435 # once a parent symlink points elsewhere, an unheld lock may name another file than this key; a held one 

436 # names the file it pinned. 

437 if (instance := cls._instances.get(singleton_key)) is None or _canonical( 

438 instance._lock_path # ruff:ignore[private-member-access] # the metaclass owns the instance it returns 

439 ) != singleton_key: 

440 if singleton_key in cls._instances_under_construction: # pragma: needs fork 

441 msg = f"Singleton lock construction is already active for {lock_file!s}" 

442 raise RuntimeError(msg) 

443 construction_registry = cls._instances_under_construction 

444 construction_pid = os.getpid() 

445 construction_registry.add(singleton_key) 

446 try: 

447 instance = super().__call__(lock_file, **params) 

448 finally: 

449 construction_registry.discard(singleton_key) 

450 if os.getpid() != construction_pid: # pragma: needs fork 

451 msg = "Lock construction cannot continue after fork; construct a new lock in the child" 

452 raise RuntimeError(msg) 

453 cls._instances[singleton_key] = instance 

454 instance._singleton_key = singleton_key # ruff:ignore[private-member-access] # the cache entry the finalizer looks up 

455 return instance 

456 

457 non_matching_params = { 

458 name: (str(passed_param), str(set_param)) 

459 for name, (passed_param, set_param) in { 

460 "thread_local": (thread_local, instance.is_thread_local()), 

461 # Compare the wait, not the value: nan never equals itself, and -1, -5 and inf all mean no limit. 

462 "timeout": ( 

463 _timeout_behavior(timeout, any_negative=True), 

464 _timeout_behavior(instance.timeout, any_negative=True), 

465 ), 

466 "mode": (mode, instance._context.mode), # ruff:ignore[private-member-access] # compares against the managed instance's own context 

467 "blocking": (blocking, instance.blocking), 

468 "poll_interval": (poll_interval, instance.poll_interval), 

469 "lifetime": (lifetime, instance.lifetime), 

470 "context_error_policy": (context_error_policy, instance.context_error_policy), 

471 "close_error_policy": (close_error_policy, instance.close_error_policy), 

472 "fallback_to_soft": (fallback_to_soft, instance.fallback_to_soft), 

473 "preserve_lock_file": (preserve_lock_file, instance.preserve_lock_file), 

474 }.items() 

475 if passed_param != set_param 

476 } 

477 # The metaclass owns the instance it returns, so reading the subclass hook stays inside this module's contract. 

478 non_matching_params.update(instance._singleton_extra_mismatches(kwargs)) # ruff:ignore[private-member-access] 

479 # Callables compare by identity, not equality: two equal callables can close over different state, so a 

480 # singleton must reject a different hook object even if it compares equal. Keep it out of the scalar dict above. 

481 hook_mismatch = on_acquired is not instance.on_acquired 

482 if not non_matching_params and not hook_mismatch: 

483 return instance # ty: ignore[invalid-return-type] # https://github.com/astral-sh/ty/issues/3231 

484 

485 msg = "Singleton lock instances cannot be initialized with differing arguments" 

486 msg += "\nNon-matching arguments: " 

487 for param_name, (passed_param, set_param) in non_matching_params.items(): 

488 msg += f"\n\t{param_name} (existing lock has {set_param} but {passed_param} was passed)" 

489 if hook_mismatch: 

490 msg += f"\n\ton_acquired (existing lock has {instance.on_acquired} but {on_acquired} was passed)" 

491 raise ValueError(msg) 

492 

493 

494def _resolve_lifetime(lifetime: float | None, cls: type[BaseFileLock], *, stacklevel: int) -> float | None: 

495 # Validate ``lifetime`` and drop a value the backend cannot honor. 

496 # 

497 # ``lifetime`` is a deliberate age-based lease: a lock file older than ``lifetime`` is broken even while its holder 

498 # is still alive. Existence locks (:class:`SoftFileLock`) implement that behavior by unlinking a reclaimable 

499 # pathname, which can overlap a live holder. A native OS lock lives on the inode, so unlinking the pathname by age 

500 # cannot revoke the kernel lock; a contender would lock a fresh inode and overlap the live holder (#590). Ignore the 

501 # request with a warning rather than accept a setting that breaks mutual exclusion. 

502 if lifetime is None: 

503 return None 

504 # An int stays exact: no float holds every int 4.0.12 accepted here. 

505 if isinstance(lifetime, bool) or not isinstance(lifetime, int): 

506 lifetime = _seconds("lifetime", lifetime) 

507 if lifetime < 0 or (isinstance(lifetime, float) and not math.isfinite(lifetime)): 

508 msg = f"lifetime must be finite and non-negative, not {lifetime!r}" 

509 raise ValueError(msg) 

510 if not cls._lifetime_supported: 

511 warnings.warn( 

512 f"lifetime is ignored for {cls.__name__}: {cls._lifetime_unsupported_reason}; " 

513 f"only SoftFileLock supports lifetime-based expiry", 

514 stacklevel=stacklevel, 

515 ) 

516 return None 

517 if cls._lifetime_replacements is not None: 

518 strict_lock, lease = cls._lifetime_replacements 

519 warnings.warn( 

520 f"{cls.__name__}(lifetime=...) uses age-based expiry and can overlap a live holder; " 

521 f"use {lease} for expiry or {strict_lock} for fail-closed locking", 

522 SoftFileLockLifetimeWarning, 

523 stacklevel=stacklevel, 

524 ) 

525 return lifetime 

526 

527 

528def _resolve_poll_interval(poll_interval: float) -> float: 

529 # Validate ``poll_interval`` used for ``time.sleep`` / ``asyncio.sleep`` retries. 

530 if (poll_interval := _seconds("poll_interval", poll_interval)) < 0 or not math.isfinite(poll_interval): 

531 msg = f"poll_interval must be finite and non-negative, not {poll_interval!r}" 

532 raise ValueError(msg) 

533 _check_timeout_max("poll_interval", poll_interval) 

534 return poll_interval 

535 

536 

537def _check_blocking(blocking: bool) -> None: # ruff:ignore[boolean-type-hint-positional-argument] # validates the option's own value 

538 if not isinstance(blocking, bool): 

539 msg = f"blocking must be a bool, not {type(blocking).__name__}" 

540 raise TypeError(msg) 

541 

542 

543def _seconds(name: str, value: float) -> float: 

544 # 4.0.12 accepted Decimal and Fraction; a bool is a typo, and a string would only fail later. 

545 if isinstance(value, bool) or not isinstance(value, (Real, Decimal)): 

546 msg = f"{name} must be a number of seconds, not {type(value).__name__}" 

547 raise TypeError(msg) 

548 # float() refuses a signaling NaN, which means no number of seconds just as a quiet one does. 

549 if isinstance(value, Decimal) and value.is_snan(): 

550 return math.nan 

551 try: 

552 seconds: Final = float(value) 

553 except OverflowError: 

554 msg = f"{name} is too large to be a number of seconds: {value!r}" 

555 raise ValueError(msg) from None 

556 # float() rounds a tiny negative to -0.0, which compares as zero, yet timeout and lifetime checks read the sign. 

557 return -math.ulp(0.0) if seconds == 0 and value < 0 else seconds 

558 

559 

560def _check_mode(mode: int, cls_name: str) -> None: 

561 if mode == _UNSET_FILE_MODE: 

562 return 

563 if isinstance(mode, bool) or not isinstance(mode, int): 

564 msg = f"mode must be an int, not {type(mode).__name__}" 

565 raise TypeError(msg) 

566 # A lock file is never executed, so setuid, setgid and sticky have no meaning on it and only widen its permissions. 

567 if not 0 <= mode <= _PERMISSION_BITS: 

568 msg = f"mode={mode:#o} must be within 0o000-0o777 for {cls_name}" 

569 raise ValueError(msg) 

570 # A lock reopens, reads, or deletes the files it creates, so a mode that denies the owner read or write fails later 

571 # and for good. 

572 if ~mode & (stat.S_IRUSR | stat.S_IWUSR): 

573 msg = f"mode={mode:#o} must grant the owner read and write for {cls_name}" 

574 raise ValueError(msg) 

575 

576 

577def _resolve_context_error_policy(policy: str) -> ContextErrorPolicy: 

578 if policy not in frozenset({"chain", "group"}): 

579 msg = f"context_error_policy must be 'chain' or 'group', got {policy!r}" 

580 raise ValueError(msg) 

581 return cast("ContextErrorPolicy", policy) 

582 

583 

584def _resolve_close_error_policy(policy: str) -> CloseErrorPolicy: 

585 if policy not in frozenset({"default", "raise", "suppress"}): 

586 msg = f"close_error_policy must be 'default', 'raise', or 'suppress', got {policy!r}" 

587 raise ValueError(msg) 

588 return cast("CloseErrorPolicy", policy) 

589 

590 

591def _resolve_preserve_lock_file(*, preserve: bool, supported: bool, cls_name: str) -> bool: 

592 # An existence lock unlinks its marker to release, so preserving the pathname would defeat unlocking. Reject the 

593 # request rather than silently ignore it, since a caller asking for a stable identity must know it cannot be kept. 

594 if preserve and not supported: 

595 msg = f"preserve_lock_file=True is not supported by {cls_name}: unlinking its marker is how it releases" 

596 raise ValueError(msg) 

597 return preserve 

598 

599 

600def _resolve_on_acquired( 

601 on_acquired: Callable[[int], None] | None, *, supported: bool, cls_name: str 

602) -> Callable[[int], None] | None: 

603 if on_acquired is None: 

604 return None 

605 # An existence lock stores protocol state in its marker, so a caller writing through the descriptor would corrupt 

606 # stale detection and ownership metadata; only native locks lend out the descriptor. 

607 if not supported: 

608 msg = f"on_acquired is not supported by {cls_name}: only native locks expose the lock descriptor" 

609 raise ValueError(msg) 

610 return on_acquired 

611 

612 

613def _supported_init_params( 

614 cls: type[BaseFileLock], params: dict[str, _LockInitValue | _ExtraValue] 

615) -> dict[str, _LockInitValue | _ExtraValue]: 

616 # A strong cache would keep dynamically created subclasses alive for the process lifetime. 

617 with _fork_transition(), _FORK_STATE.parameter_models_lock: 

618 if (model := _INIT_PARAMETER_MODELS.get(cls)) is None: 

619 parameters = inspect.signature(cls.__init__).parameters.values() 

620 model = _InitParameterModel( 

621 accepted_params=frozenset( 

622 parameter.name 

623 for parameter in parameters 

624 if parameter.kind in {inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY} 

625 ), 

626 accepts_kwargs=any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters), 

627 default_params={ 

628 name: parameter 

629 for name, parameter in inspect.signature(type(cls).__call__).parameters.items() 

630 if parameter.default is not inspect.Parameter.empty 

631 }, 

632 ) 

633 _INIT_PARAMETER_MODELS[cls] = model 

634 if model.accepts_kwargs: 

635 return params 

636 if unsupported := sorted( 

637 name 

638 for name, value in params.items() 

639 if name not in model.accepted_params 

640 and ((parameter := model.default_params.get(name)) is None or value != parameter.default) 

641 ): 

642 msg = f"{cls.__name__} does not support non-default lock options: {', '.join(unsupported)}" 

643 raise TypeError(msg) 

644 # virtualenv narrows a BaseFileLock descendant's signature; omit base defaults it does not accept (#340). 

645 return {name: value for name, value in params.items() if name in model.accepted_params} 

646 

647 

648def _timeout_behavior(timeout: float, *, any_negative: bool) -> float | str: 

649 # inf, and for a BaseFileLock any negative value, wait without a limit like -1. 

650 if math.isnan(timeout): 

651 return "nan" 

652 return -1.0 if timeout == math.inf or (any_negative and timeout < 0) else timeout 

653 

654 

655def _check_timeout_max(name: str, value: float) -> None: 

656 # Lock.acquire, Event.wait and time.sleep raise OverflowError past this bound, which is only ~49.7 days on Windows. 

657 if value > TIMEOUT_MAX: 

658 msg = f"{name} must not exceed threading.TIMEOUT_MAX ({TIMEOUT_MAX}), got {value!r}" 

659 raise ValueError(msg) 

660 

661 

662_INIT_PARAMETER_MODELS: Final[WeakKeyDictionary[type[BaseFileLock], _InitParameterModel]] = WeakKeyDictionary() 

663 

664 

665@dataclass(frozen=True, slots=True, kw_only=True) 

666class _InitParameterModel: 

667 accepted_params: frozenset[str] 

668 accepts_kwargs: bool 

669 default_params: dict[str, inspect.Parameter] 

670 

671 

672@dataclass(frozen=True, slots=True, kw_only=True) 

673class _LockSettings: 

674 thread_local: bool 

675 is_singleton: bool 

676 context_error_policy: ContextErrorPolicy 

677 close_error_policy: CloseErrorPolicy 

678 fallback_to_soft: bool 

679 preserve_lock_file: bool 

680 on_acquired: Callable[[int], None] | None 

681 

682 

683class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta): # ruff:ignore[too-many-public-methods] # public config properties 

684 """ 

685 Abstract base class for a file lock object. 

686 

687 Provides the common reentrant API and state management. Subclasses implement the locking mechanism 

688 (:class:`UnixFileLock <filelock.UnixFileLock>`, :class:`WindowsFileLock <filelock.WindowsFileLock>`, 

689 :class:`SoftFileLock <filelock.SoftFileLock>`). 

690 

691 """ 

692 

693 _instances: WeakValueDictionary[str, BaseFileLock] 

694 _instances_lock: RLock 

695 _instances_under_construction: set[str] 

696 

697 #: How the cross-instance deadlock message names the conflicting holder; the async subclass says "task". 

698 _deadlock_holder_desc: str = "FileLock instance in this thread" 

699 

700 #: Whether an age-based :attr:`lifetime` lease may break this lock. Only existence locks set it (they reclaim by 

701 #: unlinking a pathname); native OS locks leave it ``False`` since a kernel lock cannot be revoked by file age. 

702 _lifetime_supported: bool = False 

703 

704 #: Strict-lock and lease replacements for a backend with legacy age-based expiry. 

705 _lifetime_replacements: tuple[str, str] | None = None 

706 

707 #: Why a backend that refuses ``lifetime`` cannot honor it, named in the warning that drops the value. 

708 _lifetime_unsupported_reason: str = "a native OS lock cannot be broken safely by file age" 

709 

710 #: Async construction adds one metaclass frame before lifetime validation. 

711 _constructor_lifetime_warning_stacklevel: int = 3 

712 

713 #: Whether :attr:`preserve_lock_file` may be ``True``. Native locks keep the pathname on release, so they support 

714 #: it; existence locks unlink their marker to release and reject it. 

715 _preserve_lock_file_supported: bool = True 

716 

717 #: Whether an :attr:`on_acquired` hook may be set. Native locks lend the descriptor out; existence locks keep 

718 #: protocol state in the marker and reject it. 

719 _on_acquired_supported: bool = True 

720 

721 #: Ceiling in seconds on the jittered backoff between contended acquisition retries. ``0`` keeps the fixed 

722 #: poll cadence; a multi-file backend sets it so contending processes desynchronize instead of livelocking. 

723 _poll_backoff_cap: float = 0.0 

724 

725 #: The singleton cache key of this instance, or ``None`` outside the cache. 

726 _singleton_key: str | None = None 

727 

728 def __init_subclass__(cls, **kwargs: _SubclassValue) -> None: 

729 """Give each lock subclass its own singleton registry and lock.""" 

730 super().__init_subclass__(**kwargs) 

731 cls._instances = WeakValueDictionary() 

732 cls._instances_lock = RLock() 

733 cls._instances_under_construction = set() 

734 _register_fork_class(cls) 

735 

736 def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option 

737 self, 

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

739 timeout: float = -1, 

740 mode: int = _UNSET_FILE_MODE, 

741 thread_local: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility 

742 *, 

743 blocking: bool = True, 

744 is_singleton: bool = False, 

745 poll_interval: float = 0.05, 

746 lifetime: float | None = None, 

747 context_error_policy: ContextErrorPolicy = "chain", 

748 close_error_policy: CloseErrorPolicy = "default", 

749 fallback_to_soft: bool = True, 

750 preserve_lock_file: bool = False, 

751 on_acquired: Callable[[int], None] | None = None, 

752 ) -> None: 

753 """ 

754 Create a new lock object. 

755 

756 :param lock_file: path to the file 

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

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

759 negative value or ``inf``. A timeout of 0 means that there is exactly one attempt to acquire the file lock. 

760 

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

762 default ACLs, preserving POSIX default ACL inheritance in shared directories. Construction raises 

763 :class:`ValueError` unless it grants the owner read and write, which the lock needs for its own files. On 

764 Windows a native lock creates its file with an owner-only ACL when the mode has no group and other bits; any 

765 other mode, and a soft lock, leave the file the ACL its directory hands down. A Unix lock file that already 

766 exists keeps its permissions. 

767 

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

769 ``False`` then the lock will be reentrant across threads. When ``True`` (the default), **all fields of the 

770 lock's internal context are per-thread**, including the configuration values ``poll_interval``, ``timeout``, 

771 ``blocking``, ``mode``, and ``lifetime``. 

772 

773 Setting one of these properties from one thread does not change the value seen by another thread; threads 

774 that did not perform the write continue to see the value supplied at construction time. ``mode`` has no 

775 setter, so construction is the only place it is ever set. If you need configuration values to be visible 

776 across threads, construct the lock with ``thread_local=False``. 

777 

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

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

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

781 same object around. 

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

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

784 

785 :param lifetime: for :class:`SoftFileLock`, the age in seconds after which a waiting process may delete the 

786 marker, even while its holder remains alive. This legacy expiry mode does not provide strict mutual 

787 exclusion. ``None`` (the default) disables age-based expiry. Native OS locks (:class:`FileLock`) cannot be 

788 revoked by file age and ignore a non-``None`` ``lifetime`` with a warning. 

789 

790 :param context_error_policy: how a context manager reconciles a failure in its body with a failure while 

791 releasing on exit. ``"chain"`` (the default) keeps Python's behavior: the release error propagates with the 

792 body error in its ``__context__``. ``"group"`` raises a :class:`BaseExceptionGroup` holding the body error 

793 first and the release error second, so neither hides the other. 

794 

795 :param close_error_policy: what to do with an ``os.close`` failure after relinquishing descriptor ownership. 

796 ``"default"`` keeps each backend's historical behavior (Unix native locks drop a FUSE/Docker ``EIO``; 

797 Windows native locks and :class:`SoftFileLock` propagate); ``"raise"`` always propagates the ``OSError``; 

798 ``"suppress"`` always ignores it. Held state is released either way. It does not affect unlock failures or 

799 lock-file deletion. 

800 

801 :param fallback_to_soft: for :class:`UnixFileLock`, whether to switch to :class:`SoftFileLock` when the 

802 filesystem's ``flock`` returns ``ENOSYS``. ``True`` (the default) keeps the historical fallback; ``False`` 

803 fails closed, letting the ``ENOSYS`` propagate so a caller that needs kernel-enforced locking is never 

804 silently downgraded. It has no effect on Windows or :class:`SoftFileLock`. 

805 

806 :param preserve_lock_file: for native locks (:class:`FileLock`), whether filelock promises not to unlink the 

807 lock pathname on release. ``False`` (the default) keeps each backend's cleanup: Windows removes the lock 

808 file, Unix already leaves it. ``True`` keeps a stable file identity for ACLs, auditing, and holder metadata: 

809 Windows skips its post-release unlink and Unix refuses to enter the ``ENOSYS`` soft fallback (which releases 

810 by unlinking). :class:`SoftFileLock` rejects ``True``. The promise covers filelock's own release path only; 

811 it cannot stop another process or the filesystem from removing the pathname. 

812 

813 :param on_acquired: for native locks (:class:`FileLock`), a callable invoked with the borrowed lock descriptor 

814 once per physical acquisition, after filelock holds the native lock and finished backend initialization but 

815 before :meth:`~BaseFileLock.acquire` returns. Recursive acquisitions do not call it again. The callback may 

816 read, write, seek, truncate, or set metadata through ``os`` on the descriptor, but must not close, unlock, 

817 or take ownership of it, and filelock does not fsync its writes. If it raises, filelock releases the lock 

818 and re-raises. :class:`SoftFileLock` rejects the hook. 

819 

820 :raises TypeError: if *timeout* or *poll_interval* is a :class:`bool` or not a real number, *blocking* is not a 

821 :class:`bool`, or *mode* is not an :class:`int` 

822 :raises ValueError: if *poll_interval* is negative, not finite, or above :data:`threading.TIMEOUT_MAX`, if 

823 *timeout* is too large to convert to :class:`float`, or if *mode* falls outside ``0o000``-``0o777`` or 

824 denies the owner read and write 

825 

826 """ 

827 self._adopt_settings( 

828 _LockSettings( 

829 thread_local=thread_local, 

830 is_singleton=is_singleton, 

831 context_error_policy=context_error_policy, 

832 close_error_policy=close_error_policy, 

833 fallback_to_soft=fallback_to_soft, 

834 preserve_lock_file=preserve_lock_file, 

835 on_acquired=on_acquired, 

836 ) 

837 ) 

838 

839 self._context: FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext)( 

840 lock_file=os.fspath(lock_file), 

841 timeout=timeout, 

842 mode=mode, 

843 blocking=blocking, 

844 poll_interval=poll_interval, 

845 lifetime=lifetime, 

846 ) 

847 #: Each thread's committed hold on a thread-local lock, so another thread can release it. 

848 self._thread_holds: dict[int, tuple[FileLockContext, Thread]] = {} 

849 #: Each thread's uncommitted acquisition, so a backend can finish it for an exited thread. 

850 self._thread_pending: dict[int, tuple[FileLockContext, Thread]] = {} 

851 _register_fork_object(self) 

852 

853 def _adopt_settings(self, settings: _LockSettings) -> None: 

854 self._creator_pid = os.getpid() 

855 #: Holder id in the deadlock registry; a stand-in that finishes the finalizer has another id(self). 

856 self._holder_id = id(self) 

857 self._transition_lock = RLock() 

858 self._is_thread_local = settings.thread_local 

859 self._is_singleton = settings.is_singleton 

860 self._context_error_policy = settings.context_error_policy # already validated by the metaclass 

861 self._close_error_policy = settings.close_error_policy # already validated by the metaclass 

862 self._fallback_to_soft = settings.fallback_to_soft 

863 self._preserve_lock_file = settings.preserve_lock_file # already validated by the metaclass 

864 self._on_acquired = settings.on_acquired # already validated by the metaclass 

865 

866 def __enter__(self) -> Self: 

867 """ 

868 Acquire the lock. 

869 

870 :returns: the lock object 

871 

872 """ 

873 self.acquire() 

874 return self 

875 

876 def acquire( 

877 self, 

878 timeout: float | None = None, 

879 poll_interval: float | None = None, 

880 *, 

881 poll_intervall: float | None = None, 

882 blocking: bool | None = None, 

883 cancel_check: Callable[[], bool] | None = None, 

884 ) -> AcquireReturnProxy[Self]: 

885 """ 

886 Try to acquire the file lock. 

887 

888 :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default :attr:`~timeout` is and 

889 if ``timeout < 0``, there is no timeout and this method will block until the lock could be acquired 

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

891 :attr:`~poll_interval` 

892 :param poll_intervall: deprecated, kept for backwards compatibility, use ``poll_interval`` instead 

893 

894 :param blocking: whether to retry until this call holds the lock or the timeout expires, ``None`` means use the 

895 default :attr:`~blocking`. ``False`` makes one attempt and raises :class:`~Timeout` on failure. 

896 :param cancel_check: a callable returning ``True`` when the acquisition should be canceled. Checked on each poll 

897 iteration. When triggered, raises :class:`~Timeout` just like an expired timeout. 

898 

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

900 

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

902 :raises RuntimeError: if the lock was inherited across a fork, or this thread already waits on or releases it 

903 :raises TypeError: if *blocking* is not a :class:`bool` 

904 :raises ValueError: if a blocking call gets a ``nan`` timeout, or *poll_interval* exceeds 

905 :data:`threading.TIMEOUT_MAX` 

906 

907 .. code-block:: python 

908 

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

910 with lock.acquire(): 

911 pass 

912 

913 # Or use an equivalent try-finally construct: 

914 lock.acquire() 

915 try: 

916 pass 

917 finally: 

918 lock.release() 

919 

920 .. versionchanged:: 2.0.0 

921 

922 This method returns now a *proxy* object instead of *self*, so that it can be used in a with statement 

923 without side effects. 

924 

925 """ 

926 self._raise_if_inherited() 

927 if timeout is None: 

928 timeout = self._context.timeout 

929 

930 if blocking is None: 

931 blocking = self._context.blocking 

932 

933 # A truthy non-bool such as the string "false" would otherwise turn a single attempt into a blocking wait. 

934 _check_blocking(blocking) 

935 

936 if poll_intervall is not None: 

937 msg = "use poll_interval instead of poll_intervall" 

938 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

939 poll_interval = poll_intervall 

940 

941 poll_interval = poll_interval if poll_interval is not None else self._context.poll_interval 

942 poll_interval = _resolve_poll_interval(poll_interval) 

943 timeout = _resolve_timeout(timeout, blocking=blocking) 

944 

945 start_time = time.perf_counter() 

946 # Wait for admission before touching any state: a caller refused entry must leave the counter, the registry and 

947 # the descriptor exactly as it found them. 

948 with self._transition_admission( 

949 blocking=blocking, 

950 cancel_check=cancel_check, 

951 timeout=timeout, 

952 poll_interval=poll_interval, 

953 start_time=start_time, 

954 ): 

955 # The interrupted acquire cannot run until the handler returns, so a second one here would wait on it 

956 # forever or take the lock out from under it. Once the lock is held, as in an on_acquired hook, it nests. 

957 if not self.is_locked and _on_this_stack(self._context.acquiring): 

958 msg = f"Deadlock: lock '{self.lock_file}' is acquired again while this thread's acquire of it waits" 

959 raise RuntimeError(msg) 

960 # An interrupted release commits only the hold it started with and ignores a release above it on the 

961 # stack. A hold taken here would nest into one the OS dropped, or stay held for good. 

962 if _on_this_stack(self._context.releasing): 

963 msg = f"lock '{self.lock_file}' is acquired again while this thread's release of it runs" 

964 raise RuntimeError(msg) 

965 canonical = _canonical(self.lock_file) 

966 self._context.lock_counter += 1 

967 

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

969 if not self.is_locked: 

970 self._pin_lock_path(canonical) 

971 

972 try: 

973 self._poll_until_acquired( 

974 blocking=blocking, 

975 cancel_check=cancel_check, 

976 timeout=timeout, 

977 poll_interval=poll_interval, 

978 start_time=start_time, 

979 ) 

980 except BaseException: 

981 self._reconcile_failed_acquire(canonical) 

982 raise 

983 self._commit_acquire(canonical) 

984 return AcquireReturnProxy(lock=self) 

985 

986 def _raise_if_inherited(self) -> None: 

987 if self._creator_pid != os.getpid(): # pragma: forked child 

988 msg = f"{type(self).__name__} on {self.lock_file} was inherited across fork; construct a new instance" 

989 raise RuntimeError(msg) 

990 

991 @contextlib.contextmanager 

992 def _transition_admission( 

993 self, 

994 *, 

995 blocking: bool, 

996 cancel_check: Callable[[], bool] | None, 

997 timeout: float, 

998 poll_interval: float, 

999 start_time: float, 

1000 ) -> Generator[None]: 

1001 # One thread at a time drives the physical transition of a shared instance: its counter, descriptor and claim 

1002 # state are plain attributes every thread reads and writes, so an unserialized loser clears the winner's 

1003 # descriptor or double-closes it. Waiting in slices keeps cancel_check and the timeout live. A thread-local 

1004 # context is never shared, so it skips the gate. 

1005 if self._is_thread_local: 

1006 yield 

1007 return 

1008 deadline: Final = None if timeout < 0 else start_time + timeout 

1009 wait: Final = poll_interval if blocking else 0.0 

1010 # wait leads the min() so a non-blocking call, whose nan timeout goes unchecked, waits 0 rather than nan. 

1011 while not self._transition_lock.acquire( 

1012 timeout=wait if deadline is None else min(wait, max(deadline - time.perf_counter(), 0.0)) 

1013 ): 

1014 if not blocking or (cancel_check is not None and cancel_check()): 

1015 raise Timeout(self.lock_file) 

1016 if deadline is not None and time.perf_counter() >= deadline: 

1017 raise Timeout(self.lock_file) 

1018 try: 

1019 yield 

1020 finally: 

1021 self._transition_lock.release() 

1022 

1023 @property 

1024 def is_locked(self) -> bool: 

1025 """ 

1026 A boolean indicating if the lock file is holding the lock currently. 

1027 

1028 .. versionchanged:: 2.0.0 

1029 

1030 This was previously a method and is now a property. 

1031 

1032 """ 

1033 _ensure_current_process() 

1034 return self._context.lock_file_fd is not None 

1035 

1036 @property 

1037 def lock_file(self) -> str: 

1038 """Path to the lock file.""" 

1039 return self._context.lock_file 

1040 

1041 def _raise_if_would_deadlock(self, canonical: str, *, timeout: float, blocking: bool) -> None: 

1042 # Fail fast when a *different* live instance already holds this path in the current deadlock scope. 

1043 # 

1044 # Only the first, indefinitely-blocking acquire can self-deadlock this way: waiting in the OS primitive would 

1045 # block on a lock this flow already owns. A finite timeout or ``blocking=False`` keeps the normal Timeout path. 

1046 would_block = self._context.lock_counter == 1 and not self.is_locked and timeout < 0 and blocking 

1047 if would_block and _REGISTRY.held.get(self._registry_key(canonical)) not in {None, self._holder_id}: 

1048 self._context.lock_counter -= 1 

1049 msg = ( 

1050 f"Deadlock: lock '{self.lock_file}' is already held by a different {self._deadlock_holder_desc}. " 

1051 f"Use is_singleton=True to enable reentrant locking across instances." 

1052 ) 

1053 raise RuntimeError(msg) 

1054 

1055 def _registry_key(self, canonical: str) -> Hashable: 

1056 return canonical if (scope := self._deadlock_scope()) is None else (scope, canonical) 

1057 

1058 @staticmethod 

1059 def _deadlock_scope() -> Hashable | None: 

1060 # Execution unit whose own hold would deadlock a new acquire. 

1061 # 

1062 # ``None`` scopes holders to the thread, which the thread-local registry already separates. Async locks 

1063 # override it with the running task, since one event loop thread runs many tasks and only a reacquire from 

1064 # the *same* task can self-deadlock. 

1065 return None 

1066 

1067 def _pin_lock_path(self, canonical: str) -> None: 

1068 # A path without a final component names no file. Its canonical form is a directory, and pinning that would 

1069 # change the error the backend raises. 

1070 self._context.claim_root = None if canonical.endswith(os.sep) else canonical 

1071 

1072 def _poll_until_acquired( 

1073 self, 

1074 *, 

1075 blocking: bool, 

1076 cancel_check: Callable[[], bool] | None, 

1077 timeout: float, 

1078 poll_interval: float, 

1079 start_time: float, 

1080 ) -> None: 

1081 lock_id = id(self) 

1082 lock_filename = self.lock_file 

1083 attempt = 0 

1084 self._context.acquiring = inspect.currentframe() 

1085 try: 

1086 # Created once, so a directory that disappears while the acquisition waits fails it rather than building a 

1087 # second lock file beside the holder's, and not on re-entry, which joins a hold whose parent may be renamed. 

1088 if not self.is_locked: 

1089 ensure_directory_exists(self._lock_path) 

1090 while True: 

1091 self._raise_if_inherited() 

1092 if not self.is_locked: 

1093 self._try_break_expired_lock() 

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

1095 self._acquire_with_fork_tracking() 

1096 self._raise_if_inherited() 

1097 if self.is_locked: 

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

1099 return 

1100 if self._check_give_up( 

1101 blocking=blocking, 

1102 cancel_check=cancel_check, 

1103 timeout=timeout, 

1104 start_time=start_time, 

1105 ): 

1106 raise Timeout(lock_filename, self._unprobeable_holder()) 

1107 attempt += 1 

1108 delay = self._poll_delay(poll_interval, attempt) 

1109 if timeout >= 0: 

1110 delay = min(delay, max(start_time + timeout - time.perf_counter(), 0.0)) 

1111 _LOGGER.debug("Lock %s not acquired on %s, waiting %s seconds ...", lock_id, lock_filename, delay) 

1112 time.sleep(delay) 

1113 finally: 

1114 self._context.acquiring = None 

1115 

1116 @property 

1117 def _lock_path(self) -> str: 

1118 # The OS resolves a relative lock_file against the working directory at each call. A chdir during an 

1119 # acquisition or a hold would otherwise make the backend wait on, lock or remove a different file. 

1120 return self._context.claim_root or self._context.lock_file 

1121 

1122 def _try_break_expired_lock(self) -> None: 

1123 # Remove the lock file once it is older than :attr:`lifetime`; only an existence lock accepts one. 

1124 ... 

1125 

1126 def _acquire_with_fork_tracking(self) -> None: 

1127 with _fork_transition(self): 

1128 try: 

1129 self._acquire() 

1130 except BaseException as acquisition_error: 

1131 self._rollback_failed_acquire(acquisition_error) 

1132 raise 

1133 try: 

1134 self._register_context_descriptor() 

1135 except BaseException as registration_error: # pragma: needs fork 

1136 self._rollback_failed_registration(registration_error) 

1137 raise 

1138 if self.is_locked: 

1139 self._invoke_on_acquired() 

1140 

1141 @abstractmethod 

1142 def _acquire(self) -> None: 

1143 # If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file. 

1144 raise NotImplementedError 

1145 

1146 def _rollback_failed_acquire(self, acquisition_error: BaseException) -> None: 

1147 if not self.is_locked: 

1148 return 

1149 registration_error: BaseException | None = None 

1150 tracking_error: BaseException | None = None 

1151 try: 

1152 self._register_context_descriptor() 

1153 except BaseException as error: # ruff:ignore[blind-except] # preserve registration and acquisition failures 

1154 registration_error = error 

1155 try: 

1156 # Rollback may fail too; retain the fd so a child can close it without another identity probe. 

1157 self._register_unverified_context_descriptor() 

1158 except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback 

1159 tracking_error = error 

1160 try: 

1161 self._release_with_fork_tracking() 

1162 except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and acquisition failures 

1163 _raise_cleanup_errors( 

1164 "lock acquisition cleanup failed", 

1165 acquisition_error, 

1166 registration_error, 

1167 tracking_error, 

1168 rollback_error, 

1169 ) 

1170 if registration_error is not None: # pragma: needs fork 

1171 _raise_cleanup_errors( 

1172 "lock acquisition cleanup failed", acquisition_error, registration_error, tracking_error 

1173 ) 

1174 

1175 def _register_unverified_context_descriptor(self) -> None: 

1176 # The rollback only reaches here still holding a descriptor it never managed to register. 

1177 if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None: # pragma: no branch 

1178 if not _HAS_REGISTER_AT_FORK: # pragma: lacks fork 

1179 return 

1180 with _fork_transition(): # pragma: needs fork 

1181 self._context.lock_file_fd_token = _record_owned_descriptor(self._context.lock_file_fd, None) 

1182 

1183 def _release_with_fork_tracking(self) -> None: 

1184 with _fork_transition(self): 

1185 try: 

1186 self._release() 

1187 except BaseException as release_error: 

1188 # A backend unlocks first and clears the descriptor second. A signal handler that raises in between 

1189 # leaves a hold the OS dropped. A failed system call alone shows the lock is still held, so it keeps it. 

1190 if self._context.lock_file_fd is not None and not _is_system_call_failure(release_error): 

1191 with contextlib.suppress(Exception): 

1192 self._release() 

1193 raise 

1194 finally: 

1195 self._unregister_released_descriptor() 

1196 

1197 @abstractmethod 

1198 def _release(self) -> None: 

1199 # Releases the lock and sets self._context.lock_file_fd to None. 

1200 raise NotImplementedError 

1201 

1202 def _unregister_released_descriptor(self) -> None: 

1203 if self._context.lock_file_fd is None: 

1204 if (token := self._context.lock_file_fd_token) is not None: # pragma: needs fork 

1205 _unregister_owned_descriptor(token) 

1206 self._context.lock_file_fd_token = None 

1207 self._context.lock_file_fd_identity = None 

1208 

1209 def _register_context_descriptor(self) -> None: 

1210 if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None: 

1211 self._context.lock_file_fd_token = _register_owned_descriptor( 

1212 self._context.lock_file_fd, 

1213 self._context.lock_file_fd_identity, 

1214 ) 

1215 

1216 def _rollback_failed_registration(self, registration_error: BaseException) -> None: # pragma: needs fork 

1217 tracking_error: BaseException | None = None 

1218 try: 

1219 # Rollback may fail too; retain the fd so a child can close it without another identity probe. 

1220 self._register_unverified_context_descriptor() 

1221 except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback 

1222 tracking_error = error 

1223 try: 

1224 self._release_with_fork_tracking() 

1225 except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and registration failures 

1226 _raise_cleanup_errors( 

1227 "descriptor registration cleanup failed", registration_error, tracking_error, rollback_error 

1228 ) 

1229 if tracking_error is not None: # pragma: no cover - requires failed in-memory fallback 

1230 _raise_cleanup_errors("descriptor registration cleanup failed", registration_error, tracking_error) 

1231 

1232 def _invoke_on_acquired(self) -> None: 

1233 # The wrapper runs in the backend executor for async locks, preserving the callback's documented thread. 

1234 if self._on_acquired is None or self._context.lock_counter != 1: 

1235 return 

1236 try: 

1237 self._on_acquired(cast("int", self._context.lock_file_fd)) 

1238 except BaseException as callback_error: # arbitrary caller code; roll back on any failure 

1239 callback_context = callback_error.__context__ 

1240 try: 

1241 self._release_with_fork_tracking() 

1242 except BaseException as release_error: # ruff:ignore[blind-except] # both errors surface via the group below 

1243 _raise_body_and_release(callback_error, release_error) 

1244 callback_error.__context__ = callback_context 

1245 raise 

1246 

1247 def _check_give_up( 

1248 self, 

1249 *, 

1250 blocking: bool, 

1251 cancel_check: Callable[[], bool] | None, 

1252 timeout: float, 

1253 start_time: float, 

1254 ) -> bool: 

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

1256 if blocking is False: 

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

1258 return True 

1259 if cancel_check is not None and cancel_check(): 

1260 _LOGGER.debug("Cancellation requested for lock %s on %s", lock_id, lock_filename) 

1261 return True 

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

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

1264 return True 

1265 return False 

1266 

1267 def _unprobeable_holder(self) -> str | None: # ruff:ignore[no-self-use] # existence locks override it 

1268 return None 

1269 

1270 def _poll_delay(self, poll_interval: float, attempt: int) -> float: 

1271 # A single-file lock retries on a fixed cadence. A backend that publishes several files per acquisition sets a 

1272 # cap, and then contending processes back off across a jittered, exponentially widening window instead of 

1273 # colliding on every poll; poll_interval stays the floor so a lone waiter is still responsive. 

1274 if not self._poll_backoff_cap: 

1275 return poll_interval 

1276 # Cap the exponent before doubling: under heavy contention attempt reaches the thousands, and 2**attempt would 

1277 # overflow the float multiply long before the window itself stops growing past the cap. 

1278 window = min( 

1279 self._poll_backoff_cap, poll_interval * 2 ** min(attempt, _MAX_BACKOFF_EXPONENT) 

1280 ) # pragma: needs hard-link 

1281 return max(poll_interval, secrets.randbelow(int(window * 1_000_000) + 1) / 1_000_000) # pragma: needs hard-link 

1282 

1283 def _reconcile_failed_acquire(self, canonical: str) -> None: 

1284 # An acquire that raised while still holding the native lock (a hook that failed and whose rollback could not 

1285 # release) must keep the registry entry so a later release can retry the OS unlock; otherwise roll the counter 

1286 # back. is_locked was already reconciled by whichever release ran. 

1287 if self.is_locked: 

1288 self._commit_acquire(canonical) 

1289 else: 

1290 self._undo_acquire() 

1291 

1292 def _undo_acquire(self) -> None: 

1293 # Roll back the counter after a failed acquire, dropping the registry entry once nothing holds the path. 

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

1295 if self._context.lock_counter == 0: 

1296 self._drop_registry_entry() 

1297 self._context.claim_root = None 

1298 

1299 def _drop_registry_entry(self) -> None: 

1300 # Forget the key owned by this hold without resolving a mutable path again. 

1301 # Ordered so a run an interrupt cuts short repeats safely: the entry goes only while it is still ours, and the 

1302 # key outlives the registry it indexes, so a later commit sees no registry and records the hold afresh. 

1303 key, registry = self._context.lock_file_key, self._context.lock_file_registry 

1304 if registry is not None and registry.get(key) == self._holder_id: 

1305 del registry[key] 

1306 self._context.lock_file_registry = None 

1307 self._context.lock_file_key = None 

1308 

1309 def _commit_acquire(self, canonical: str) -> None: 

1310 # Record this instance as the holder once the first acquire succeeds, so peers can detect the deadlock. 

1311 # Keyed on the registry rather than the counter: an on_acquired hook that acquires again bumps the counter 

1312 # past 1 before this first commit runs, and must not leave the hold unregistered. 

1313 if self._context.lock_file_registry is None: 

1314 key = self._registry_key(canonical) 

1315 # The holder scope is resolved once at commit so a later release from another flow drops the right entry. 

1316 self._context.lock_file_key = key 

1317 # A shared instance may be released from another thread, which must clear the entry in the acquirer's 

1318 # registry: the releaser's own thread-local registry never held it. 

1319 self._context.lock_file_registry = _REGISTRY.held 

1320 _REGISTRY.held[key] = self._holder_id 

1321 

1322 def __exit__( 

1323 self, 

1324 exc_type: type[BaseException] | None, 

1325 exc_value: BaseException | None, 

1326 traceback: TracebackType | None, 

1327 ) -> None: 

1328 """Release the lock, reconciling a release failure with any body failure per :attr:`context_error_policy`.""" 

1329 self._release_in_context(exc_value) 

1330 

1331 def _release_in_context(self, body_error: BaseException | None) -> None: 

1332 # Release from a context-manager exit. "chain" lets a release failure propagate with the body error already in 

1333 # its __context__ (Python's default); "group" raises both as sibling leaves so neither one hides the other. 

1334 try: 

1335 self.release() 

1336 except BaseException as release_error: 

1337 if body_error is None or self._context_error_policy == "chain": 

1338 raise 

1339 _raise_body_and_release(body_error, release_error) 

1340 

1341 def release(self, force: bool = False) -> None: # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility 

1342 """ 

1343 Release the file lock. The lock is only completely released when the lock counter reaches 0. The lock file 

1344 itself may be deleted automatically, the behavior is platform-specific. 

1345 

1346 :param force: If true, the lock counter is ignored and the lock is released in every case. On a thread-local 

1347 lock it also releases the holds of threads that exited without releasing; a hold of a thread still running 

1348 stays with that thread. Dropping the last reference to the lock releases every thread's hold. 

1349 

1350 """ 

1351 counter_before: int | None = None 

1352 try: 

1353 counter_before = self._context.lock_counter 

1354 self._release_current(force=force) 

1355 except BaseException as release_error: 

1356 # A handler that raised after release() began and before the backend changed anything leaves the counter as 

1357 # it was; only then does a second attempt release the hold the caller gave up. A failed system call keeps it 

1358 # for an explicit retry, and a changed counter means a nested level or the backend's own retry already ran. 

1359 if ( 

1360 counter_before is not None 

1361 and counter_before == self._context.lock_counter 

1362 and self._context.lock_file_fd is not None 

1363 and not _is_system_call_failure(release_error) 

1364 ): 

1365 with contextlib.suppress(Exception): 

1366 self._release_current(force=force) 

1367 raise 

1368 if force: 

1369 self._release_thread_holds(exited_only=True) 

1370 

1371 def _release_current(self, *, force: bool) -> None: 

1372 # A shared instance releases under the same gate its acquisition ran through, so a thread entering the lock 

1373 # never observes a partially torn-down owner. Only an acquirer that found the lock free holds the gate for 

1374 # long, so an unheld lock returns without waiting behind it. 

1375 if self._context.lock_file_fd is None: 

1376 # Only a committed hold registers, so an entry without a descriptor is a release a signal handler stopped 

1377 # after the unlock; left alone it refuses every other instance on this path in this thread. 

1378 if self._context.lock_file_registry is not None: 

1379 self._finish_interrupted_release() 

1380 return 

1381 with contextlib.nullcontext() if self._is_thread_local else self._transition_lock: 

1382 state: Final = self._hold_state() 

1383 # A signal handler can land between the backend's unlock and its clearing of the descriptor, where a second 

1384 # release would close the number twice, maybe hitting a file that reused it. The flag holds the releasing 

1385 # frame, so only a release still on this stack stops it, and a flag a cut-short frame left set does not. 

1386 if self._creator_pid != os.getpid() or state.lock_file_fd is None or _on_this_stack(state.releasing): 

1387 return 

1388 if not force and state.lock_counter > 1: 

1389 state.lock_counter -= 1 

1390 return 

1391 state.releasing = inspect.currentframe() 

1392 try: 

1393 _LOGGER.debug("Attempting to release lock %s on %s", id(self), self.lock_file) 

1394 self._release_with_fork_tracking() 

1395 finally: 

1396 # A failure after the OS unlock (during close or unlink) still released the lock: the backend cleared 

1397 # its descriptor, so commit the counter and registry to released even as the cleanup error propagates. 

1398 # A failure that left the lock held keeps the counter so a later release can retry the OS unlock. 

1399 if state.lock_file_fd is None: # pragma: no branch # a release that returns cleared the descriptor 

1400 self._commit_release() 

1401 state.releasing = None 

1402 _LOGGER.debug("Lock %s released on %s", id(self), self.lock_file) 

1403 

1404 def _finish_interrupted_release(self) -> None: 

1405 # An unheld lock never waits behind an acquirer, whose own commit takes the entry over anyway. 

1406 if not self._transition_lock.acquire(blocking=False): 

1407 return # pragma: no cover # only a concurrent acquire of a shared instance holds the gate 

1408 try: 

1409 if self._context.lock_file_fd is None: # pragma: no branch # only a racing acquire fills it 

1410 self._commit_release() 

1411 finally: 

1412 self._transition_lock.release() 

1413 

1414 def _hold_state(self) -> FileLockContext: 

1415 return self._context.current if isinstance(self._context, ThreadLocalFileContext) else self._context 

1416 

1417 def _commit_release(self) -> None: 

1418 # Record the lock as fully released: reset the recursion counter and drop the deadlock-registry entry. 

1419 self._context.lock_counter = 0 

1420 self._drop_registry_entry() 

1421 self._context.claim_root = None 

1422 

1423 def _release_thread_holds(self, *, exited_only: bool) -> None: 

1424 if not isinstance(context := self._context, ThreadLocalFileContext): 

1425 return 

1426 # Serialized, or two threads would both close an exited thread's descriptor; copy() is atomic free-threaded. 

1427 with self._transition_lock: 

1428 own: Final = context.current 

1429 for held, thread in self._thread_holds.copy().values(): 

1430 if exited_only and thread.is_alive(): 

1431 continue 

1432 context.current = held 

1433 try: 

1434 self._release_current(force=True) 

1435 finally: 

1436 context.current = own 

1437 

1438 def __del__(self) -> None: 

1439 """Force-release so a dropped reference never leaks a held lock, including every thread's hold.""" 

1440 if self.__dict__.get("_creator_pid") != os.getpid(): 

1441 return # pragma: forked child 

1442 # CPython runs __del__ before it clears weak references. Until then the singleton cache still hands this lock 

1443 # out, and a thread that takes a hold through it would lose that hold to a release here. The cycle collector 

1444 # clears the references first, and at exit no other thread runs. 

1445 if (key := self._singleton_key) is not None and not sys.is_finalizing() and self._instances.get(key) is self: 

1446 self._release_once_unreachable() 

1447 else: 

1448 self._release_abandoned_holds() 

1449 

1450 def _release_once_unreachable(self) -> None: 

1451 # The callback runs once the lock is gone, so it reads the state through a holder. The state alone keeps the 

1452 # holder alive: a reference from the callback would keep the state, and through it the lock, alive. 

1453 self._finalizer_state = _FinalizerState(state=self.__dict__) 

1454 _UNREACHABLE_LOCKS.add(ref(self, partial(_finalize_unreachable, ref(self._finalizer_state), type(self)))) 

1455 

1456 def _release_abandoned_holds(self) -> None: 

1457 # A finalizer must not raise. A release error during garbage collection would otherwise surface as an 

1458 # unraisable-exception warning, attributed to whichever code triggered collection. The dropped lock still gets 

1459 # best-effort cleanup; an explicit release() reports the same error to a caller who can act on it. 

1460 with contextlib.suppress(Exception): 

1461 self.release(force=True) 

1462 # Nothing references the lock any more, so no thread can still be inside it through this instance. 

1463 with contextlib.suppress(Exception): 

1464 self._release_thread_holds(exited_only=False) 

1465 

1466 @property 

1467 def is_singleton(self) -> bool: 

1468 """ 

1469 A flag indicating if this lock is singleton or not. 

1470 

1471 .. versionadded:: 3.13.0 

1472 

1473 """ 

1474 return self._is_singleton 

1475 

1476 @property 

1477 def context_error_policy(self) -> ContextErrorPolicy: 

1478 """ 

1479 How a context manager reconciles a body failure with a release failure on exit. 

1480 

1481 .. versionadded:: 3.30.0 

1482 

1483 """ 

1484 return self._context_error_policy 

1485 

1486 @property 

1487 def close_error_policy(self) -> CloseErrorPolicy: 

1488 """ 

1489 What a lock does with an ``os.close`` failure after relinquishing descriptor ownership. 

1490 

1491 .. versionadded:: 3.30.0 

1492 

1493 """ 

1494 return self._close_error_policy 

1495 

1496 @property 

1497 def fallback_to_soft(self) -> bool: 

1498 """ 

1499 Whether a :class:`FileLock` falls back to :class:`SoftFileLock` when the filesystem lacks ``flock``. 

1500 

1501 Only :class:`UnixFileLock` acts on it: when ``False`` an ``ENOSYS`` from ``flock`` propagates instead of 

1502 switching to existence-lock semantics. 

1503 

1504 .. versionadded:: 3.30.0 

1505 

1506 """ 

1507 return self._fallback_to_soft 

1508 

1509 @property 

1510 def preserve_lock_file(self) -> bool: 

1511 """ 

1512 Whether filelock promises not to unlink the lock pathname on release. 

1513 

1514 When ``True``, Windows skips its post-release unlink and Unix refuses the ``ENOSYS`` soft fallback. 

1515 :class:`SoftFileLock` rejects ``True`` because unlinking its marker is how it releases. 

1516 

1517 .. versionadded:: 3.30.0 

1518 

1519 """ 

1520 return self._preserve_lock_file 

1521 

1522 @property 

1523 def on_acquired(self) -> Callable[[int], None] | None: 

1524 """ 

1525 The callback run with the borrowed lock descriptor once per physical acquisition, or ``None``. 

1526 

1527 Native locks only. It runs after the native lock is held and backend initialization finished, before 

1528 :meth:`~BaseFileLock.acquire` returns; a raise rolls back the acquisition. :class:`SoftFileLock` rejects it. 

1529 

1530 .. versionadded:: 3.30.0 

1531 

1532 """ 

1533 return self._on_acquired 

1534 

1535 @property 

1536 def timeout(self) -> float: 

1537 """ 

1538 The default timeout value, in seconds. 

1539 

1540 .. versionadded:: 2.0.0 

1541 

1542 """ 

1543 return self._context.timeout 

1544 

1545 @timeout.setter 

1546 def timeout(self, value: float | str | bytes | bytearray) -> None: 

1547 """ 

1548 Change the default timeout value. 

1549 

1550 :param value: the new value, in seconds; a negative value or ``inf`` waits without a limit 

1551 

1552 :raises TypeError: if *value* is a :class:`bool` or not a real number 

1553 :raises ValueError: if *value* is too large to convert to :class:`float` 

1554 

1555 .. deprecated:: 4.0.13 

1556 

1557 Passing a numeric string; pass the number instead. 

1558 

1559 """ 

1560 if isinstance(value, (str, bytes, bytearray)): 

1561 warnings.warn( 

1562 "setting timeout from a string is deprecated and will be removed; pass a number", 

1563 DeprecationWarning, 

1564 stacklevel=2, 

1565 ) 

1566 value = float(value) 

1567 self._context.timeout = _seconds("timeout", value) 

1568 

1569 @property 

1570 def blocking(self) -> bool: 

1571 """ 

1572 Whether the locking is blocking or not. 

1573 

1574 .. versionadded:: 3.14.0 

1575 

1576 """ 

1577 return self._context.blocking 

1578 

1579 @blocking.setter 

1580 def blocking(self, value: bool) -> None: 

1581 """ 

1582 Change the default blocking value. 

1583 

1584 :param value: the new value as bool 

1585 

1586 :raises TypeError: if *value* is not a bool 

1587 

1588 """ 

1589 _check_blocking(value) 

1590 self._context.blocking = value 

1591 

1592 @property 

1593 def poll_interval(self) -> float: 

1594 """ 

1595 The default polling interval, in seconds. 

1596 

1597 .. versionadded:: 3.24.0 

1598 

1599 """ 

1600 return self._context.poll_interval 

1601 

1602 @poll_interval.setter 

1603 def poll_interval(self, value: float) -> None: 

1604 """ 

1605 Change the default polling interval. 

1606 

1607 :param value: the new value, in seconds 

1608 

1609 :raises ValueError: if *value* is negative, not finite, or above :data:`threading.TIMEOUT_MAX` 

1610 :raises TypeError: if *value* is a :class:`bool` or not a real number 

1611 

1612 """ 

1613 self._context.poll_interval = _resolve_poll_interval(value) 

1614 

1615 @property 

1616 def lifetime(self) -> float | None: 

1617 """ 

1618 The soft marker age in seconds that permits expiry, or ``None`` to disable age-based expiry. 

1619 

1620 A non-``None`` value permits a waiter to enter while the previous holder remains active, so it does not provide 

1621 strict mutual exclusion. Native locks ignore the value with a warning. 

1622 

1623 .. versionadded:: 3.24.0 

1624 

1625 """ 

1626 return self._context.lifetime 

1627 

1628 @lifetime.setter 

1629 def lifetime(self, value: float | None) -> None: 

1630 """ 

1631 Change the legacy age-based expiry threshold. 

1632 

1633 :param value: the new value in seconds, or ``None`` to disable expiration 

1634 

1635 :raises ValueError: if *value* is negative or not finite 

1636 :raises TypeError: if *value* is not ``None`` and not a real number 

1637 

1638 """ 

1639 self._context.lifetime = _resolve_lifetime(value, type(self), stacklevel=3) 

1640 

1641 @property 

1642 def mode(self) -> int: 

1643 """ 

1644 The file permissions requested for the lock file. 

1645 

1646 Without an explicit ``mode`` this reports ``0o644`` for backwards compatibility, but the file is created with 

1647 ``0o666`` filtered through the umask and default ACLs, so with a ``002`` umask it ends up ``0o664``. Check 

1648 :attr:`has_explicit_mode` before relying on the value, or ``os.stat`` the file for its actual permissions. 

1649 

1650 """ 

1651 return 0o644 if self._context.mode == _UNSET_FILE_MODE else self._context.mode 

1652 

1653 @property 

1654 def has_explicit_mode(self) -> bool: 

1655 """Whether the file permissions were explicitly set.""" 

1656 return self._context.mode != _UNSET_FILE_MODE 

1657 

1658 @property 

1659 def lock_counter(self) -> int: 

1660 """The number of times this lock has been acquired (but not yet released).""" 

1661 _ensure_current_process() 

1662 return self._context.lock_counter 

1663 

1664 @classmethod 

1665 def _reset_class_after_fork(cls) -> None: # pragma: forked child 

1666 cls._instances = WeakValueDictionary() 

1667 cls._instances_lock = RLock() 

1668 cls._instances_under_construction = set() 

1669 

1670 def _singleton_extra_mismatches( # ruff:ignore[no-self-use] # the base class adds no options of its own 

1671 self, _kwargs: Mapping[str, _ExtraValue], / 

1672 ) -> dict[str, tuple[str, str]]: 

1673 # A subclass's own options reach the metaclass only as kwargs, and only the subclass knows their defaults and 

1674 # how __init__ resolves them, so each override compares its options and merges what super() reports. 

1675 return {} 

1676 

1677 def is_thread_local(self) -> bool: 

1678 """:returns: a flag indicating if this lock is thread local or not""" 

1679 return self._is_thread_local 

1680 

1681 def _close_released_fd(self, fd: int, *, default_suppresses: bool) -> None: 

1682 # CPython never retries close() after EINTR because the descriptor number may already be reused, so neither does 

1683 # this. close_error_policy decides the error's fate after the backend relinquishes descriptor ownership. 

1684 try: 

1685 os.close(fd) 

1686 except OSError: 

1687 if not self._suppresses_close_error(default_suppresses=default_suppresses): 

1688 raise 

1689 

1690 def _suppresses_close_error(self, *, default_suppresses: bool) -> bool: 

1691 return self._close_error_policy == "suppress" or (self._close_error_policy == "default" and default_suppresses) 

1692 

1693 def _open_mode(self) -> int: 

1694 # Mode for ``os.open``: 0o666 when unset so umask and ACLs decide, otherwise the explicit mode. 

1695 return 0o666 if self._context.mode == _UNSET_FILE_MODE else self._context.mode 

1696 

1697 def _mark_descriptor_owned(self, fd: int, identity: tuple[int, int] | None = None) -> None: 

1698 state: Final = self._hold_state() 

1699 # Held first, pending cleared second: an interrupt between the two then leaves the descriptor recorded. 

1700 state.lock_file_fd = fd 

1701 state.lock_file_fd_identity = identity 

1702 state.pending_lock_file_fd = None 

1703 state.pending_lock_file_fd_identity = None 

1704 if isinstance(self._context, ThreadLocalFileContext): 

1705 self._thread_pending.pop(id(state), None) 

1706 self._thread_holds[id(state)] = state, current_thread() 

1707 

1708 def _mark_descriptor_pending(self, fd: int, identity: tuple[int, int] | None = None) -> None: 

1709 state: Final = self._hold_state() 

1710 state.pending_lock_file_fd = fd 

1711 state.pending_lock_file_fd_identity = identity 

1712 if isinstance(self._context, ThreadLocalFileContext): 

1713 self._thread_pending[id(state)] = state, current_thread() 

1714 

1715 def _mark_descriptor_released(self) -> None: 

1716 state: Final = self._hold_state() 

1717 state.pending_lock_file_fd = None 

1718 state.pending_lock_file_fd_identity = None 

1719 state.lock_file_fd = None 

1720 state.lock_file_fd_identity = None 

1721 if isinstance(self._context, ThreadLocalFileContext): 

1722 self._thread_pending.pop(id(state), None) 

1723 self._thread_holds.pop(id(state), None) 

1724 

1725 def _reset_after_fork_in_child(self) -> None: # pragma: forked child 

1726 # fork copies the lock in whatever state the parent's threads left it, so give the child an unheld one. 

1727 self._transition_lock = RLock() 

1728 if isinstance(self._context, ThreadLocalFileContext): 

1729 self._thread_holds.clear() 

1730 self._thread_pending.clear() 

1731 self._context.owner_claim_paths = () 

1732 self._context.claim_root = None 

1733 self._context.lock_file_fd = None 

1734 self._context.lock_file_fd_token = None 

1735 self._context.lock_file_fd_identity = None 

1736 self._context.pending_lock_file_fd = None 

1737 self._context.pending_lock_file_fd_identity = None 

1738 self._context.lock_counter = 0 

1739 self._context.releasing = None 

1740 self._context.acquiring = None 

1741 self._context.lock_file_key = None 

1742 self._context.lock_file_registry = None 

1743 self._context.lease_claim = None 

1744 

1745 def _descriptors_for_fork(self) -> tuple[tuple[int, tuple[int, int] | None], ...]: # pragma: needs fork 

1746 descriptors: list[tuple[int, tuple[int, int] | None]] = [] 

1747 if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None: 

1748 descriptors.append((self._context.lock_file_fd, self._context.lock_file_fd_identity)) 

1749 if self._context.pending_lock_file_fd is not None: 

1750 descriptors.append((self._context.pending_lock_file_fd, self._context.pending_lock_file_fd_identity)) 

1751 return tuple(descriptors) 

1752 

1753 

1754def _resolve_read_write_timeout(timeout: float, *, blocking: bool) -> float: 

1755 # 0 would still get SQLite's full busy timeout, and a truthy "false" would turn one attempt into a blocking wait. 

1756 _check_blocking(blocking) 

1757 if (timeout := _resolve_timeout(timeout, blocking=blocking)) < 0 and timeout != -1 and blocking: 

1758 msg = f"timeout must be a non-negative number or -1, not {timeout!r}" 

1759 raise ValueError(msg) 

1760 return timeout 

1761 

1762 

1763def _acquire_thread_lock( 

1764 lock: Lock, *, blocking: bool, timeout: float, poll_interval: float, cancel_check: Callable[[], bool] | None 

1765) -> bool: 

1766 # Take a read-write lock's in-process lock; with *cancel_check*, ask it every *poll_interval* while waiting. 

1767 if not blocking: 

1768 return lock.acquire(blocking=False) 

1769 if cancel_check is None: 

1770 # Lock.acquire raises OverflowError past TIMEOUT_MAX, which is 49.7 days on Windows. 

1771 return lock.acquire() if timeout == -1 else lock.acquire(timeout=min(timeout, TIMEOUT_MAX)) 

1772 deadline = None if timeout == -1 else time.perf_counter() + timeout 

1773 while not lock.acquire( 

1774 timeout=poll_interval if deadline is None else min(poll_interval, max(deadline - time.perf_counter(), 0)) 

1775 ): 

1776 if cancel_check() or (deadline is not None and time.perf_counter() >= deadline): 

1777 return False 

1778 return True 

1779 

1780 

1781def _resolve_timeout(timeout: float, *, blocking: bool) -> float: 

1782 # Validate an acquire ``timeout``; every negative value waits without a limit. 

1783 if math.isnan(timeout := _seconds("timeout", timeout)) and blocking: 

1784 msg = "timeout must be a number of seconds, not nan" 

1785 raise ValueError(msg) 

1786 # The fail-fast deadlock check only sees a negative timeout as unlimited, so spell inf that way. 

1787 return -1 if timeout == math.inf else timeout 

1788 

1789 

1790@dataclass( # simplint: ignore[SMP701] # state is reset to None on finalize 

1791 slots=True, weakref_slot=True, kw_only=True 

1792) 

1793class _FinalizerState: 

1794 # A finalized lock's state, held for the release that runs once the lock is unreachable. 

1795 state: dict[str, object] | None # simplint: ignore[SMP602] # the instance __dict__ holds any value 

1796 

1797 

1798#: Weak references whose callback finishes a singleton's finalizer once the cache no longer finds it. 

1799_UNREACHABLE_LOCKS: Final[set[ref[BaseFileLock]]] = set() 

1800 

1801 

1802def _finalize_unreachable(holder: ref[_FinalizerState], cls: type[BaseFileLock], reference: ref[BaseFileLock]) -> None: 

1803 _UNREACHABLE_LOCKS.discard(reference) 

1804 if (finalizer_state := holder()) is None or (state := finalizer_state.state) is None: 

1805 return # pragma: no cover # only a collector freeing a resurrected lock as part of a cycle drops the holder 

1806 # The lock's dictionary points back at the holder; that cycle ends here. 

1807 finalizer_state.state = None 

1808 # The release methods only read and write this state, so a fresh instance takes it over. Dropping that instance 

1809 # runs __del__ again, and the cache no longer finds the lock. 

1810 stand_in = object.__new__(cls) 

1811 stand_in.__dict__ = state 

1812 del stand_in 

1813 

1814 

1815def _is_system_call_failure(error: BaseException) -> bool: 

1816 # A failed system call sets errno. A bare TimeoutError raised by a signal handler is an OSError without one. 

1817 if _is_exception_group(error): 

1818 return all(map(_is_system_call_failure, cast("_ExceptionGroupProtocol", error).exceptions)) 

1819 return isinstance(error, OSError) and error.errno is not None 

1820 

1821 

1822def _on_this_stack(frame: FrameType | None) -> bool: 

1823 if frame is None: 

1824 return False 

1825 caller = inspect.currentframe() 

1826 while caller is not None and caller is not frame: 

1827 caller = caller.f_back 

1828 return caller is not None 

1829 

1830 

1831# acquire() returns this wrapper instead of self so entering the with-statement does not call __enter__ a second 

1832# time; returning self would re-acquire the lock in BaseFileLock.__enter__ without a matching release (issue #37). 

1833class AcquireReturnProxy(Generic[_LockT]): 

1834 """ 

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

1836 

1837 :param lock: the lock to release on exit 

1838 """ 

1839 

1840 def __init__(self, lock: _LockT) -> None: 

1841 self.lock: _LockT = lock 

1842 

1843 def __enter__(self) -> _LockT: 

1844 return self.lock 

1845 

1846 def __exit__( 

1847 self, 

1848 exc_type: type[BaseException] | None, 

1849 exc_value: BaseException | None, 

1850 traceback: TracebackType | None, 

1851 ) -> None: 

1852 # Widen to the bound: ty narrows the else branch to the type variable minus BaseFileLock, not to the union rest. 

1853 lock: Final = cast("BaseFileLock | ReadWriteLock | SoftReadWriteLock", self.lock) 

1854 if isinstance(lock, BaseFileLock): 

1855 lock._release_in_context(exc_value) # ruff:ignore[private-member-access] # forwards __exit__ to the owned lock's context release 

1856 else: # a reader/writer lock does not carry a context_error_policy 

1857 lock.release() 

1858 

1859 

1860@dataclass # simplint: ignore[SMP702, SMP703] # local base rejects slots; public async context signature 

1861class FileLockContext: 

1862 # Holds the context for a ``BaseFileLock`` object. 

1863 # A separate class so ThreadLocalFileContext can make the whole context thread-local. 

1864 

1865 lock_file: str 

1866 timeout: float 

1867 mode: int 

1868 blocking: bool 

1869 poll_interval: float 

1870 

1871 #: The lock lifetime in seconds; ``None`` means the lock never expires. 

1872 lifetime: float | None = None 

1873 

1874 #: File descriptor from os.open for the lock file; not None while the lock is held. 

1875 lock_file_fd: int | None = None 

1876 

1877 #: Registry token for the descriptor owned by this thread's context. 

1878 lock_file_fd_token: int | None = None 

1879 

1880 #: Identity captured by a backend that already inspected the descriptor. 

1881 lock_file_fd_identity: tuple[int, int] | None = None 

1882 

1883 #: Descriptor opened by a backend but not yet committed as the held lock. 

1884 pending_lock_file_fd: int | None = None 

1885 

1886 #: Identity captured for a descriptor whose acquisition has not committed. 

1887 pending_lock_file_fd_identity: tuple[int, int] | None = None 

1888 

1889 #: Depth of nested acquisitions; the lock is released only when it returns to 0. 

1890 lock_counter: int = 0 

1891 

1892 #: Canonical registry key captured when the first physical acquisition commits. 

1893 lock_file_key: Hashable | None = None 

1894 

1895 #: The acquiring thread's deadlock registry holding :attr:`lock_file_key`. 

1896 lock_file_registry: dict[Hashable, int] | None = None 

1897 

1898 #: Claim pathnames this owner published, removed by name on release so no holder ever unlinks a peer's claim. 

1899 owner_claim_paths: tuple[str, ...] = () 

1900 

1901 #: The frame of the :meth:`BaseFileLock.release` tearing this hold down, so a re-entrant call from a signal handler 

1902 #: on the same thread returns instead of closing the descriptor a second time. 

1903 releasing: FrameType | None = None 

1904 

1905 #: The frame of an acquire still polling for this hold, so one a signal handler starts on this thread fails fast. 

1906 acquiring: FrameType | None = None 

1907 

1908 #: Canonical lock path, resolved when an acquisition starts and kept until its hold ends. A waiter on a relative 

1909 #: path stays in the directory it started in, and the release finds the file there, after a chdir in between. 

1910 claim_root: str | None = None 

1911 

1912 #: The lease claim of this hold, kept with it so a cross-thread release stops the right heartbeat. 

1913 lease_claim: _LeaseClaim | None = None 

1914 

1915 

1916class ThreadLocalFileContext(FileLockContext, local): 

1917 # A thread local version of the ``FileLockContext`` class. 

1918 # 

1919 # Each thread's state lives in an ordinary :class:`FileLockContext` at :attr:`current`, which the lock also keeps, 

1920 # so 

1921 # ``release(force=True)`` and the finalizer can unlock a hold whose thread exited. 

1922 current: FileLockContext 

1923 

1924 # threading.local runs __init__ again with these arguments the first time each thread touches the context. 

1925 def __init__( # ruff:ignore[too-many-arguments] # mirrors the FileLockContext fields set at construction 

1926 self, 

1927 *, 

1928 lock_file: str, 

1929 timeout: float, 

1930 mode: int, 

1931 blocking: bool, 

1932 poll_interval: float, 

1933 lifetime: float | None = None, 

1934 ) -> None: 

1935 self.current = FileLockContext( 

1936 lock_file=lock_file, 

1937 timeout=timeout, 

1938 mode=mode, 

1939 blocking=blocking, 

1940 poll_interval=poll_interval, 

1941 lifetime=lifetime, 

1942 ) 

1943 

1944 

1945def _delegate_to_current(name: str) -> property: 

1946 return property( 

1947 lambda context: getattr(context.current, name), 

1948 lambda context, value: setattr(context.current, name, value), 

1949 ) 

1950 

1951 

1952for _field in fields(FileLockContext): 

1953 setattr(ThreadLocalFileContext, _field.name, _delegate_to_current(_field.name)) 

1954 

1955 

1956@dataclass(frozen=True, slots=True, kw_only=True) 

1957class _OwnedDescriptor: 

1958 fd: int 

1959 creator_pid: int 

1960 device: int | None 

1961 inode: int | None 

1962 

1963 

1964class _ForkState: 

1965 def __init__(self) -> None: 

1966 self.gate = Condition(RLock()) 

1967 self.registry_lock = RLock() 

1968 self.parameter_models_lock = RLock() 

1969 self.transitions: dict[int, dict[int, _ForkDescriptorOwner | None]] = {} 

1970 self.active_transitions = 0 

1971 self.admission_closed = False 

1972 self.fork_owner_depths: dict[int, int] = {} 

1973 self.pinned_objects: dict[int, list[tuple[_ForkResettable, ...]]] = {} 

1974 self.pinned_classes: dict[int, list[tuple[_ForkResettableClass, ...]]] = {} 

1975 self.provisional_descriptor_tokens: dict[int, list[tuple[int, ...]]] = {} 

1976 self.pid = os.getpid() 

1977 

1978 def reset_synchronization(self) -> None: # pragma: forked child 

1979 self.gate = Condition(RLock()) 

1980 self.registry_lock = RLock() 

1981 self.parameter_models_lock = RLock() 

1982 self.transitions = {} 

1983 self.active_transitions = 0 

1984 self.admission_closed = False 

1985 self.fork_owner_depths = {} 

1986 self.pinned_objects = {} 

1987 self.pinned_classes = {} 

1988 self.provisional_descriptor_tokens = {} 

1989 

1990 

1991_FORK_OBJECTS: Final[WeakValueDictionary[int, _ForkResettable]] = WeakValueDictionary() 

1992_FORK_CLASSES: Final[WeakValueDictionary[int, _ForkResettableClass]] = WeakValueDictionary() 

1993_OWNED_DESCRIPTORS: Final[dict[int, _OwnedDescriptor]] = {} 

1994_DESCRIPTOR_TOKENS: Final[count[int]] = count() 

1995_TRANSITION_TOKENS: Final[count[int]] = count() 

1996_FORK_STATE: Final[_ForkState] = _ForkState() 

1997_FORK_AUDIT_EVENTS: Final[frozenset[str]] = frozenset({"os.fork", "os.forkpty"}) 

1998# CPython before 3.12 guards sys.settrace with one process-wide flag that stays set while audit hooks run, so a thread 

1999# switch inside a Python hook makes a concurrent settrace raise. Coverage and debuggers call it on every thread start. 

2000_SETTRACE_SURVIVES_AUDIT_HOOKS: Final[bool] = sys.implementation.name != "cpython" or sys.version_info >= (3, 12) 

2001 

2002 

2003@cache 

2004def _resolve_lazy_imports_before_shutdown() -> None: 

2005 if hasattr(types, "LazyImportType"): # pragma: >=3.15 cover 

2006 atexit.register(_resolve_lazy_imports) 

2007 

2008 

2009def _resolve_lazy_imports() -> None: # pragma: >=3.15 cover 

2010 # Interpreter shutdown clears sys.modules and sys.meta_path before it finalizes a lock still alive at exit, so a 

2011 # lazy import (PEP 810) that the release path in __del__ reaches first can no longer load. Load them while it can. 

2012 for name, module in list(sys.modules.items()): 

2013 if name == "filelock" or name.startswith("filelock."): 

2014 for attribute, value in list(module.__dict__.items()): 

2015 if type(value) is types.LazyImportType: 

2016 getattr(module, attribute) 

2017 

2018 

2019def _register_fork_hooks() -> None: 

2020 if _REGISTER_AT_FORK is None: 

2021 return # pragma: lacks fork 

2022 if _SETTRACE_SURVIVES_AUDIT_HOOKS: # pragma: needs fork # pragma: needs settrace-safe-audit-hooks 

2023 sys.addaudithook(_audit_fork_safety) 

2024 _REGISTER_AT_FORK( # pragma: needs fork 

2025 before=_pin_fork_objects, 

2026 after_in_parent=_resume_parent_after_fork, 

2027 after_in_child=_reset_child_after_fork, 

2028 ) 

2029 

2030 

2031@contextmanager 

2032def _fork_transition(descriptor_owner: _ForkDescriptorOwner | None = None) -> Generator[None]: 

2033 # No fork follows exit, and a daemon thread stopped inside this bookkeeping holds its lock forever. 

2034 if not _HAS_REGISTER_AT_FORK or sys.is_finalizing(): 

2035 yield # pragma: lacks fork 

2036 return # pragma: lacks fork 

2037 _ensure_current_process() # pragma: needs fork 

2038 creator_pid = os.getpid() # pragma: needs fork 

2039 # A generator holding this transition can be finalized on another thread, which must close the entering thread's. 

2040 thread_id: Final = get_ident() # pragma: needs fork 

2041 with _FORK_STATE.gate: # pragma: needs fork 

2042 while ( 

2043 _FORK_STATE.admission_closed 

2044 and thread_id not in _FORK_STATE.fork_owner_depths 

2045 and thread_id not in _FORK_STATE.transitions 

2046 ): 

2047 _FORK_STATE.gate.wait() # pragma: no cover - exercised in isolated interpreter 

2048 token: Final = next(_TRANSITION_TOKENS) 

2049 _FORK_STATE.transitions.setdefault(thread_id, {})[token] = descriptor_owner 

2050 _FORK_STATE.active_transitions += 1 

2051 try: # pragma: needs fork 

2052 yield 

2053 finally: 

2054 if os.getpid() == creator_pid: # pragma: needs fork 

2055 with _FORK_STATE.gate: 

2056 owned: Final = _FORK_STATE.transitions[thread_id] 

2057 del owned[token] 

2058 if not owned: 

2059 del _FORK_STATE.transitions[thread_id] 

2060 _FORK_STATE.active_transitions -= 1 

2061 _FORK_STATE.gate.notify_all() 

2062 

2063 

2064def _register_fork_object(instance: _ForkResettable) -> None: 

2065 if not _HAS_REGISTER_AT_FORK: 

2066 return # pragma: lacks fork 

2067 with _fork_transition(), _FORK_STATE.registry_lock: # pragma: needs fork 

2068 _FORK_OBJECTS[id(instance)] = instance 

2069 _refresh_owner_pins() 

2070 

2071 

2072def _register_fork_class(cls: _ForkResettableClass) -> None: 

2073 if not _HAS_REGISTER_AT_FORK: 

2074 return # pragma: lacks fork 

2075 with _fork_transition(), _FORK_STATE.registry_lock: # pragma: needs fork 

2076 _FORK_CLASSES[id(cls)] = cls 

2077 _refresh_owner_pins() 

2078 

2079 

2080def _register_owned_descriptor(fd: int, identity: tuple[int, int] | None) -> int | None: 

2081 if not _HAS_REGISTER_AT_FORK: 

2082 return None # pragma: lacks fork 

2083 with _fork_transition(): # pragma: needs fork 

2084 if identity is None: 

2085 stat_result = os.fstat(fd) 

2086 identity = stat_result.st_dev, stat_result.st_ino 

2087 return _record_owned_descriptor(fd, identity) 

2088 

2089 

2090def _record_owned_descriptor(fd: int, identity: tuple[int, int] | None) -> int: # pragma: needs fork 

2091 with _FORK_STATE.registry_lock: 

2092 token = next(_DESCRIPTOR_TOKENS) 

2093 _OWNED_DESCRIPTORS[token] = _OwnedDescriptor( 

2094 fd=fd, 

2095 creator_pid=os.getpid(), 

2096 device=None if identity is None else identity[0], 

2097 inode=None if identity is None else identity[1], 

2098 ) 

2099 return token 

2100 

2101 

2102def _unregister_owned_descriptor(token: int) -> None: # pragma: needs fork 

2103 # No fork follows exit, and a daemon thread stopped inside this bookkeeping holds its lock forever. 

2104 if sys.is_finalizing(): # pragma: no cover # only exit reaches it, after coverage stops measuring 

2105 return 

2106 with _fork_transition(), _FORK_STATE.registry_lock: 

2107 _OWNED_DESCRIPTORS.pop(token, None) 

2108 

2109 

2110def _pin_fork_objects() -> None: # pragma: needs fork 

2111 _ensure_current_process() 

2112 thread_id = get_ident() 

2113 provisional_descriptor_tokens: list[int] = [] 

2114 # A signal handler that raises during the wait does not stop the fork. The after hooks pop one snapshot per 

2115 # admission closed here, so the finally records the snapshot on failure too. 

2116 try: 

2117 with _FORK_STATE.gate: 

2118 _FORK_STATE.fork_owner_depths[thread_id] = _FORK_STATE.fork_owner_depths.get(thread_id, 0) + 1 

2119 _FORK_STATE.admission_closed = True 

2120 while _FORK_STATE.active_transitions > len(_FORK_STATE.transitions.get(thread_id, ())): 

2121 _FORK_STATE.gate.wait() 

2122 transition_owners = tuple(_FORK_STATE.transitions.get(thread_id, {}).values()) 

2123 with _FORK_STATE.registry_lock: 

2124 for token, descriptor in tuple(_OWNED_DESCRIPTORS.items()): 

2125 if descriptor.creator_pid != os.getpid() or descriptor.device is not None: 

2126 continue 

2127 try: 

2128 stat_result = os.fstat(descriptor.fd) 

2129 except OSError: 

2130 continue 

2131 _OWNED_DESCRIPTORS[token] = _OwnedDescriptor( 

2132 fd=descriptor.fd, 

2133 creator_pid=descriptor.creator_pid, 

2134 device=stat_result.st_dev, 

2135 inode=stat_result.st_ino, 

2136 ) 

2137 owners = {id(owner): owner for owner in transition_owners if owner is not None} 

2138 provisional_descriptor_tokens.extend( 

2139 starmap( 

2140 _snapshot_descriptor_for_fork, 

2141 (descriptor for owner in owners.values() for descriptor in owner._descriptors_for_fork()), # ruff:ignore[private-member-access] # snapshots each owner's own fork descriptors 

2142 ) 

2143 ) 

2144 finally: 

2145 with _FORK_STATE.registry_lock: 

2146 _FORK_STATE.provisional_descriptor_tokens.setdefault(thread_id, []).append( 

2147 tuple(provisional_descriptor_tokens) 

2148 ) 

2149 _FORK_STATE.pinned_objects.setdefault(thread_id, []).append(tuple(_FORK_OBJECTS.values())) 

2150 _FORK_STATE.pinned_classes.setdefault(thread_id, []).append(tuple(_FORK_CLASSES.values())) 

2151 

2152 

2153def _resume_parent_after_fork() -> None: # pragma: needs fork 

2154 thread_id = get_ident() 

2155 try: 

2156 with _FORK_STATE.registry_lock: 

2157 for token in _FORK_STATE.provisional_descriptor_tokens[thread_id].pop(): 

2158 _OWNED_DESCRIPTORS.pop(token, None) 

2159 _FORK_STATE.pinned_objects[thread_id].pop() 

2160 _FORK_STATE.pinned_classes[thread_id].pop() 

2161 if not _FORK_STATE.provisional_descriptor_tokens[thread_id]: # pragma: needs fork 

2162 del _FORK_STATE.provisional_descriptor_tokens[thread_id] 

2163 del _FORK_STATE.pinned_objects[thread_id] 

2164 del _FORK_STATE.pinned_classes[thread_id] 

2165 finally: 

2166 # No other code reopens admission. A failure above would leave the other threads waiting for good. 

2167 with _FORK_STATE.gate: 

2168 # A handler that raised before the depth was stored left no entry to decrement. 

2169 if (depth := _FORK_STATE.fork_owner_depths.get(thread_id, 1)) <= 1: 

2170 _FORK_STATE.fork_owner_depths.pop(thread_id, None) 

2171 else: # pragma: no cover - earlier at-fork callbacks may deadlock first 

2172 _FORK_STATE.fork_owner_depths[thread_id] = depth - 1 

2173 _FORK_STATE.admission_closed = bool(_FORK_STATE.fork_owner_depths) 

2174 _FORK_STATE.gate.notify_all() 

2175 

2176 

2177def _ensure_current_process() -> None: 

2178 _reset_child_after_fork() 

2179 

2180 

2181def _reset_child_after_fork() -> None: # pragma: forked child 

2182 if (pid := os.getpid()) == _FORK_STATE.pid: 

2183 return 

2184 thread_id = get_ident() 

2185 pinned_objects = _FORK_STATE.pinned_objects.get(thread_id, [()])[-1] 

2186 pinned_classes = _FORK_STATE.pinned_classes.get(thread_id, [()])[-1] 

2187 descriptors: list[_OwnedDescriptor] = [] 

2188 for token, descriptor in tuple(_OWNED_DESCRIPTORS.items()): 

2189 if descriptor.creator_pid != pid: 

2190 descriptors.append(_OWNED_DESCRIPTORS.pop(token)) 

2191 _FORK_STATE.reset_synchronization() 

2192 _FORK_STATE.pid = pid 

2193 _INIT_PARAMETER_MODELS.clear() 

2194 _detach_child_state(descriptors, pinned_objects, pinned_classes) 

2195 

2196 

2197def _snapshot_descriptor_for_fork(fd: int, identity: tuple[int, int] | None) -> int: # pragma: needs fork 

2198 if identity is None: # pragma: needs fork 

2199 try: 

2200 stat_result = os.fstat(fd) 

2201 except OSError: 

2202 pass 

2203 else: 

2204 identity = stat_result.st_dev, stat_result.st_ino 

2205 return _record_owned_descriptor(fd, identity) 

2206 

2207 

2208def _detach_child_state( # pragma: forked child 

2209 descriptors: list[_OwnedDescriptor], 

2210 pinned_objects: tuple[_ForkResettable, ...], 

2211 pinned_classes: tuple[_ForkResettableClass, ...], 

2212) -> None: 

2213 for descriptor in descriptors: 

2214 if descriptor.device is None: 

2215 continue 

2216 try: 

2217 stat_result = os.fstat(descriptor.fd) 

2218 except OSError: 

2219 continue 

2220 if (stat_result.st_dev, stat_result.st_ino) != (descriptor.device, descriptor.inode): 

2221 continue 

2222 with contextlib.suppress(OSError): 

2223 os.close(descriptor.fd) 

2224 for instance in pinned_objects: 

2225 instance._reset_after_fork_in_child() # ruff:ignore[private-member-access] # resets each pinned instance in the fork child 

2226 for cls in pinned_classes: 

2227 cls._reset_class_after_fork() 

2228 _REGISTRY.held.clear() 

2229 

2230 

2231def _refresh_owner_pins() -> None: # pragma: needs fork 

2232 with _FORK_STATE.gate: 

2233 fork_owner_thread_ids = tuple(_FORK_STATE.fork_owner_depths) 

2234 if get_ident() not in fork_owner_thread_ids: # pragma: no cover - at-fork callbacks disable tracing 

2235 return 

2236 objects = tuple(_FORK_OBJECTS.values()) 

2237 classes = tuple(_FORK_CLASSES.values()) 

2238 for thread_id in fork_owner_thread_ids: 

2239 if object_snapshots := _FORK_STATE.pinned_objects.get(thread_id): # pragma: needs fork 

2240 object_snapshots[:] = [objects] * len(object_snapshots) 

2241 _FORK_STATE.pinned_classes[thread_id][:] = [classes] * len(object_snapshots) 

2242 

2243 

2244# The defaults capture the module globals: the hook outlives them at interpreter shutdown, where CPython wipes the 

2245# module dict to None before the final audit events fire. 

2246def _audit_fork_safety( # pragma: no cover - CPython disables tracing while Python audit hooks run 

2247 event: str, 

2248 _args: Unused, 

2249 *, 

2250 _fork_events: frozenset[str] = _FORK_AUDIT_EVENTS, 

2251 _state: _ForkState = _FORK_STATE, 

2252 _get_ident: Callable[[], int] = get_ident, 

2253) -> None: 

2254 if event in _fork_events: 

2255 if _get_ident() in _state.transitions or _state.fork_owner_depths: 

2256 msg = f"{event} is unsafe while filelock is changing descriptor ownership" 

2257 raise RuntimeError(msg) 

2258 elif event == "_posixsubprocess.fork_exec" and _get_ident() in _state.transitions: 

2259 msg = "fork_exec is unsafe while filelock is changing descriptor ownership" 

2260 raise RuntimeError(msg) 

2261 

2262 

2263_register_fork_hooks() 

2264 

2265 

2266__all__ = [ 

2267 "_UNSET_FILE_MODE", 

2268 "AcquireReturnProxy", 

2269 "BaseFileLock", 

2270 "CloseErrorPolicy", 

2271 "ContextErrorPolicy", 

2272 "FileLockContext", 

2273 "FileLockMeta", 

2274 "LockOptions", 

2275 "ThreadLocalFileContext", 

2276 "_acquire_thread_lock", 

2277 "_append_exception_context", 

2278 "_canonical", 

2279 "_check_blocking", 

2280 "_check_timeout_max", 

2281 "_delegate_to_current", 

2282 "_ensure_current_process", 

2283 "_fork_transition", 

2284 "_grouped_errors", 

2285 "_raise_body_and_release", 

2286 "_raise_chained_errors", 

2287 "_raise_cleanup_errors", 

2288 "_raise_grouped_errors", 

2289 "_register_fork_class", 

2290 "_register_fork_object", 

2291 "_register_owned_descriptor", 

2292 "_resolve_poll_interval", 

2293 "_resolve_read_write_timeout", 

2294 "_resolve_timeout", 

2295 "_seconds", 

2296 "_timeout_behavior", 

2297 "_unregister_owned_descriptor", 

2298]