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
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
1from __future__ import annotations
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]
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
40from filelock._error import SoftFileLockLifetimeWarning, Timeout
41from filelock._util import ensure_directory_exists
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
47_PERMISSION_BITS: Final[int] = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
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
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"})
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"})
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
66 from _typeshed import Unused
68 from filelock._lease import _LeaseClaim
69 from filelock._read_write import ReadWriteLock
70 from filelock._soft_rw import SoftReadWriteLock
72 class _ForkResettable(Protocol):
73 def _reset_after_fork_in_child(self) -> None: ...
75 class _ForkDescriptorOwner(Protocol):
76 def _descriptors_for_fork(self) -> tuple[tuple[int, tuple[int, int] | None], ...]: ...
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: ...
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: ...
92 from typing import Self
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
98_ExtraValue = TypeVar("_ExtraValue")
99_MarkerValue = TypeVar("_MarkerValue")
100_SubclassValue = TypeVar("_SubclassValue")
101_LockInitValue = float | int | bool | str | None | Callable[[int], None]
104class LockOptions(TypedDict, total=False):
105 """Every option the metaclass forwards, so a subclass adding its own can still type what it passes through."""
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
121def _is_exception_group(error: BaseException) -> bool:
122 return isinstance(error, BaseExceptionGroup)
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
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)
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
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
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
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
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
247if TYPE_CHECKING:
249 class _ExceptionGroupProtocol(Protocol):
250 @property
251 def message(self) -> str: ...
253 @property
254 def exceptions(self) -> tuple[BaseException, ...]: ...
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
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)
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)
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 )
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
324def _canonical(path: str | os.PathLike[str]) -> str:
325 """
326 Return one stable key for *path*, collapsing equivalent spellings without following a final symlink.
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
339class _ThreadLocalRegistry(local):
340 def __init__(self) -> None:
341 super().__init__()
342 self.held: dict[Hashable, int] = {}
345_registry: Final[_ThreadLocalRegistry] = _ThreadLocalRegistry()
348_T = TypeVar("_T", bound="BaseFileLock")
349_LockT = TypeVar("_LockT", bound="BaseFileLock | ReadWriteLock | SoftReadWriteLock")
352class FileLockMeta(ABCMeta):
353 _instances: WeakValueDictionary[str, BaseFileLock]
354 _instances_lock: RLock
355 _instances_under_construction: set[str]
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)
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
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
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)
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.
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
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
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)
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
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)
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)
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)
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
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
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}
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
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)
634_INIT_PARAMETER_MODELS: Final[WeakKeyDictionary[type[BaseFileLock], _InitParameterModel]] = WeakKeyDictionary()
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
659@dataclass(frozen=True)
660class _InitParameterModel:
661 accepted_params: frozenset[str]
662 accepts_kwargs: bool
663 default_params: dict[str, inspect.Parameter]
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.
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>`).
674 """
676 _instances: WeakValueDictionary[str, BaseFileLock]
677 _instances_lock: RLock
678 _instances_under_construction: set[str]
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"
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
687 #: Strict-lock and lease replacements for a backend with legacy age-based expiry.
688 _lifetime_replacements: tuple[str, str] | None = None
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"
693 #: Async construction adds one metaclass frame before lifetime validation.
694 _constructor_lifetime_warning_stacklevel: int = 3
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
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
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
708 #: The singleton cache key of this instance, or ``None`` outside the cache.
709 _singleton_key: str | None = None
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)
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()
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.
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.
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
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
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)
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 {}
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
842 @property
843 def is_singleton(self) -> bool:
844 """
845 A flag indicating if this lock is singleton or not.
847 .. versionadded:: 3.13.0
849 """
850 return self._is_singleton
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.
857 .. versionadded:: 3.30.0
859 """
860 return self._context_error_policy
862 @property
863 def close_error_policy(self) -> CloseErrorPolicy:
864 """
865 What a lock does with an ``os.close`` failure after relinquishing descriptor ownership.
867 .. versionadded:: 3.30.0
869 """
870 return self._close_error_policy
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
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)
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``.
889 Only :class:`UnixFileLock` acts on it: when ``False`` an ``ENOSYS`` from ``flock`` propagates instead of
890 switching to existence-lock semantics.
892 .. versionadded:: 3.30.0
894 """
895 return self._fallback_to_soft
897 @property
898 def preserve_lock_file(self) -> bool:
899 """
900 Whether filelock promises not to unlink the lock pathname on release.
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.
905 .. versionadded:: 3.30.0
907 """
908 return self._preserve_lock_file
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``.
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.
918 .. versionadded:: 3.30.0
920 """
921 return self._on_acquired
923 @property
924 def lock_file(self) -> str:
925 """Path to the lock file."""
926 return self._context.lock_file
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
934 @property
935 def timeout(self) -> float:
936 """
937 The default timeout value, in seconds.
939 .. versionadded:: 2.0.0
941 """
942 return self._context.timeout
944 @timeout.setter
945 def timeout(self, value: float | str | bytes | bytearray) -> None:
946 """
947 Change the default timeout value.
949 :param value: the new value, in seconds; a negative value or ``inf`` waits without a limit
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`
954 .. deprecated:: 4.0.13
956 Passing a numeric string; pass the number instead.
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)
968 @property
969 def blocking(self) -> bool:
970 """
971 Whether the locking is blocking or not.
973 .. versionadded:: 3.14.0
975 """
976 return self._context.blocking
978 @blocking.setter
979 def blocking(self, value: bool) -> None:
980 """
981 Change the default blocking value.
983 :param value: the new value as bool
985 :raises TypeError: if *value* is not a bool
987 """
988 _check_blocking(value)
989 self._context.blocking = value
991 @property
992 def poll_interval(self) -> float:
993 """
994 The default polling interval, in seconds.
996 .. versionadded:: 3.24.0
998 """
999 return self._context.poll_interval
1001 @poll_interval.setter
1002 def poll_interval(self, value: float) -> None:
1003 """
1004 Change the default polling interval.
1006 :param value: the new value, in seconds
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
1011 """
1012 self._context.poll_interval = _resolve_poll_interval(value)
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.
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.
1022 .. versionadded:: 3.24.0
1024 """
1025 return self._context.lifetime
1027 @lifetime.setter
1028 def lifetime(self, value: float | None) -> None:
1029 """
1030 Change the legacy age-based expiry threshold.
1032 :param value: the new value in seconds, or ``None`` to disable expiration
1034 :raises ValueError: if *value* is negative or not finite
1035 :raises TypeError: if *value* is not ``None`` and not a real number
1037 """
1038 self._context.lifetime = _resolve_lifetime(value, type(self), stacklevel=3)
1040 @property
1041 def mode(self) -> int:
1042 """
1043 The file permissions requested for the lock file.
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.
1049 """
1050 return 0o644 if self._context.mode == _UNSET_FILE_MODE else self._context.mode
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
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
1061 @property
1062 def is_locked(self) -> bool:
1063 """
1064 A boolean indicating if the lock file is holding the lock currently.
1066 .. versionchanged:: 2.0.0
1068 This was previously a method and is now a property.
1070 """
1071 _ensure_current_process()
1072 return self._context.lock_file_fd is not None
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
1080 def __enter__(self) -> Self:
1081 """
1082 Acquire the lock.
1084 :returns: the lock object
1086 """
1087 self.acquire()
1088 return self
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)
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)
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()
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)
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))))
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.
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.
1159 :returns: a context object that will unlock the file when the context is exited
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`
1166 .. code-block:: python
1168 # You can use this method in the context manager (recommended)
1169 with lock.acquire():
1170 pass
1172 # Or use an equivalent try-finally construct:
1173 lock.acquire()
1174 try:
1175 pass
1176 finally:
1177 lock.release()
1179 .. versionchanged:: 2.0.0
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.
1184 """
1185 self._raise_if_inherited()
1186 if timeout is None:
1187 timeout = self._context.timeout
1189 if blocking is None:
1190 blocking = self._context.blocking
1192 # A truthy non-bool such as the string "false" would otherwise turn a single attempt into a blocking wait.
1193 _check_blocking(blocking)
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
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)
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
1227 self._raise_if_would_deadlock(canonical, timeout=timeout, blocking=blocking)
1228 if not self.is_locked:
1229 self._pin_lock_path(canonical)
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)
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()
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.
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.
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)
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)
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()
1350 def _hold_state(self) -> FileLockContext:
1351 return self._context.current if isinstance(self._context, ThreadLocalFileContext) else self._context
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
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)
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()
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()
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)
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
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)
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.
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)
1445 def _registry_key(self, canonical: str) -> Hashable:
1446 return canonical if (scope := self._deadlock_scope()) is None else (scope, canonical)
1448 @staticmethod
1449 def _deadlock_scope() -> Hashable | None:
1450 """
1451 Execution unit whose own hold would deadlock a new acquire.
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
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
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
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()
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
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()
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 )
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)
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()
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 )
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)
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
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
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
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
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
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
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."""
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
1698 def _unprobeable_holder(self) -> str | None: # ruff:ignore[no-self-use] # existence locks override it
1699 return None
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
1706 @abstractmethod
1707 def _release(self) -> None:
1708 """Releases the lock and sets self._context.lock_file_fd to None."""
1709 raise NotImplementedError
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
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
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
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."""
1751 state: dict[str, object] | None
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()
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
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
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
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."""
1792 def __init__(self, lock: _LockT) -> None:
1793 self.lock: _LockT = lock
1795 def __enter__(self) -> _LockT:
1796 return self.lock
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()
1812@dataclass
1813class FileLockContext:
1814 """Holds the context for a ``BaseFileLock`` object."""
1816 # A separate class so ThreadLocalFileContext can make the whole context thread-local.
1818 lock_file: str
1819 timeout: float
1820 mode: int
1821 blocking: bool
1822 poll_interval: float
1824 #: The lock lifetime in seconds; ``None`` means the lock never expires.
1825 lifetime: float | None = None
1827 #: File descriptor from os.open for the lock file; not None while the lock is held.
1828 lock_file_fd: int | None = None
1830 #: Registry token for the descriptor owned by this thread's context.
1831 lock_file_fd_token: int | None = None
1833 #: Identity captured by a backend that already inspected the descriptor.
1834 lock_file_fd_identity: tuple[int, int] | None = None
1836 #: Descriptor opened by a backend but not yet committed as the held lock.
1837 pending_lock_file_fd: int | None = None
1839 #: Identity captured for a descriptor whose acquisition has not committed.
1840 pending_lock_file_fd_identity: tuple[int, int] | None = None
1842 #: Depth of nested acquisitions; the lock is released only when it returns to 0.
1843 lock_counter: int = 0
1845 #: Canonical registry key captured when the first physical acquisition commits.
1846 lock_file_key: Hashable | None = None
1848 #: The acquiring thread's deadlock registry holding :attr:`lock_file_key`.
1849 lock_file_registry: dict[Hashable, int] | None = None
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, ...] = ()
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
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
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
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
1869class ThreadLocalFileContext(FileLockContext, local):
1870 """
1871 A thread local version of the ``FileLockContext`` class.
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 """
1877 current: FileLockContext
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 )
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 )
1907for _field in fields(FileLockContext):
1908 setattr(ThreadLocalFileContext, _field.name, _delegate_to_current(_field.name))
1911@dataclass(frozen=True)
1912class _OwnedDescriptor:
1913 fd: int
1914 creator_pid: int
1915 device: int | None
1916 inode: int | None
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()
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 = {}
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)
1958@cache
1959def _resolve_lazy_imports_before_shutdown() -> None:
1960 if hasattr(types, "LazyImportType"): # pragma: >=3.15 cover
1961 atexit.register(_resolve_lazy_imports)
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)
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 )
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)
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()
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()
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)
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)
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
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)
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()))
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()
2111def _ensure_current_process() -> None:
2112 _reset_child_after_fork()
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)
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
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()
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 )
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)
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()
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)
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)
2238_register_fork_hooks()
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]