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

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

1136 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_CONTEXT_ERROR_POLICIES: Final[frozenset[str]] = frozenset({"chain", "group"}) 

55 

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

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

58_CLOSE_ERROR_POLICIES: Final[frozenset[str]] = frozenset({"default", "raise", "suppress"}) 

59 

60if TYPE_CHECKING: 

61 from collections.abc import Generator, Mapping 

62 from threading import Lock 

63 from types import FrameType, TracebackType 

64 from typing import Protocol 

65 

66 from _typeshed import Unused 

67 

68 from filelock._lease import _LeaseClaim 

69 from filelock._read_write import ReadWriteLock 

70 from filelock._soft_rw import SoftReadWriteLock 

71 

72 class _ForkResettable(Protocol): 

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

74 

75 class _ForkDescriptorOwner(Protocol): 

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

77 

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

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

80 class _ForkResettableClass(Protocol): 

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

82 

83 class _RegisterAtFork(Protocol): 

84 def __call__( 

85 self, 

86 *, 

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

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

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

90 ) -> None: ... 

91 

92 from typing import Self 

93 

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

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

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

97 

98_ExtraValue = TypeVar("_ExtraValue") 

99_MarkerValue = TypeVar("_MarkerValue") 

100_SubclassValue = TypeVar("_SubclassValue") 

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

102 

103 

104class LockOptions(TypedDict, total=False): 

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

106 

107 timeout: float 

108 mode: int 

109 thread_local: bool 

110 blocking: bool 

111 is_singleton: bool 

112 poll_interval: float 

113 lifetime: float | None 

114 context_error_policy: ContextErrorPolicy 

115 close_error_policy: CloseErrorPolicy 

116 fallback_to_soft: bool 

117 preserve_lock_file: bool 

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

119 

120 

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

122 return isinstance(error, BaseExceptionGroup) 

123 

124 

125def _raise_grouped_errors( 

126 message: str, 

127 first_error: BaseException, 

128 second_error: BaseException, 

129 *additional_errors: BaseException, 

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

131) -> NoReturn: 

132 errors = (first_error, second_error, *additional_errors) 

133 _detach_grouped_contexts(errors) 

134 group: Final = BaseExceptionGroup(message, errors) 

135 if marker is not None: 

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

137 raise group from None 

138 

139 

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

141 seen: set[int] = set() 

142 pending = list(errors) 

143 while pending: 

144 error = pending.pop() 

145 if id(error) in seen: 

146 continue 

147 seen.add(id(error)) 

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

149 context is error 

150 or _same_exception_tree(error, context) 

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

152 ): 

153 error.__context__ = None 

154 elif context is not None: 

155 pending.append(context) 

156 if error.__cause__ is not None: 

157 pending.append(error.__cause__) 

158 if _is_exception_group(error): 

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

160 

161 

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

163 pending = [(first, second)] 

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

165 while pending: 

166 first_error, second_error = pending.pop() 

167 if first_error is second_error: 

168 continue 

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

170 continue 

171 seen.add(pair) 

172 if ( 

173 type(first_error) is not type(second_error) 

174 or not _is_exception_group(first_error) 

175 or not _is_exception_group(second_error) 

176 ): 

177 return False 

178 first_group = cast("_ExceptionGroupProtocol", first_error) 

179 second_group = cast("_ExceptionGroupProtocol", second_error) 

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

181 return False 

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

183 return True 

184 

185 

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

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

188 return False 

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

190 seen: set[int] = set() 

191 while pending: 

192 child = pending.pop() 

193 if child is target: 

194 return True 

195 if id(child) in seen: 

196 continue 

197 seen.add(id(child)) 

198 if _is_exception_group(child): 

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

200 return False 

201 

202 

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

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

205 return 

206 if error.__context__ is None: 

207 error.__context__ = context 

208 return 

209 tail = error 

210 seen: set[int] = set() 

211 while id(tail) not in seen: 

212 seen.add(id(tail)) 

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

214 tail.__context__ = context 

215 return 

216 tail = next_error 

217 

218 

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

220 pending = [error] 

221 seen: set[int] = set() 

222 while pending: 

223 current = pending.pop() 

224 if current is target: 

225 return True 

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

227 continue 

228 seen.add(id(current)) 

229 if current.__cause__ is not None: 

230 pending.append(current.__cause__) 

231 if current.__context__ is not None: 

232 pending.append(current.__context__) 

233 if _is_exception_group(current): 

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

235 return False 

236 

237 

238def _grouped_errors( 

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

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

241 if not _is_exception_group(error): 

242 return None 

243 group = cast("_ExceptionGroupProtocol", error) 

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

245 

246 

247if TYPE_CHECKING: 

248 

249 class _ExceptionGroupProtocol(Protocol): 

250 @property 

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

252 

253 @property 

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

255 

256 

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

258 if second_error is None: 

259 first_context = first_error.__context__ 

260 try: 

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

262 except BaseException: 

263 first_error.__context__ = first_context 

264 raise 

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

266 _detach_exception_context(second_context, first_error) 

267 _append_exception_context(first_error, second_context) 

268 first_context = first_error.__context__ 

269 try: 

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

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

272 first_error.__context__ = first_context 

273 try: 

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

275 except BaseException: 

276 second_error.__context__ = first_error 

277 first_error.__context__ = first_context 

278 raise 

279 

280 

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

282 pending = [error] 

283 seen: set[int] = set() 

284 while pending: 

285 current = pending.pop() 

286 if id(current) in seen: 

287 continue 

288 seen.add(id(current)) 

289 if current.__context__ is target: 

290 current.__context__ = None 

291 elif current.__context__ is not None: 

292 pending.append(current.__context__) 

293 if current.__cause__ is not None: 

294 pending.append(current.__cause__) 

295 if _is_exception_group(current): 

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

297 

298 

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

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

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

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

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

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

305 

306 

307def _raise_cleanup_errors( 

308 message: str, 

309 primary_error: BaseException, 

310 *cleanup_errors: BaseException | None, 

311) -> NoReturn: 

312 _raise_grouped_errors( 

313 message, 

314 primary_error, 

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

316 ) 

317 

318 

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

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

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

322 

323 

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

325 """ 

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

327 

328 Relative, absolute, and ``./`` spellings of one lock file must map to a single singleton instance, deadlock-registry 

329 entry, and removal key. Resolving the whole path with ``realpath`` would follow a final symlink and alias a lock 

330 target the backend deliberately rejects, so the registry identity would differ from the backend's. Resolving only 

331 the parent directory and re-appending the literal final component collapses the equivalent spellings while keeping a 

332 final symlink a distinct key. On Windows the parent is resolved with ``abspath`` so junctions and reparse points are 

333 not followed either. 

334 """ 

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

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

337 

338 

339class _ThreadLocalRegistry(local): 

340 def __init__(self) -> None: 

341 super().__init__() 

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

343 

344 

345_registry: Final[_ThreadLocalRegistry] = _ThreadLocalRegistry() 

346 

347 

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

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

350 

351 

352class FileLockMeta(ABCMeta): 

353 _instances: WeakValueDictionary[str, BaseFileLock] 

354 _instances_lock: RLock 

355 _instances_under_construction: set[str] 

356 

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

358 cls: type[_T], 

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

360 timeout: float = -1, 

361 mode: int = _UNSET_FILE_MODE, 

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

363 *, 

364 blocking: bool = True, 

365 is_singleton: bool = False, 

366 poll_interval: float = 0.05, 

367 lifetime: float | None = None, 

368 context_error_policy: ContextErrorPolicy = "chain", 

369 close_error_policy: CloseErrorPolicy = "default", 

370 fallback_to_soft: bool = True, 

371 preserve_lock_file: bool = False, 

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

373 **kwargs: _ExtraValue, 

374 ) -> _T: 

375 _ensure_current_process() 

376 _resolve_lazy_imports_before_shutdown() 

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

378 poll_interval = _resolve_poll_interval(poll_interval) 

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

380 # __del__ then trips over the missing context. 

381 _check_blocking(blocking) 

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

383 timeout = _seconds("timeout", timeout) 

384 _check_mode(mode, cls.__name__) 

385 context_error_policy = _resolve_context_error_policy(context_error_policy) 

386 close_error_policy = _resolve_close_error_policy(close_error_policy) 

387 preserve_lock_file = _resolve_preserve_lock_file( 

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

389 ) 

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

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

392 params: Final = _supported_init_params( 

393 cls, 

394 { 

395 "timeout": timeout, 

396 "mode": mode, 

397 "thread_local": thread_local, 

398 "blocking": blocking, 

399 "is_singleton": is_singleton, 

400 "poll_interval": poll_interval, 

401 "lifetime": lifetime, 

402 "context_error_policy": context_error_policy, 

403 "close_error_policy": close_error_policy, 

404 "fallback_to_soft": fallback_to_soft, 

405 "preserve_lock_file": preserve_lock_file, 

406 "on_acquired": on_acquired, 

407 **kwargs, 

408 }, 

409 ) 

410 if not is_singleton: 

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

412 

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

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

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

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

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

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

419 # deadlock-registry key acquire() uses. 

420 singleton_key = _canonical(lock_file) 

421 with cls._instances_lock: 

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

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

424 # names the file it pinned. 

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

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

427 ) != singleton_key: 

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

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

430 raise RuntimeError(msg) 

431 construction_registry = cls._instances_under_construction 

432 construction_pid = os.getpid() 

433 construction_registry.add(singleton_key) 

434 try: 

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

436 finally: 

437 construction_registry.discard(singleton_key) 

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

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

440 raise RuntimeError(msg) 

441 cls._instances[singleton_key] = instance 

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

443 return instance 

444 

445 non_matching_params = { 

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

447 for name, (passed_param, set_param) in { 

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

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

450 "timeout": ( 

451 _timeout_behavior(timeout, any_negative=True), 

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

453 ), 

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

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

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

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

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

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

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

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

462 }.items() 

463 if passed_param != set_param 

464 } 

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

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

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

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

469 hook_mismatch = on_acquired is not instance.on_acquired 

470 if not non_matching_params and not hook_mismatch: 

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

472 

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

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

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

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

477 if hook_mismatch: 

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

479 raise ValueError(msg) 

480 

481 

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

483 """ 

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

485 

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

487 still alive. Existence locks (:class:`SoftFileLock`) implement that behavior by unlinking a reclaimable pathname, 

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

489 the kernel lock; a contender would lock a fresh inode and overlap the live holder (#590). Ignore the request with a 

490 warning rather than accept a setting that breaks mutual exclusion. 

491 """ 

492 if lifetime is None: 

493 return None 

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

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

496 lifetime = _seconds("lifetime", lifetime) 

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

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

499 raise ValueError(msg) 

500 if not cls._lifetime_supported: 

501 warnings.warn( 

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

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

504 stacklevel=stacklevel, 

505 ) 

506 return None 

507 if cls._lifetime_replacements is not None: 

508 strict_lock, lease = cls._lifetime_replacements 

509 warnings.warn( 

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

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

512 SoftFileLockLifetimeWarning, 

513 stacklevel=stacklevel, 

514 ) 

515 return lifetime 

516 

517 

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

519 """Validate ``poll_interval`` used for ``time.sleep`` / ``asyncio.sleep`` retries.""" 

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

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

522 raise ValueError(msg) 

523 _check_timeout_max("poll_interval", poll_interval) 

524 return poll_interval 

525 

526 

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

528 if not isinstance(blocking, bool): 

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

530 raise TypeError(msg) 

531 

532 

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

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

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

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

537 raise TypeError(msg) 

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

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

540 return math.nan 

541 try: 

542 seconds: Final = float(value) 

543 except OverflowError: 

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

545 raise ValueError(msg) from None 

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

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

548 

549 

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

551 if mode == _UNSET_FILE_MODE: 

552 return 

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

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

555 raise TypeError(msg) 

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

557 if not 0 <= mode <= _PERMISSION_BITS: 

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

559 raise ValueError(msg) 

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

561 # and for good. 

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

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

564 raise ValueError(msg) 

565 

566 

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

568 if policy not in _CONTEXT_ERROR_POLICIES: 

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

570 raise ValueError(msg) 

571 return cast("ContextErrorPolicy", policy) 

572 

573 

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

575 if policy not in _CLOSE_ERROR_POLICIES: 

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

577 raise ValueError(msg) 

578 return cast("CloseErrorPolicy", policy) 

579 

580 

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

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

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

584 if preserve and not supported: 

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

586 raise ValueError(msg) 

587 return preserve 

588 

589 

590def _resolve_on_acquired( 

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

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

593 if on_acquired is None: 

594 return None 

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

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

597 if not supported: 

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

599 raise ValueError(msg) 

600 return on_acquired 

601 

602 

603def _supported_init_params( 

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

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

606 if (model := _init_parameter_model(cls)).accepts_kwargs: 

607 return params 

608 if unsupported := sorted( 

609 name 

610 for name, value in params.items() 

611 if name not in model.accepted_params 

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

613 ): 

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

615 raise TypeError(msg) 

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

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

618 

619 

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

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

622 if math.isnan(timeout): 

623 return "nan" 

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

625 

626 

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

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

629 if value > TIMEOUT_MAX: 

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

631 raise ValueError(msg) 

632 

633 

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

635 

636 

637def _init_parameter_model(cls: type[BaseFileLock]) -> _InitParameterModel: 

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

639 with _fork_transition(), _FORK_STATE.parameter_models_lock: 

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

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

642 model = _InitParameterModel( 

643 accepted_params=frozenset( 

644 parameter.name 

645 for parameter in parameters 

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

647 ), 

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

649 default_params={ 

650 name: parameter 

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

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

653 }, 

654 ) 

655 _INIT_PARAMETER_MODELS[cls] = model 

656 return model 

657 

658 

659@dataclass(frozen=True) 

660class _InitParameterModel: 

661 accepted_params: frozenset[str] 

662 accepts_kwargs: bool 

663 default_params: dict[str, inspect.Parameter] 

664 

665 

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

667 """ 

668 Abstract base class for a file lock object. 

669 

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

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

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

673 

674 """ 

675 

676 _instances: WeakValueDictionary[str, BaseFileLock] 

677 _instances_lock: RLock 

678 _instances_under_construction: set[str] 

679 

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

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

682 

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

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

685 _lifetime_supported: bool = False 

686 

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

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

689 

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

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

692 

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

694 _constructor_lifetime_warning_stacklevel: int = 3 

695 

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

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

698 _preserve_lock_file_supported: bool = True 

699 

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

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

702 _on_acquired_supported: bool = True 

703 

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

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

706 _poll_backoff_cap: float = 0.0 

707 

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

709 _singleton_key: str | None = None 

710 

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

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

713 super().__init_subclass__(**kwargs) 

714 cls._instances = WeakValueDictionary() 

715 cls._instances_lock = RLock() 

716 cls._instances_under_construction = set() 

717 _register_fork_class(cls) 

718 

719 @classmethod 

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

721 cls._instances = WeakValueDictionary() 

722 cls._instances_lock = RLock() 

723 cls._instances_under_construction = set() 

724 

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

726 self, 

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

728 timeout: float = -1, 

729 mode: int = _UNSET_FILE_MODE, 

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

731 *, 

732 blocking: bool = True, 

733 is_singleton: bool = False, 

734 poll_interval: float = 0.05, 

735 lifetime: float | None = None, 

736 context_error_policy: ContextErrorPolicy = "chain", 

737 close_error_policy: CloseErrorPolicy = "default", 

738 fallback_to_soft: bool = True, 

739 preserve_lock_file: bool = False, 

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

741 ) -> None: 

742 """ 

743 Create a new lock object. 

744 

745 :param lock_file: path to the file 

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

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

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

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

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

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

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

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

754 exists keeps its permissions. 

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

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

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

758 ``blocking``, ``mode``, and ``lifetime``. Setting one of these properties from one thread does not change 

759 the value seen by another thread; threads that did not perform the write continue to see the value supplied 

760 at construction time. ``mode`` has no setter, so construction is the only place it is ever set. If you need 

761 configuration values to be visible across threads, construct the lock with ``thread_local=False``. 

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

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

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

765 same object around. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

780 lock-file deletion. 

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

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

783 ``False`` fails closed, letting the ``ENOSYS`` propagate so a caller that needs kernel-enforced 

784 locking is never silently downgraded. It has no effect on Windows or :class:`SoftFileLock`. 

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

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

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

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

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

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

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

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

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

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

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

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

797 

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

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

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

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

802 denies the owner read and write 

803 

804 """ 

805 self._creator_pid = os.getpid() 

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

807 self._holder_id = id(self) 

808 self._transition_lock = RLock() 

809 self._is_thread_local = thread_local 

810 self._is_singleton = is_singleton 

811 self._context_error_policy = context_error_policy # already validated by the metaclass 

812 self._close_error_policy = close_error_policy # already validated by the metaclass 

813 self._fallback_to_soft = fallback_to_soft 

814 self._preserve_lock_file = preserve_lock_file # already validated by the metaclass 

815 self._on_acquired = on_acquired # already validated by the metaclass 

816 

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

818 lock_file=os.fspath(lock_file), 

819 timeout=timeout, 

820 mode=mode, 

821 blocking=blocking, 

822 poll_interval=poll_interval, 

823 lifetime=lifetime, 

824 ) 

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

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

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

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

829 _register_fork_object(self) 

830 

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

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

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

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

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

836 return {} 

837 

838 def is_thread_local(self) -> bool: 

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

840 return self._is_thread_local 

841 

842 @property 

843 def is_singleton(self) -> bool: 

844 """ 

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

846 

847 .. versionadded:: 3.13.0 

848 

849 """ 

850 return self._is_singleton 

851 

852 @property 

853 def context_error_policy(self) -> ContextErrorPolicy: 

854 """ 

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

856 

857 .. versionadded:: 3.30.0 

858 

859 """ 

860 return self._context_error_policy 

861 

862 @property 

863 def close_error_policy(self) -> CloseErrorPolicy: 

864 """ 

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

866 

867 .. versionadded:: 3.30.0 

868 

869 """ 

870 return self._close_error_policy 

871 

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

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

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

875 try: 

876 os.close(fd) 

877 except OSError: 

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

879 raise 

880 

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

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

883 

884 @property 

885 def fallback_to_soft(self) -> bool: 

886 """ 

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

888 

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

890 switching to existence-lock semantics. 

891 

892 .. versionadded:: 3.30.0 

893 

894 """ 

895 return self._fallback_to_soft 

896 

897 @property 

898 def preserve_lock_file(self) -> bool: 

899 """ 

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

901 

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

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

904 

905 .. versionadded:: 3.30.0 

906 

907 """ 

908 return self._preserve_lock_file 

909 

910 @property 

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

912 """ 

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

914 

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

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

917 

918 .. versionadded:: 3.30.0 

919 

920 """ 

921 return self._on_acquired 

922 

923 @property 

924 def lock_file(self) -> str: 

925 """Path to the lock file.""" 

926 return self._context.lock_file 

927 

928 @property 

929 def _lock_path(self) -> str: 

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

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

932 return self._context.claim_root or self._context.lock_file 

933 

934 @property 

935 def timeout(self) -> float: 

936 """ 

937 The default timeout value, in seconds. 

938 

939 .. versionadded:: 2.0.0 

940 

941 """ 

942 return self._context.timeout 

943 

944 @timeout.setter 

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

946 """ 

947 Change the default timeout value. 

948 

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

950 

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

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

953 

954 .. deprecated:: 4.0.13 

955 

956 Passing a numeric string; pass the number instead. 

957 

958 """ 

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

960 warnings.warn( 

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

962 DeprecationWarning, 

963 stacklevel=2, 

964 ) 

965 value = float(value) 

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

967 

968 @property 

969 def blocking(self) -> bool: 

970 """ 

971 Whether the locking is blocking or not. 

972 

973 .. versionadded:: 3.14.0 

974 

975 """ 

976 return self._context.blocking 

977 

978 @blocking.setter 

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

980 """ 

981 Change the default blocking value. 

982 

983 :param value: the new value as bool 

984 

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

986 

987 """ 

988 _check_blocking(value) 

989 self._context.blocking = value 

990 

991 @property 

992 def poll_interval(self) -> float: 

993 """ 

994 The default polling interval, in seconds. 

995 

996 .. versionadded:: 3.24.0 

997 

998 """ 

999 return self._context.poll_interval 

1000 

1001 @poll_interval.setter 

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

1003 """ 

1004 Change the default polling interval. 

1005 

1006 :param value: the new value, in seconds 

1007 

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

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

1010 

1011 """ 

1012 self._context.poll_interval = _resolve_poll_interval(value) 

1013 

1014 @property 

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

1016 """ 

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

1018 

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

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

1021 

1022 .. versionadded:: 3.24.0 

1023 

1024 """ 

1025 return self._context.lifetime 

1026 

1027 @lifetime.setter 

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

1029 """ 

1030 Change the legacy age-based expiry threshold. 

1031 

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

1033 

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

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

1036 

1037 """ 

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

1039 

1040 @property 

1041 def mode(self) -> int: 

1042 """ 

1043 The file permissions requested for the lock file. 

1044 

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

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

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

1048 

1049 """ 

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

1051 

1052 @property 

1053 def has_explicit_mode(self) -> bool: 

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

1055 return self._context.mode != _UNSET_FILE_MODE 

1056 

1057 def _open_mode(self) -> int: 

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

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

1060 

1061 @property 

1062 def is_locked(self) -> bool: 

1063 """ 

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

1065 

1066 .. versionchanged:: 2.0.0 

1067 

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

1069 

1070 """ 

1071 _ensure_current_process() 

1072 return self._context.lock_file_fd is not None 

1073 

1074 @property 

1075 def lock_counter(self) -> int: 

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

1077 _ensure_current_process() 

1078 return self._context.lock_counter 

1079 

1080 def __enter__(self) -> Self: 

1081 """ 

1082 Acquire the lock. 

1083 

1084 :returns: the lock object 

1085 

1086 """ 

1087 self.acquire() 

1088 return self 

1089 

1090 def __exit__( 

1091 self, 

1092 exc_type: type[BaseException] | None, 

1093 exc_value: BaseException | None, 

1094 traceback: TracebackType | None, 

1095 ) -> None: 

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

1097 self._release_in_context(exc_value) 

1098 

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

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

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

1102 try: 

1103 self.release() 

1104 except BaseException as release_error: 

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

1106 raise 

1107 _raise_body_and_release(body_error, release_error) 

1108 

1109 def __del__(self) -> None: 

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

1111 if vars(self).get("_creator_pid") != os.getpid(): 

1112 return # pragma: forked child 

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

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

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

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

1117 self._release_once_unreachable() 

1118 else: 

1119 self._release_abandoned_holds() 

1120 

1121 def _release_abandoned_holds(self) -> None: 

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

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

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

1125 with contextlib.suppress(Exception): 

1126 self.release(force=True) 

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

1128 with contextlib.suppress(Exception): 

1129 self._release_thread_holds(exited_only=False) 

1130 

1131 def _release_once_unreachable(self) -> None: 

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

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

1134 self._finalizer_state = _FinalizerState(vars(self)) 

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

1136 

1137 def acquire( 

1138 self, 

1139 timeout: float | None = None, 

1140 poll_interval: float | None = None, 

1141 *, 

1142 poll_intervall: float | None = None, 

1143 blocking: bool | None = None, 

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

1145 ) -> AcquireReturnProxy[Self]: 

1146 """ 

1147 Try to acquire the file lock. 

1148 

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

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

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

1152 :attr:`~poll_interval` 

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

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

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

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

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

1158 

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

1160 

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

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

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

1164 :data:`threading.TIMEOUT_MAX` 

1165 

1166 .. code-block:: python 

1167 

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

1169 with lock.acquire(): 

1170 pass 

1171 

1172 # Or use an equivalent try-finally construct: 

1173 lock.acquire() 

1174 try: 

1175 pass 

1176 finally: 

1177 lock.release() 

1178 

1179 .. versionchanged:: 2.0.0 

1180 

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

1182 without side effects. 

1183 

1184 """ 

1185 self._raise_if_inherited() 

1186 if timeout is None: 

1187 timeout = self._context.timeout 

1188 

1189 if blocking is None: 

1190 blocking = self._context.blocking 

1191 

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

1193 _check_blocking(blocking) 

1194 

1195 if poll_intervall is not None: 

1196 msg = "use poll_interval instead of poll_intervall" 

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

1198 poll_interval = poll_intervall 

1199 

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

1201 poll_interval = _resolve_poll_interval(poll_interval) 

1202 timeout = _resolve_timeout(timeout, blocking=blocking) 

1203 

1204 start_time = time.perf_counter() 

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

1206 # the descriptor exactly as it found them. 

1207 with self._transition_admission( 

1208 blocking=blocking, 

1209 cancel_check=cancel_check, 

1210 timeout=timeout, 

1211 poll_interval=poll_interval, 

1212 start_time=start_time, 

1213 ): 

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

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

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

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

1218 raise RuntimeError(msg) 

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

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

1221 if _on_this_stack(self._context.releasing): 

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

1223 raise RuntimeError(msg) 

1224 canonical = _canonical(self.lock_file) 

1225 self._context.lock_counter += 1 

1226 

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

1228 if not self.is_locked: 

1229 self._pin_lock_path(canonical) 

1230 

1231 try: 

1232 self._poll_until_acquired( 

1233 blocking=blocking, 

1234 cancel_check=cancel_check, 

1235 timeout=timeout, 

1236 poll_interval=poll_interval, 

1237 start_time=start_time, 

1238 ) 

1239 except BaseException: 

1240 self._reconcile_failed_acquire(canonical) 

1241 raise 

1242 self._commit_acquire(canonical) 

1243 return AcquireReturnProxy(lock=self) 

1244 

1245 @contextlib.contextmanager 

1246 def _transition_admission( 

1247 self, 

1248 *, 

1249 blocking: bool, 

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

1251 timeout: float, 

1252 poll_interval: float, 

1253 start_time: float, 

1254 ) -> Generator[None]: 

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

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

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

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

1259 if self._is_thread_local: 

1260 yield 

1261 return 

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

1263 wait: Final = poll_interval if blocking else 0.0 

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

1265 while not self._transition_lock.acquire( 

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

1267 ): 

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

1269 raise Timeout(self.lock_file) 

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

1271 raise Timeout(self.lock_file) 

1272 try: 

1273 yield 

1274 finally: 

1275 self._transition_lock.release() 

1276 

1277 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 

1278 """ 

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

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

1281 

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

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

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

1285 

1286 """ 

1287 counter_before: int | None = None 

1288 try: 

1289 counter_before = self._context.lock_counter 

1290 self._release_current(force=force) 

1291 except BaseException as release_error: 

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

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

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

1295 if ( 

1296 counter_before is not None 

1297 and counter_before == self._context.lock_counter 

1298 and self._context.lock_file_fd is not None 

1299 and not _is_system_call_failure(release_error) 

1300 ): 

1301 with contextlib.suppress(Exception): 

1302 self._release_current(force=force) 

1303 raise 

1304 if force: 

1305 self._release_thread_holds(exited_only=True) 

1306 

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

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

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

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

1311 if self._context.lock_file_fd is None: 

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

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

1314 if self._context.lock_file_registry is not None: 

1315 self._finish_interrupted_release() 

1316 return 

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

1318 state: Final = self._hold_state() 

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

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

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

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

1323 return 

1324 if not force and state.lock_counter > 1: 

1325 state.lock_counter -= 1 

1326 return 

1327 state.releasing = inspect.currentframe() 

1328 try: 

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

1330 self._release_with_fork_tracking() 

1331 finally: 

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

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

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

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

1336 self._commit_release() 

1337 state.releasing = None 

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

1339 

1340 def _finish_interrupted_release(self) -> None: 

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

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

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

1344 try: 

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

1346 self._commit_release() 

1347 finally: 

1348 self._transition_lock.release() 

1349 

1350 def _hold_state(self) -> FileLockContext: 

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

1352 

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

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

1355 return 

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

1357 with self._transition_lock: 

1358 own: Final = context.current 

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

1360 if exited_only and thread.is_alive(): 

1361 continue 

1362 context.current = held 

1363 try: 

1364 self._release_current(force=True) 

1365 finally: 

1366 context.current = own 

1367 

1368 def _raise_if_inherited(self) -> None: 

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

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

1371 raise RuntimeError(msg) 

1372 

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

1374 state: Final = self._hold_state() 

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

1376 state.lock_file_fd = fd 

1377 state.lock_file_fd_identity = identity 

1378 state.pending_lock_file_fd = None 

1379 state.pending_lock_file_fd_identity = None 

1380 if isinstance(self._context, ThreadLocalFileContext): 

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

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

1383 

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

1385 state: Final = self._hold_state() 

1386 state.pending_lock_file_fd = fd 

1387 state.pending_lock_file_fd_identity = identity 

1388 if isinstance(self._context, ThreadLocalFileContext): 

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

1390 

1391 def _mark_descriptor_released(self) -> None: 

1392 state: Final = self._hold_state() 

1393 state.pending_lock_file_fd = None 

1394 state.pending_lock_file_fd_identity = None 

1395 state.lock_file_fd = None 

1396 state.lock_file_fd_identity = None 

1397 if isinstance(self._context, ThreadLocalFileContext): 

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

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

1400 

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

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

1403 self._transition_lock = RLock() 

1404 if isinstance(self._context, ThreadLocalFileContext): 

1405 self._thread_holds.clear() 

1406 self._thread_pending.clear() 

1407 self._context.owner_claim_paths = () 

1408 self._context.claim_root = None 

1409 self._context.lock_file_fd = None 

1410 self._context.lock_file_fd_token = None 

1411 self._context.lock_file_fd_identity = None 

1412 self._context.pending_lock_file_fd = None 

1413 self._context.pending_lock_file_fd_identity = None 

1414 self._context.lock_counter = 0 

1415 self._context.releasing = None 

1416 self._context.acquiring = None 

1417 self._context.lock_file_key = None 

1418 self._context.lock_file_registry = None 

1419 self._context.lease_claim = None 

1420 

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

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

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

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

1425 if self._context.pending_lock_file_fd is not None: 

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

1427 return tuple(descriptors) 

1428 

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

1430 """ 

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

1432 

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

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

1435 """ 

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

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

1438 self._context.lock_counter -= 1 

1439 msg = ( 

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

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

1442 ) 

1443 raise RuntimeError(msg) 

1444 

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

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

1447 

1448 @staticmethod 

1449 def _deadlock_scope() -> Hashable | None: 

1450 """ 

1451 Execution unit whose own hold would deadlock a new acquire. 

1452 

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

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

1455 the *same* task can self-deadlock. 

1456 """ 

1457 return None 

1458 

1459 def _poll_until_acquired( 

1460 self, 

1461 *, 

1462 blocking: bool, 

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

1464 timeout: float, 

1465 poll_interval: float, 

1466 start_time: float, 

1467 ) -> None: 

1468 lock_id = id(self) 

1469 lock_filename = self.lock_file 

1470 attempt = 0 

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

1472 try: 

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

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

1475 if not self.is_locked: 

1476 ensure_directory_exists(self._lock_path) 

1477 while True: 

1478 self._raise_if_inherited() 

1479 if not self.is_locked: 

1480 self._try_break_expired_lock() 

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

1482 self._acquire_with_fork_tracking() 

1483 self._raise_if_inherited() 

1484 if self.is_locked: 

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

1486 return 

1487 if self._check_give_up( 

1488 blocking=blocking, 

1489 cancel_check=cancel_check, 

1490 timeout=timeout, 

1491 start_time=start_time, 

1492 ): 

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

1494 attempt += 1 

1495 delay = self._poll_delay(poll_interval, attempt) 

1496 if timeout >= 0: 

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

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

1499 _LOGGER.debug(msg, lock_id, lock_filename, delay) 

1500 time.sleep(delay) 

1501 finally: 

1502 self._context.acquiring = None 

1503 

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

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

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

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

1508 if not self._poll_backoff_cap: 

1509 return poll_interval 

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

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

1512 window = min( 

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

1514 ) # pragma: needs hard-link 

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

1516 

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

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

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

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

1521 if self.is_locked: 

1522 self._commit_acquire(canonical) 

1523 else: 

1524 self._undo_acquire() 

1525 

1526 def _invoke_on_acquired(self) -> None: 

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

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

1529 return 

1530 try: 

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

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

1533 callback_context = callback_error.__context__ 

1534 try: 

1535 self._release_with_fork_tracking() 

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

1537 _raise_body_and_release(callback_error, release_error) 

1538 callback_error.__context__ = callback_context 

1539 raise 

1540 

1541 def _acquire_with_fork_tracking(self) -> None: 

1542 with _fork_transition(self): 

1543 try: 

1544 self._acquire() 

1545 except BaseException as acquisition_error: 

1546 self._rollback_failed_acquire(acquisition_error) 

1547 raise 

1548 try: 

1549 self._register_context_descriptor() 

1550 except BaseException as registration_error: # pragma: needs fork 

1551 self._rollback_failed_registration(registration_error) 

1552 raise 

1553 if self.is_locked: 

1554 self._invoke_on_acquired() 

1555 

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

1557 if not self.is_locked: 

1558 return 

1559 registration_error: BaseException | None = None 

1560 tracking_error: BaseException | None = None 

1561 try: 

1562 self._register_context_descriptor() 

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

1564 registration_error = error 

1565 try: 

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

1567 self._register_unverified_context_descriptor() 

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

1569 tracking_error = error 

1570 try: 

1571 self._release_with_fork_tracking() 

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

1573 _raise_cleanup_errors( 

1574 "lock acquisition cleanup failed", 

1575 acquisition_error, 

1576 registration_error, 

1577 tracking_error, 

1578 rollback_error, 

1579 ) 

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

1581 _raise_cleanup_errors( 

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

1583 ) 

1584 

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

1586 tracking_error: BaseException | None = None 

1587 try: 

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

1589 self._register_unverified_context_descriptor() 

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

1591 tracking_error = error 

1592 try: 

1593 self._release_with_fork_tracking() 

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

1595 _raise_cleanup_errors( 

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

1597 ) 

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

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

1600 

1601 def _release_with_fork_tracking(self) -> None: 

1602 with _fork_transition(self): 

1603 try: 

1604 self._release() 

1605 except BaseException as release_error: 

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

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

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

1609 with contextlib.suppress(Exception): 

1610 self._release() 

1611 raise 

1612 finally: 

1613 self._unregister_released_descriptor() 

1614 

1615 def _register_context_descriptor(self) -> None: 

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

1617 self._context.lock_file_fd_token = _register_owned_descriptor( 

1618 self._context.lock_file_fd, 

1619 self._context.lock_file_fd_identity, 

1620 ) 

1621 

1622 def _register_unverified_context_descriptor(self) -> None: 

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

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

1625 self._context.lock_file_fd_token = _register_unverified_owned_descriptor(self._context.lock_file_fd) 

1626 

1627 def _unregister_released_descriptor(self) -> None: 

1628 if self._context.lock_file_fd is None: 

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

1630 _unregister_owned_descriptor(token) 

1631 self._context.lock_file_fd_token = None 

1632 self._context.lock_file_fd_identity = None 

1633 

1634 def _undo_acquire(self) -> None: 

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

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

1637 if self._context.lock_counter == 0: 

1638 self._drop_registry_entry() 

1639 self._context.claim_root = None 

1640 

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

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

1643 # change the error the backend raises. 

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

1645 

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

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

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

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

1650 if self._context.lock_file_registry is None: 

1651 key = self._registry_key(canonical) 

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

1653 self._context.lock_file_key = key 

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

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

1656 self._context.lock_file_registry = _registry.held 

1657 _registry.held[key] = self._holder_id 

1658 

1659 def _drop_registry_entry(self) -> None: 

1660 """Forget the key owned by this hold without resolving a mutable path again.""" 

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

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

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

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

1665 del registry[key] 

1666 self._context.lock_file_registry = None 

1667 self._context.lock_file_key = None 

1668 

1669 def _commit_release(self) -> None: 

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

1671 self._context.lock_counter = 0 

1672 self._drop_registry_entry() 

1673 self._context.claim_root = None 

1674 

1675 def _try_break_expired_lock(self) -> None: 

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

1677 

1678 def _check_give_up( 

1679 self, 

1680 *, 

1681 blocking: bool, 

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

1683 timeout: float, 

1684 start_time: float, 

1685 ) -> bool: 

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

1687 if blocking is False: 

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

1689 return True 

1690 if cancel_check is not None and cancel_check(): 

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

1692 return True 

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

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

1695 return True 

1696 return False 

1697 

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

1699 return None 

1700 

1701 @abstractmethod 

1702 def _acquire(self) -> None: 

1703 """If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file.""" 

1704 raise NotImplementedError 

1705 

1706 @abstractmethod 

1707 def _release(self) -> None: 

1708 """Releases the lock and sets self._context.lock_file_fd to None.""" 

1709 raise NotImplementedError 

1710 

1711 

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

1713 """Validate a read-write lock's acquire ``timeout``: ``-1`` or a non-negative number, ``inf`` meaning ``-1``.""" 

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

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

1716 raise ValueError(msg) 

1717 return timeout 

1718 

1719 

1720def _acquire_thread_lock( 

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

1722) -> bool: 

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

1724 if not blocking: 

1725 return lock.acquire(blocking=False) 

1726 if cancel_check is None: 

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

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

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

1730 while not lock.acquire( 

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

1732 ): 

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

1734 return False 

1735 return True 

1736 

1737 

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

1739 """Validate an acquire ``timeout``; every negative value waits without a limit.""" 

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

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

1742 raise ValueError(msg) 

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

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

1745 

1746 

1747@dataclass(slots=True, weakref_slot=True) 

1748class _FinalizerState: 

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

1750 

1751 state: dict[str, object] | None 

1752 

1753 

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

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

1756 

1757 

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

1759 _UNREACHABLE_LOCKS.discard(reference) 

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

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

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

1763 finalizer_state.state = None 

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

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

1766 stand_in = object.__new__(cls) 

1767 stand_in.__dict__ = state 

1768 del stand_in 

1769 

1770 

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

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

1773 if _is_exception_group(error): 

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

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

1776 

1777 

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

1779 if frame is None: 

1780 return False 

1781 caller = inspect.currentframe() 

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

1783 caller = caller.f_back 

1784 return caller is not None 

1785 

1786 

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

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

1789class AcquireReturnProxy(Generic[_LockT]): 

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

1791 

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

1793 self.lock: _LockT = lock 

1794 

1795 def __enter__(self) -> _LockT: 

1796 return self.lock 

1797 

1798 def __exit__( 

1799 self, 

1800 exc_type: type[BaseException] | None, 

1801 exc_value: BaseException | None, 

1802 traceback: TracebackType | None, 

1803 ) -> None: 

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

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

1806 if isinstance(lock, BaseFileLock): 

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

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

1809 lock.release() 

1810 

1811 

1812@dataclass 

1813class FileLockContext: 

1814 """Holds the context for a ``BaseFileLock`` object.""" 

1815 

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

1817 

1818 lock_file: str 

1819 timeout: float 

1820 mode: int 

1821 blocking: bool 

1822 poll_interval: float 

1823 

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

1825 lifetime: float | None = None 

1826 

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

1828 lock_file_fd: int | None = None 

1829 

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

1831 lock_file_fd_token: int | None = None 

1832 

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

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

1835 

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

1837 pending_lock_file_fd: int | None = None 

1838 

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

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

1841 

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

1843 lock_counter: int = 0 

1844 

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

1846 lock_file_key: Hashable | None = None 

1847 

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

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

1850 

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

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

1853 

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

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

1856 releasing: FrameType | None = None 

1857 

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

1859 acquiring: FrameType | None = None 

1860 

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

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

1863 claim_root: str | None = None 

1864 

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

1866 lease_claim: _LeaseClaim | None = None 

1867 

1868 

1869class ThreadLocalFileContext(FileLockContext, local): 

1870 """ 

1871 A thread local version of the ``FileLockContext`` class. 

1872 

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

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

1875 """ 

1876 

1877 current: FileLockContext 

1878 

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

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

1881 self, 

1882 *, 

1883 lock_file: str, 

1884 timeout: float, 

1885 mode: int, 

1886 blocking: bool, 

1887 poll_interval: float, 

1888 lifetime: float | None = None, 

1889 ) -> None: 

1890 self.current = FileLockContext( 

1891 lock_file=lock_file, 

1892 timeout=timeout, 

1893 mode=mode, 

1894 blocking=blocking, 

1895 poll_interval=poll_interval, 

1896 lifetime=lifetime, 

1897 ) 

1898 

1899 

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

1901 return property( 

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

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

1904 ) 

1905 

1906 

1907for _field in fields(FileLockContext): 

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

1909 

1910 

1911@dataclass(frozen=True) 

1912class _OwnedDescriptor: 

1913 fd: int 

1914 creator_pid: int 

1915 device: int | None 

1916 inode: int | None 

1917 

1918 

1919class _ForkState: 

1920 def __init__(self) -> None: 

1921 self.gate = Condition(RLock()) 

1922 self.registry_lock = RLock() 

1923 self.parameter_models_lock = RLock() 

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

1925 self.active_transitions = 0 

1926 self.admission_closed = False 

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

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

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

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

1931 self.pid = os.getpid() 

1932 

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

1934 self.gate = Condition(RLock()) 

1935 self.registry_lock = RLock() 

1936 self.parameter_models_lock = RLock() 

1937 self.transitions = {} 

1938 self.active_transitions = 0 

1939 self.admission_closed = False 

1940 self.fork_owner_depths = {} 

1941 self.pinned_objects = {} 

1942 self.pinned_classes = {} 

1943 self.provisional_descriptor_tokens = {} 

1944 

1945 

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

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

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

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

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

1951_FORK_STATE: Final = _ForkState() 

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

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

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

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

1956 

1957 

1958@cache 

1959def _resolve_lazy_imports_before_shutdown() -> None: 

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

1961 atexit.register(_resolve_lazy_imports) 

1962 

1963 

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

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

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

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

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

1969 for attribute, value in list(vars(module).items()): 

1970 if type(value) is types.LazyImportType: 

1971 getattr(module, attribute) 

1972 

1973 

1974def _register_fork_hooks() -> None: 

1975 if _REGISTER_AT_FORK is None: 

1976 return # pragma: lacks fork 

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

1978 sys.addaudithook(_audit_fork_safety) 

1979 _REGISTER_AT_FORK( # pragma: needs fork 

1980 before=_pin_fork_objects, 

1981 after_in_parent=_resume_parent_after_fork, 

1982 after_in_child=_reset_child_after_fork, 

1983 ) 

1984 

1985 

1986@contextmanager 

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

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

1989 if not _HAS_REGISTER_AT_FORK or sys.is_finalizing(): 

1990 yield # pragma: lacks fork 

1991 return # pragma: lacks fork 

1992 _ensure_current_process() # pragma: needs fork 

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

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

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

1996 token: Final = _enter_fork_transition(descriptor_owner, thread_id) # pragma: needs fork 

1997 try: # pragma: needs fork 

1998 yield 

1999 finally: 

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

2001 _leave_fork_transition(token, thread_id) 

2002 

2003 

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

2005 if not _HAS_REGISTER_AT_FORK: 

2006 return # pragma: lacks fork 

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

2008 _FORK_OBJECTS[id(instance)] = instance 

2009 _refresh_owner_pins() 

2010 

2011 

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

2013 if not _HAS_REGISTER_AT_FORK: 

2014 return # pragma: lacks fork 

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

2016 _FORK_CLASSES[id(cls)] = cls 

2017 _refresh_owner_pins() 

2018 

2019 

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

2021 if not _HAS_REGISTER_AT_FORK: 

2022 return None # pragma: lacks fork 

2023 with _fork_transition(): # pragma: needs fork 

2024 if identity is None: 

2025 stat_result = os.fstat(fd) 

2026 identity = stat_result.st_dev, stat_result.st_ino 

2027 return _record_owned_descriptor(fd, identity) 

2028 

2029 

2030def _register_unverified_owned_descriptor(fd: int) -> int | None: 

2031 if not _HAS_REGISTER_AT_FORK: 

2032 return None # pragma: lacks fork 

2033 with _fork_transition(): # pragma: needs fork 

2034 return _record_owned_descriptor(fd, None) 

2035 

2036 

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

2038 with _FORK_STATE.registry_lock: 

2039 token = next(_DESCRIPTOR_TOKENS) 

2040 _OWNED_DESCRIPTORS[token] = _OwnedDescriptor( 

2041 fd=fd, 

2042 creator_pid=os.getpid(), 

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

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

2045 ) 

2046 return token 

2047 

2048 

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

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

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

2052 return 

2053 with _fork_transition(), _FORK_STATE.registry_lock: 

2054 _OWNED_DESCRIPTORS.pop(token, None) 

2055 

2056 

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

2058 _ensure_current_process() 

2059 thread_id = get_ident() 

2060 provisional_descriptor_tokens: list[int] = [] 

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

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

2063 try: 

2064 with _FORK_STATE.gate: 

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

2066 _FORK_STATE.admission_closed = True 

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

2068 _FORK_STATE.gate.wait() 

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

2070 _verify_unverified_descriptors() 

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

2072 provisional_descriptor_tokens.extend( 

2073 starmap( 

2074 _snapshot_descriptor_for_fork, 

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

2076 ) 

2077 ) 

2078 finally: 

2079 with _FORK_STATE.registry_lock: 

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

2081 tuple(provisional_descriptor_tokens) 

2082 ) 

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

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

2085 

2086 

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

2088 thread_id = get_ident() 

2089 try: 

2090 with _FORK_STATE.registry_lock: 

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

2092 _OWNED_DESCRIPTORS.pop(token, None) 

2093 _FORK_STATE.pinned_objects[thread_id].pop() 

2094 _FORK_STATE.pinned_classes[thread_id].pop() 

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

2096 del _FORK_STATE.provisional_descriptor_tokens[thread_id] 

2097 del _FORK_STATE.pinned_objects[thread_id] 

2098 del _FORK_STATE.pinned_classes[thread_id] 

2099 finally: 

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

2101 with _FORK_STATE.gate: 

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

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

2104 _FORK_STATE.fork_owner_depths.pop(thread_id, None) 

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

2106 _FORK_STATE.fork_owner_depths[thread_id] = depth - 1 

2107 _FORK_STATE.admission_closed = bool(_FORK_STATE.fork_owner_depths) 

2108 _FORK_STATE.gate.notify_all() 

2109 

2110 

2111def _ensure_current_process() -> None: 

2112 _reset_child_after_fork() 

2113 

2114 

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

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

2117 return 

2118 thread_id = get_ident() 

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

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

2121 descriptors: list[_OwnedDescriptor] = [] 

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

2123 if descriptor.creator_pid != pid: 

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

2125 _FORK_STATE.reset_synchronization() 

2126 _FORK_STATE.pid = pid 

2127 _INIT_PARAMETER_MODELS.clear() 

2128 _detach_child_state(descriptors, pinned_objects, pinned_classes) 

2129 

2130 

2131def _enter_fork_transition(descriptor_owner: _ForkDescriptorOwner | None, thread_id: int) -> int: # pragma: needs fork 

2132 with _FORK_STATE.gate: 

2133 while ( 

2134 _FORK_STATE.admission_closed 

2135 and thread_id not in _FORK_STATE.fork_owner_depths 

2136 and thread_id not in _FORK_STATE.transitions 

2137 ): 

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

2139 token = next(_TRANSITION_TOKENS) 

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

2141 _FORK_STATE.active_transitions += 1 

2142 return token 

2143 

2144 

2145def _leave_fork_transition(token: int, thread_id: int) -> None: # pragma: needs fork 

2146 with _FORK_STATE.gate: 

2147 owned: Final = _FORK_STATE.transitions[thread_id] 

2148 del owned[token] 

2149 if not owned: 

2150 del _FORK_STATE.transitions[thread_id] 

2151 _FORK_STATE.active_transitions -= 1 

2152 _FORK_STATE.gate.notify_all() 

2153 

2154 

2155def _verify_unverified_descriptors() -> None: # pragma: needs fork 

2156 with _FORK_STATE.registry_lock: 

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

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

2159 continue 

2160 try: 

2161 stat_result = os.fstat(descriptor.fd) 

2162 except OSError: 

2163 continue 

2164 _OWNED_DESCRIPTORS[token] = _OwnedDescriptor( 

2165 fd=descriptor.fd, 

2166 creator_pid=descriptor.creator_pid, 

2167 device=stat_result.st_dev, 

2168 inode=stat_result.st_ino, 

2169 ) 

2170 

2171 

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

2173 if identity is None: # pragma: needs fork 

2174 try: 

2175 stat_result = os.fstat(fd) 

2176 except OSError: 

2177 pass 

2178 else: 

2179 identity = stat_result.st_dev, stat_result.st_ino 

2180 return _record_owned_descriptor(fd, identity) 

2181 

2182 

2183def _detach_child_state( # pragma: forked child 

2184 descriptors: list[_OwnedDescriptor], 

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

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

2187) -> None: 

2188 for descriptor in descriptors: 

2189 if descriptor.device is None: 

2190 continue 

2191 try: 

2192 stat_result = os.fstat(descriptor.fd) 

2193 except OSError: 

2194 continue 

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

2196 continue 

2197 with contextlib.suppress(OSError): 

2198 os.close(descriptor.fd) 

2199 for instance in pinned_objects: 

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

2201 for cls in pinned_classes: 

2202 cls._reset_class_after_fork() 

2203 _registry.held.clear() 

2204 

2205 

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

2207 with _FORK_STATE.gate: 

2208 fork_owner_thread_ids = tuple(_FORK_STATE.fork_owner_depths) 

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

2210 return 

2211 objects = tuple(_FORK_OBJECTS.values()) 

2212 classes = tuple(_FORK_CLASSES.values()) 

2213 for thread_id in fork_owner_thread_ids: 

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

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

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

2217 

2218 

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

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

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

2222 event: str, 

2223 _args: Unused, 

2224 *, 

2225 _fork_events: frozenset[str] = _FORK_AUDIT_EVENTS, 

2226 _state: _ForkState = _FORK_STATE, 

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

2228) -> None: 

2229 if event in _fork_events: 

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

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

2232 raise RuntimeError(msg) 

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

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

2235 raise RuntimeError(msg) 

2236 

2237 

2238_register_fork_hooks() 

2239 

2240 

2241__all__ = [ 

2242 "_UNSET_FILE_MODE", 

2243 "AcquireReturnProxy", 

2244 "BaseFileLock", 

2245 "CloseErrorPolicy", 

2246 "ContextErrorPolicy", 

2247 "FileLockContext", 

2248 "FileLockMeta", 

2249 "LockOptions", 

2250 "ThreadLocalFileContext", 

2251 "_acquire_thread_lock", 

2252 "_append_exception_context", 

2253 "_canonical", 

2254 "_check_timeout_max", 

2255 "_delegate_to_current", 

2256 "_ensure_current_process", 

2257 "_fork_transition", 

2258 "_grouped_errors", 

2259 "_raise_body_and_release", 

2260 "_raise_chained_errors", 

2261 "_raise_cleanup_errors", 

2262 "_raise_grouped_errors", 

2263 "_register_fork_class", 

2264 "_register_fork_object", 

2265 "_register_owned_descriptor", 

2266 "_resolve_poll_interval", 

2267 "_resolve_read_write_timeout", 

2268 "_resolve_timeout", 

2269 "_seconds", 

2270 "_timeout_behavior", 

2271 "_unregister_owned_descriptor", 

2272]