Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_api.py: 51%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"]
55#: What a descriptor-owning backend does with an ``os.close`` failure after relinquishing ownership (see the property).
56CloseErrorPolicy = Literal["default", "raise", "suppress"]
58if TYPE_CHECKING:
59 from collections.abc import Generator, Mapping
60 from threading import Lock
61 from types import FrameType, TracebackType
62 from typing import Protocol
64 from _typeshed import Unused
66 from filelock._lease import _LeaseClaim
67 from filelock._read_write import ReadWriteLock
68 from filelock._soft_rw._sync import SoftReadWriteLock
70 class _ForkResettable(Protocol):
71 def _reset_after_fork_in_child(self) -> None: ...
73 class _ForkDescriptorOwner(Protocol):
74 def _descriptors_for_fork(self) -> tuple[tuple[int, tuple[int, int] | None], ...]: ...
76 # Matched against the class object itself rather than `type[...]` of it. A metaclass supplies this method to the
77 # class while leaving instances without it, so a `type[_ForkResettableClass]` bound rejects `ReadWriteLock`.
78 class _ForkResettableClass(Protocol):
79 def _reset_class_after_fork(self) -> None: ...
81 class _RegisterAtFork(Protocol):
82 def __call__(
83 self,
84 *,
85 before: Callable[[], None] | None = None,
86 after_in_parent: Callable[[], None] | None = None,
87 after_in_child: Callable[[], None] | None = None,
88 ) -> None: ...
90 from typing import Self
92_LOGGER: Final[logging.Logger] = logging.getLogger("filelock")
93_REGISTER_AT_FORK: Final[_RegisterAtFork | None] = cast("_RegisterAtFork | None", getattr(os, "register_at_fork", None))
94_HAS_REGISTER_AT_FORK: Final[bool] = _REGISTER_AT_FORK is not None
96_ExtraValue = TypeVar("_ExtraValue")
97_MarkerValue = TypeVar("_MarkerValue")
98_SubclassValue = TypeVar("_SubclassValue")
99_LockInitValue = float | int | bool | str | None | Callable[[int], None]
102class LockOptions(TypedDict, total=False):
103 """
104 Every option the metaclass forwards, so a subclass adding its own can still type what it passes through.
106 :ivar timeout: default acquire timeout in seconds; a negative value or ``inf`` waits without a limit
107 :ivar mode: file permissions for the lock file, as in :class:`BaseFileLock`
108 :ivar thread_local: whether the lock's internal context is per thread, so a reentrant lock is not shared
109 :ivar blocking: whether acquiring the lock waits for it
110 :ivar is_singleton: whether one instance is shared per lock file
111 :ivar poll_interval: default interval in seconds for polling the lock file
113 :ivar lifetime: for :class:`SoftFileLock`, the age in seconds after which a waiting process may delete the marker
114 :ivar context_error_policy: how a context manager reconciles a body failure with a release failure
115 :ivar close_error_policy: what to do with an ``os.close`` failure after the descriptor is relinquished
116 :ivar fallback_to_soft: for :class:`UnixFileLock`, whether ``ENOSYS`` falls back to :class:`SoftFileLock`
117 :ivar preserve_lock_file: for native locks, whether the lock pathname is kept on release
118 :ivar on_acquired: for native locks, a callable invoked with the borrowed lock descriptor on each acquisition
119 """
121 timeout: float
122 mode: int
123 thread_local: bool
124 blocking: bool
125 is_singleton: bool
126 poll_interval: float
127 lifetime: float | None
128 context_error_policy: ContextErrorPolicy
129 close_error_policy: CloseErrorPolicy
130 fallback_to_soft: bool
131 preserve_lock_file: bool
132 on_acquired: Callable[[int], None] | None
135def _is_exception_group(error: BaseException) -> bool:
136 return isinstance(error, BaseExceptionGroup)
139def _raise_grouped_errors(
140 message: str,
141 first_error: BaseException,
142 second_error: BaseException,
143 *additional_errors: BaseException,
144 marker: tuple[str, _MarkerValue] | None = None,
145) -> NoReturn:
146 errors = (first_error, second_error, *additional_errors)
147 _detach_grouped_contexts(errors)
148 group: Final = BaseExceptionGroup(message, errors)
149 if marker is not None:
150 setattr(group, marker[0], marker[1])
151 raise group from None
154def _detach_grouped_contexts(errors: tuple[BaseException, ...]) -> None:
155 seen: set[int] = set()
156 pending = list(errors)
157 while pending:
158 error = pending.pop()
159 if id(error) in seen:
160 continue
161 seen.add(id(error))
162 if (context := error.__context__) is not None and (
163 context is error
164 or _same_exception_tree(error, context)
165 or any(context is root or _contains_exception(root, context) for root in errors)
166 ):
167 error.__context__ = None
168 elif context is not None:
169 pending.append(context)
170 if error.__cause__ is not None:
171 pending.append(error.__cause__)
172 if _is_exception_group(error):
173 pending.extend(cast("_ExceptionGroupProtocol", error).exceptions)
176def _same_exception_tree(first: BaseException, second: BaseException) -> bool:
177 pending = [(first, second)]
178 seen: set[tuple[int, int]] = set()
179 while pending:
180 first_error, second_error = pending.pop()
181 if first_error is second_error:
182 continue
183 if (pair := (id(first_error), id(second_error))) in seen:
184 continue
185 seen.add(pair)
186 if (
187 type(first_error) is not type(second_error)
188 or not _is_exception_group(first_error)
189 or not _is_exception_group(second_error)
190 ):
191 return False
192 first_group = cast("_ExceptionGroupProtocol", first_error)
193 second_group = cast("_ExceptionGroupProtocol", second_error)
194 if first_group.message != second_group.message or len(first_group.exceptions) != len(second_group.exceptions):
195 return False
196 pending.extend(zip(first_group.exceptions, second_group.exceptions, strict=True))
197 return True
200def _contains_exception(error: BaseException, target: BaseException | None) -> bool:
201 if target is None or not _is_exception_group(error):
202 return False
203 pending = list(cast("_ExceptionGroupProtocol", error).exceptions)
204 seen: set[int] = set()
205 while pending:
206 child = pending.pop()
207 if child is target:
208 return True
209 if id(child) in seen:
210 continue
211 seen.add(id(child))
212 if _is_exception_group(child):
213 pending.extend(cast("_ExceptionGroupProtocol", child).exceptions)
214 return False
217def _grouped_errors(
218 error: BaseException, message: str, marker: tuple[str, _MarkerValue]
219) -> tuple[BaseException, ...] | None:
220 if not _is_exception_group(error):
221 return None
222 group = cast("_ExceptionGroupProtocol", error)
223 return group.exceptions if group.message == message and getattr(group, marker[0], None) is marker[1] else None
226if TYPE_CHECKING:
228 class _ExceptionGroupProtocol(Protocol):
229 @property
230 def message(self) -> str: ...
232 @property
233 def exceptions(self) -> tuple[BaseException, ...]: ...
236def _raise_chained_errors(first_error: BaseException, second_error: BaseException | None = None) -> NoReturn:
237 if second_error is None:
238 first_context = first_error.__context__
239 try:
240 raise first_error # ruff:ignore[raise-within-try] # the handler restores caller-supplied context before propagation
241 except BaseException:
242 first_error.__context__ = first_context
243 raise
244 if (second_context := second_error.__context__) is not None and second_context is not first_error:
245 _detach_exception_context(second_context, first_error)
246 _append_exception_context(first_error, second_context)
247 first_context = first_error.__context__
248 try:
249 raise first_error # ruff:ignore[raise-within-try] # the second raise needs this error as implicit context
250 except BaseException: # ruff:ignore[blind-except] # first_error may be a control-flow exception
251 first_error.__context__ = first_context
252 try:
253 raise second_error # ruff:ignore[raise-within-try] # the handler makes the chain interpreter-independent
254 except BaseException:
255 second_error.__context__ = first_error
256 first_error.__context__ = first_context
257 raise
260def _detach_exception_context(error: BaseException, target: BaseException) -> None:
261 pending = [error]
262 seen: set[int] = set()
263 while pending:
264 current = pending.pop()
265 if id(current) in seen:
266 continue
267 seen.add(id(current))
268 if current.__context__ is target:
269 current.__context__ = None
270 elif current.__context__ is not None:
271 pending.append(current.__context__)
272 if current.__cause__ is not None:
273 pending.append(current.__cause__)
274 if _is_exception_group(current):
275 pending.extend(cast("_ExceptionGroupProtocol", current).exceptions)
278def _append_exception_context(error: BaseException, context: BaseException) -> None:
279 if _exception_graph_contains(error, context) or _exception_graph_contains(context, error):
280 return
281 if error.__context__ is None:
282 error.__context__ = context
283 return
284 tail = error
285 seen: set[int] = set()
286 while id(tail) not in seen:
287 seen.add(id(tail))
288 if (next_error := tail.__cause__ if tail.__cause__ is not None else tail.__context__) is None:
289 tail.__context__ = context
290 return
291 tail = next_error
294def _exception_graph_contains(error: BaseException, target: BaseException) -> bool:
295 pending = [error]
296 seen: set[int] = set()
297 while pending:
298 current = pending.pop()
299 if current is target:
300 return True
301 if id(current) in seen: # pragma: no cover - arbitrary caller exceptions can contain cycles
302 continue
303 seen.add(id(current))
304 if current.__cause__ is not None:
305 pending.append(current.__cause__)
306 if current.__context__ is not None:
307 pending.append(current.__context__)
308 if _is_exception_group(current):
309 pending.extend(cast("_ExceptionGroupProtocol", current).exceptions)
310 return False
313def _raise_body_and_release(body_error: BaseException, release_error: BaseException) -> NoReturn:
314 # Group mode: surface the body failure and the release failure as sibling leaves instead of letting one hide in the
315 # other's __context__. BaseExceptionGroup returns a plain ExceptionGroup when both leaves subclass Exception, so
316 # ``except*`` and ``except Exception`` still catch them; a BaseException leaf (KeyboardInterrupt, CancelledError)
317 # keeps the group outside ordinary handlers. ``from None`` stops the group itself gaining a redundant __context__.
318 _raise_grouped_errors("lock body and release both failed", body_error, release_error)
321def _raise_cleanup_errors(
322 message: str,
323 primary_error: BaseException,
324 *cleanup_errors: BaseException | None,
325) -> NoReturn:
326 _raise_grouped_errors(
327 message,
328 primary_error,
329 *(error for error in cleanup_errors if error is not None),
330 )
333# On Windows os.path.realpath calls CreateFileW with share_mode=0, which blocks concurrent DeleteFileW and causes
334# livelocks under threaded contention with SoftFileLock. os.path.abspath is purely string-based and avoids this.
335_RESOLVE_DIR: Final[Callable[[str], str]] = os.path.abspath if sys.platform == "win32" else os.path.realpath
338def _canonical(path: str | os.PathLike[str]) -> str:
339 # Return one stable key for *path*, collapsing equivalent spellings without following a final symlink.
340 #
341 # Relative, absolute, and ``./`` spellings of one lock file must map to a single singleton instance,
342 # deadlock-registry entry, and removal key. Resolving the whole path with ``realpath`` would follow a final symlink
343 # and alias a lock target the backend deliberately rejects, so the registry identity would differ from the
344 # backend's. Resolving only the parent directory and re-appending the literal final component collapses the
345 # equivalent spellings while keeping a final symlink a distinct key. On Windows the parent is resolved with
346 # ``abspath`` so junctions and reparse points are not followed either.
347 parent, name = os.path.split(os.fspath(path))
348 return os.path.join(_RESOLVE_DIR(parent or os.curdir), name) # ruff:ignore[os-path-join] # string join matches abspath/realpath
351class _ThreadLocalRegistry(local):
352 def __init__(self) -> None:
353 super().__init__()
354 self.held: dict[Hashable, int] = {}
357_REGISTRY: Final[_ThreadLocalRegistry] = _ThreadLocalRegistry()
360_T = TypeVar("_T", bound="BaseFileLock")
361_LockT = TypeVar("_LockT", bound="BaseFileLock | ReadWriteLock | SoftReadWriteLock")
364class FileLockMeta(ABCMeta):
365 _instances: WeakValueDictionary[str, BaseFileLock]
366 _instances_lock: RLock
367 _instances_under_construction: set[str]
369 def __call__( # ruff:ignore[too-many-arguments] # forwards the public constructor's documented parameters
370 cls: type[_T],
371 lock_file: str | os.PathLike[str],
372 timeout: float = -1,
373 mode: int = _UNSET_FILE_MODE,
374 thread_local: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility
375 *,
376 blocking: bool = True,
377 is_singleton: bool = False,
378 poll_interval: float = 0.05,
379 lifetime: float | None = None,
380 context_error_policy: ContextErrorPolicy = "chain",
381 close_error_policy: CloseErrorPolicy = "default",
382 fallback_to_soft: bool = True,
383 preserve_lock_file: bool = False,
384 on_acquired: Callable[[int], None] | None = None,
385 **kwargs: _ExtraValue,
386 ) -> _T:
387 _ensure_current_process()
388 _resolve_lazy_imports_before_shutdown()
389 lifetime = _resolve_lifetime(lifetime, cls, stacklevel=cls._constructor_lifetime_warning_stacklevel)
390 poll_interval = _resolve_poll_interval(poll_interval)
391 # Validate before building the instance: a raise inside __init__ would leave a half-constructed object whose
392 # __del__ then trips over the missing context.
393 _check_blocking(blocking)
394 # nan is left to acquire, which knows whether the call blocks; a non-blocking call ignores the timeout.
395 timeout = _seconds("timeout", timeout)
396 _check_mode(mode, cls.__name__)
397 context_error_policy = _resolve_context_error_policy(context_error_policy)
398 close_error_policy = _resolve_close_error_policy(close_error_policy)
399 preserve_lock_file = _resolve_preserve_lock_file(
400 preserve=preserve_lock_file, supported=cls._preserve_lock_file_supported, cls_name=cls.__name__
401 )
402 on_acquired = _resolve_on_acquired(on_acquired, supported=cls._on_acquired_supported, cls_name=cls.__name__)
403 # Checked before the singleton lookup so a cache hit rejects a misspelled option just like a fresh build does.
404 params: Final = _supported_init_params(
405 cls,
406 {
407 "timeout": timeout,
408 "mode": mode,
409 "thread_local": thread_local,
410 "blocking": blocking,
411 "is_singleton": is_singleton,
412 "poll_interval": poll_interval,
413 "lifetime": lifetime,
414 "context_error_policy": context_error_policy,
415 "close_error_policy": close_error_policy,
416 "fallback_to_soft": fallback_to_soft,
417 "preserve_lock_file": preserve_lock_file,
418 "on_acquired": on_acquired,
419 **kwargs,
420 },
421 )
422 if not is_singleton:
423 return super().__call__(lock_file, **params)
425 # Look up, build and store under one lock. Without it two threads racing the first construction for a
426 # path both miss the cache and each build their own instance, so callers relying on is_singleton for
427 # reentrant locking across instances end up with two "singletons" and acquire()'s deadlock check then
428 # rejects a legitimate reentrant acquire; the unguarded writes to the WeakValueDictionary are a data
429 # race besides. ReadWriteLock and SoftReadWriteLock already guard their singleton caches this way.
430 # Key the cache on the canonical form so equivalent spellings of one path share a singleton, and it matches the
431 # deadlock-registry key acquire() uses.
432 singleton_key = _canonical(lock_file)
433 with cls._instances_lock:
434 # A cached lock resolves its relative path at each acquisition and pins it for the hold. After a chdir, or
435 # once a parent symlink points elsewhere, an unheld lock may name another file than this key; a held one
436 # names the file it pinned.
437 if (instance := cls._instances.get(singleton_key)) is None or _canonical(
438 instance._lock_path # ruff:ignore[private-member-access] # the metaclass owns the instance it returns
439 ) != singleton_key:
440 if singleton_key in cls._instances_under_construction: # pragma: needs fork
441 msg = f"Singleton lock construction is already active for {lock_file!s}"
442 raise RuntimeError(msg)
443 construction_registry = cls._instances_under_construction
444 construction_pid = os.getpid()
445 construction_registry.add(singleton_key)
446 try:
447 instance = super().__call__(lock_file, **params)
448 finally:
449 construction_registry.discard(singleton_key)
450 if os.getpid() != construction_pid: # pragma: needs fork
451 msg = "Lock construction cannot continue after fork; construct a new lock in the child"
452 raise RuntimeError(msg)
453 cls._instances[singleton_key] = instance
454 instance._singleton_key = singleton_key # ruff:ignore[private-member-access] # the cache entry the finalizer looks up
455 return instance
457 non_matching_params = {
458 name: (str(passed_param), str(set_param))
459 for name, (passed_param, set_param) in {
460 "thread_local": (thread_local, instance.is_thread_local()),
461 # Compare the wait, not the value: nan never equals itself, and -1, -5 and inf all mean no limit.
462 "timeout": (
463 _timeout_behavior(timeout, any_negative=True),
464 _timeout_behavior(instance.timeout, any_negative=True),
465 ),
466 "mode": (mode, instance._context.mode), # ruff:ignore[private-member-access] # compares against the managed instance's own context
467 "blocking": (blocking, instance.blocking),
468 "poll_interval": (poll_interval, instance.poll_interval),
469 "lifetime": (lifetime, instance.lifetime),
470 "context_error_policy": (context_error_policy, instance.context_error_policy),
471 "close_error_policy": (close_error_policy, instance.close_error_policy),
472 "fallback_to_soft": (fallback_to_soft, instance.fallback_to_soft),
473 "preserve_lock_file": (preserve_lock_file, instance.preserve_lock_file),
474 }.items()
475 if passed_param != set_param
476 }
477 # The metaclass owns the instance it returns, so reading the subclass hook stays inside this module's contract.
478 non_matching_params.update(instance._singleton_extra_mismatches(kwargs)) # ruff:ignore[private-member-access]
479 # Callables compare by identity, not equality: two equal callables can close over different state, so a
480 # singleton must reject a different hook object even if it compares equal. Keep it out of the scalar dict above.
481 hook_mismatch = on_acquired is not instance.on_acquired
482 if not non_matching_params and not hook_mismatch:
483 return instance # ty: ignore[invalid-return-type] # https://github.com/astral-sh/ty/issues/3231
485 msg = "Singleton lock instances cannot be initialized with differing arguments"
486 msg += "\nNon-matching arguments: "
487 for param_name, (passed_param, set_param) in non_matching_params.items():
488 msg += f"\n\t{param_name} (existing lock has {set_param} but {passed_param} was passed)"
489 if hook_mismatch:
490 msg += f"\n\ton_acquired (existing lock has {instance.on_acquired} but {on_acquired} was passed)"
491 raise ValueError(msg)
494def _resolve_lifetime(lifetime: float | None, cls: type[BaseFileLock], *, stacklevel: int) -> float | None:
495 # Validate ``lifetime`` and drop a value the backend cannot honor.
496 #
497 # ``lifetime`` is a deliberate age-based lease: a lock file older than ``lifetime`` is broken even while its holder
498 # is still alive. Existence locks (:class:`SoftFileLock`) implement that behavior by unlinking a reclaimable
499 # pathname, which can overlap a live holder. A native OS lock lives on the inode, so unlinking the pathname by age
500 # cannot revoke the kernel lock; a contender would lock a fresh inode and overlap the live holder (#590). Ignore the
501 # request with a warning rather than accept a setting that breaks mutual exclusion.
502 if lifetime is None:
503 return None
504 # An int stays exact: no float holds every int 4.0.12 accepted here.
505 if isinstance(lifetime, bool) or not isinstance(lifetime, int):
506 lifetime = _seconds("lifetime", lifetime)
507 if lifetime < 0 or (isinstance(lifetime, float) and not math.isfinite(lifetime)):
508 msg = f"lifetime must be finite and non-negative, not {lifetime!r}"
509 raise ValueError(msg)
510 if not cls._lifetime_supported:
511 warnings.warn(
512 f"lifetime is ignored for {cls.__name__}: {cls._lifetime_unsupported_reason}; "
513 f"only SoftFileLock supports lifetime-based expiry",
514 stacklevel=stacklevel,
515 )
516 return None
517 if cls._lifetime_replacements is not None:
518 strict_lock, lease = cls._lifetime_replacements
519 warnings.warn(
520 f"{cls.__name__}(lifetime=...) uses age-based expiry and can overlap a live holder; "
521 f"use {lease} for expiry or {strict_lock} for fail-closed locking",
522 SoftFileLockLifetimeWarning,
523 stacklevel=stacklevel,
524 )
525 return lifetime
528def _resolve_poll_interval(poll_interval: float) -> float:
529 # Validate ``poll_interval`` used for ``time.sleep`` / ``asyncio.sleep`` retries.
530 if (poll_interval := _seconds("poll_interval", poll_interval)) < 0 or not math.isfinite(poll_interval):
531 msg = f"poll_interval must be finite and non-negative, not {poll_interval!r}"
532 raise ValueError(msg)
533 _check_timeout_max("poll_interval", poll_interval)
534 return poll_interval
537def _check_blocking(blocking: bool) -> None: # ruff:ignore[boolean-type-hint-positional-argument] # validates the option's own value
538 if not isinstance(blocking, bool):
539 msg = f"blocking must be a bool, not {type(blocking).__name__}"
540 raise TypeError(msg)
543def _seconds(name: str, value: float) -> float:
544 # 4.0.12 accepted Decimal and Fraction; a bool is a typo, and a string would only fail later.
545 if isinstance(value, bool) or not isinstance(value, (Real, Decimal)):
546 msg = f"{name} must be a number of seconds, not {type(value).__name__}"
547 raise TypeError(msg)
548 # float() refuses a signaling NaN, which means no number of seconds just as a quiet one does.
549 if isinstance(value, Decimal) and value.is_snan():
550 return math.nan
551 try:
552 seconds: Final = float(value)
553 except OverflowError:
554 msg = f"{name} is too large to be a number of seconds: {value!r}"
555 raise ValueError(msg) from None
556 # float() rounds a tiny negative to -0.0, which compares as zero, yet timeout and lifetime checks read the sign.
557 return -math.ulp(0.0) if seconds == 0 and value < 0 else seconds
560def _check_mode(mode: int, cls_name: str) -> None:
561 if mode == _UNSET_FILE_MODE:
562 return
563 if isinstance(mode, bool) or not isinstance(mode, int):
564 msg = f"mode must be an int, not {type(mode).__name__}"
565 raise TypeError(msg)
566 # A lock file is never executed, so setuid, setgid and sticky have no meaning on it and only widen its permissions.
567 if not 0 <= mode <= _PERMISSION_BITS:
568 msg = f"mode={mode:#o} must be within 0o000-0o777 for {cls_name}"
569 raise ValueError(msg)
570 # A lock reopens, reads, or deletes the files it creates, so a mode that denies the owner read or write fails later
571 # and for good.
572 if ~mode & (stat.S_IRUSR | stat.S_IWUSR):
573 msg = f"mode={mode:#o} must grant the owner read and write for {cls_name}"
574 raise ValueError(msg)
577def _resolve_context_error_policy(policy: str) -> ContextErrorPolicy:
578 if policy not in frozenset({"chain", "group"}):
579 msg = f"context_error_policy must be 'chain' or 'group', got {policy!r}"
580 raise ValueError(msg)
581 return cast("ContextErrorPolicy", policy)
584def _resolve_close_error_policy(policy: str) -> CloseErrorPolicy:
585 if policy not in frozenset({"default", "raise", "suppress"}):
586 msg = f"close_error_policy must be 'default', 'raise', or 'suppress', got {policy!r}"
587 raise ValueError(msg)
588 return cast("CloseErrorPolicy", policy)
591def _resolve_preserve_lock_file(*, preserve: bool, supported: bool, cls_name: str) -> bool:
592 # An existence lock unlinks its marker to release, so preserving the pathname would defeat unlocking. Reject the
593 # request rather than silently ignore it, since a caller asking for a stable identity must know it cannot be kept.
594 if preserve and not supported:
595 msg = f"preserve_lock_file=True is not supported by {cls_name}: unlinking its marker is how it releases"
596 raise ValueError(msg)
597 return preserve
600def _resolve_on_acquired(
601 on_acquired: Callable[[int], None] | None, *, supported: bool, cls_name: str
602) -> Callable[[int], None] | None:
603 if on_acquired is None:
604 return None
605 # An existence lock stores protocol state in its marker, so a caller writing through the descriptor would corrupt
606 # stale detection and ownership metadata; only native locks lend out the descriptor.
607 if not supported:
608 msg = f"on_acquired is not supported by {cls_name}: only native locks expose the lock descriptor"
609 raise ValueError(msg)
610 return on_acquired
613def _supported_init_params(
614 cls: type[BaseFileLock], params: dict[str, _LockInitValue | _ExtraValue]
615) -> dict[str, _LockInitValue | _ExtraValue]:
616 # A strong cache would keep dynamically created subclasses alive for the process lifetime.
617 with _fork_transition(), _FORK_STATE.parameter_models_lock:
618 if (model := _INIT_PARAMETER_MODELS.get(cls)) is None:
619 parameters = inspect.signature(cls.__init__).parameters.values()
620 model = _InitParameterModel(
621 accepted_params=frozenset(
622 parameter.name
623 for parameter in parameters
624 if parameter.kind in {inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY}
625 ),
626 accepts_kwargs=any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters),
627 default_params={
628 name: parameter
629 for name, parameter in inspect.signature(type(cls).__call__).parameters.items()
630 if parameter.default is not inspect.Parameter.empty
631 },
632 )
633 _INIT_PARAMETER_MODELS[cls] = model
634 if model.accepts_kwargs:
635 return params
636 if unsupported := sorted(
637 name
638 for name, value in params.items()
639 if name not in model.accepted_params
640 and ((parameter := model.default_params.get(name)) is None or value != parameter.default)
641 ):
642 msg = f"{cls.__name__} does not support non-default lock options: {', '.join(unsupported)}"
643 raise TypeError(msg)
644 # virtualenv narrows a BaseFileLock descendant's signature; omit base defaults it does not accept (#340).
645 return {name: value for name, value in params.items() if name in model.accepted_params}
648def _timeout_behavior(timeout: float, *, any_negative: bool) -> float | str:
649 # inf, and for a BaseFileLock any negative value, wait without a limit like -1.
650 if math.isnan(timeout):
651 return "nan"
652 return -1.0 if timeout == math.inf or (any_negative and timeout < 0) else timeout
655def _check_timeout_max(name: str, value: float) -> None:
656 # Lock.acquire, Event.wait and time.sleep raise OverflowError past this bound, which is only ~49.7 days on Windows.
657 if value > TIMEOUT_MAX:
658 msg = f"{name} must not exceed threading.TIMEOUT_MAX ({TIMEOUT_MAX}), got {value!r}"
659 raise ValueError(msg)
662_INIT_PARAMETER_MODELS: Final[WeakKeyDictionary[type[BaseFileLock], _InitParameterModel]] = WeakKeyDictionary()
665@dataclass(frozen=True, slots=True, kw_only=True)
666class _InitParameterModel:
667 accepted_params: frozenset[str]
668 accepts_kwargs: bool
669 default_params: dict[str, inspect.Parameter]
672@dataclass(frozen=True, slots=True, kw_only=True)
673class _LockSettings:
674 thread_local: bool
675 is_singleton: bool
676 context_error_policy: ContextErrorPolicy
677 close_error_policy: CloseErrorPolicy
678 fallback_to_soft: bool
679 preserve_lock_file: bool
680 on_acquired: Callable[[int], None] | None
683class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta): # ruff:ignore[too-many-public-methods] # public config properties
684 """
685 Abstract base class for a file lock object.
687 Provides the common reentrant API and state management. Subclasses implement the locking mechanism
688 (:class:`UnixFileLock <filelock.UnixFileLock>`, :class:`WindowsFileLock <filelock.WindowsFileLock>`,
689 :class:`SoftFileLock <filelock.SoftFileLock>`).
691 """
693 _instances: WeakValueDictionary[str, BaseFileLock]
694 _instances_lock: RLock
695 _instances_under_construction: set[str]
697 #: How the cross-instance deadlock message names the conflicting holder; the async subclass says "task".
698 _deadlock_holder_desc: str = "FileLock instance in this thread"
700 #: Whether an age-based :attr:`lifetime` lease may break this lock. Only existence locks set it (they reclaim by
701 #: unlinking a pathname); native OS locks leave it ``False`` since a kernel lock cannot be revoked by file age.
702 _lifetime_supported: bool = False
704 #: Strict-lock and lease replacements for a backend with legacy age-based expiry.
705 _lifetime_replacements: tuple[str, str] | None = None
707 #: Why a backend that refuses ``lifetime`` cannot honor it, named in the warning that drops the value.
708 _lifetime_unsupported_reason: str = "a native OS lock cannot be broken safely by file age"
710 #: Async construction adds one metaclass frame before lifetime validation.
711 _constructor_lifetime_warning_stacklevel: int = 3
713 #: Whether :attr:`preserve_lock_file` may be ``True``. Native locks keep the pathname on release, so they support
714 #: it; existence locks unlink their marker to release and reject it.
715 _preserve_lock_file_supported: bool = True
717 #: Whether an :attr:`on_acquired` hook may be set. Native locks lend the descriptor out; existence locks keep
718 #: protocol state in the marker and reject it.
719 _on_acquired_supported: bool = True
721 #: Ceiling in seconds on the jittered backoff between contended acquisition retries. ``0`` keeps the fixed
722 #: poll cadence; a multi-file backend sets it so contending processes desynchronize instead of livelocking.
723 _poll_backoff_cap: float = 0.0
725 #: The singleton cache key of this instance, or ``None`` outside the cache.
726 _singleton_key: str | None = None
728 def __init_subclass__(cls, **kwargs: _SubclassValue) -> None:
729 """Give each lock subclass its own singleton registry and lock."""
730 super().__init_subclass__(**kwargs)
731 cls._instances = WeakValueDictionary()
732 cls._instances_lock = RLock()
733 cls._instances_under_construction = set()
734 _register_fork_class(cls)
736 def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option
737 self,
738 lock_file: str | os.PathLike[str],
739 timeout: float = -1,
740 mode: int = _UNSET_FILE_MODE,
741 thread_local: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility
742 *,
743 blocking: bool = True,
744 is_singleton: bool = False,
745 poll_interval: float = 0.05,
746 lifetime: float | None = None,
747 context_error_policy: ContextErrorPolicy = "chain",
748 close_error_policy: CloseErrorPolicy = "default",
749 fallback_to_soft: bool = True,
750 preserve_lock_file: bool = False,
751 on_acquired: Callable[[int], None] | None = None,
752 ) -> None:
753 """
754 Create a new lock object.
756 :param lock_file: path to the file
757 :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in the
758 acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it to a
759 negative value or ``inf``. A timeout of 0 means that there is exactly one attempt to acquire the file lock.
761 :param mode: file permissions for the lockfile. When not specified, the OS controls permissions via umask and
762 default ACLs, preserving POSIX default ACL inheritance in shared directories. Construction raises
763 :class:`ValueError` unless it grants the owner read and write, which the lock needs for its own files. On
764 Windows a native lock creates its file with an owner-only ACL when the mode has no group and other bits; any
765 other mode, and a soft lock, leave the file the ACL its directory hands down. A Unix lock file that already
766 exists keeps its permissions.
768 :param thread_local: Whether this object's internal context should be thread local or not. If this is set to
769 ``False`` then the lock will be reentrant across threads. When ``True`` (the default), **all fields of the
770 lock's internal context are per-thread**, including the configuration values ``poll_interval``, ``timeout``,
771 ``blocking``, ``mode``, and ``lifetime``.
773 Setting one of these properties from one thread does not change the value seen by another thread; threads
774 that did not perform the write continue to see the value supplied at construction time. ``mode`` has no
775 setter, so construction is the only place it is ever set. If you need configuration values to be visible
776 across threads, construct the lock with ``thread_local=False``.
778 :param blocking: whether the lock should be blocking or not
779 :param is_singleton: If this is set to ``True`` then only one instance of this class will be created per lock
780 file. This is useful if you want to use the lock object for reentrant locking without needing to pass the
781 same object around.
782 :param poll_interval: default interval for polling the lock file, in seconds. It will be used as fallback value
783 in the acquire method, if no poll_interval value (``None``) is given.
785 :param lifetime: for :class:`SoftFileLock`, the age in seconds after which a waiting process may delete the
786 marker, even while its holder remains alive. This legacy expiry mode does not provide strict mutual
787 exclusion. ``None`` (the default) disables age-based expiry. Native OS locks (:class:`FileLock`) cannot be
788 revoked by file age and ignore a non-``None`` ``lifetime`` with a warning.
790 :param context_error_policy: how a context manager reconciles a failure in its body with a failure while
791 releasing on exit. ``"chain"`` (the default) keeps Python's behavior: the release error propagates with the
792 body error in its ``__context__``. ``"group"`` raises a :class:`BaseExceptionGroup` holding the body error
793 first and the release error second, so neither hides the other.
795 :param close_error_policy: what to do with an ``os.close`` failure after relinquishing descriptor ownership.
796 ``"default"`` keeps each backend's historical behavior (Unix native locks drop a FUSE/Docker ``EIO``;
797 Windows native locks and :class:`SoftFileLock` propagate); ``"raise"`` always propagates the ``OSError``;
798 ``"suppress"`` always ignores it. Held state is released either way. It does not affect unlock failures or
799 lock-file deletion.
801 :param fallback_to_soft: for :class:`UnixFileLock`, whether to switch to :class:`SoftFileLock` when the
802 filesystem's ``flock`` returns ``ENOSYS``. ``True`` (the default) keeps the historical fallback; ``False``
803 fails closed, letting the ``ENOSYS`` propagate so a caller that needs kernel-enforced locking is never
804 silently downgraded. It has no effect on Windows or :class:`SoftFileLock`.
806 :param preserve_lock_file: for native locks (:class:`FileLock`), whether filelock promises not to unlink the
807 lock pathname on release. ``False`` (the default) keeps each backend's cleanup: Windows removes the lock
808 file, Unix already leaves it. ``True`` keeps a stable file identity for ACLs, auditing, and holder metadata:
809 Windows skips its post-release unlink and Unix refuses to enter the ``ENOSYS`` soft fallback (which releases
810 by unlinking). :class:`SoftFileLock` rejects ``True``. The promise covers filelock's own release path only;
811 it cannot stop another process or the filesystem from removing the pathname.
813 :param on_acquired: for native locks (:class:`FileLock`), a callable invoked with the borrowed lock descriptor
814 once per physical acquisition, after filelock holds the native lock and finished backend initialization but
815 before :meth:`~BaseFileLock.acquire` returns. Recursive acquisitions do not call it again. The callback may
816 read, write, seek, truncate, or set metadata through ``os`` on the descriptor, but must not close, unlock,
817 or take ownership of it, and filelock does not fsync its writes. If it raises, filelock releases the lock
818 and re-raises. :class:`SoftFileLock` rejects the hook.
820 :raises TypeError: if *timeout* or *poll_interval* is a :class:`bool` or not a real number, *blocking* is not a
821 :class:`bool`, or *mode* is not an :class:`int`
822 :raises ValueError: if *poll_interval* is negative, not finite, or above :data:`threading.TIMEOUT_MAX`, if
823 *timeout* is too large to convert to :class:`float`, or if *mode* falls outside ``0o000``-``0o777`` or
824 denies the owner read and write
826 """
827 self._adopt_settings(
828 _LockSettings(
829 thread_local=thread_local,
830 is_singleton=is_singleton,
831 context_error_policy=context_error_policy,
832 close_error_policy=close_error_policy,
833 fallback_to_soft=fallback_to_soft,
834 preserve_lock_file=preserve_lock_file,
835 on_acquired=on_acquired,
836 )
837 )
839 self._context: FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext)(
840 lock_file=os.fspath(lock_file),
841 timeout=timeout,
842 mode=mode,
843 blocking=blocking,
844 poll_interval=poll_interval,
845 lifetime=lifetime,
846 )
847 #: Each thread's committed hold on a thread-local lock, so another thread can release it.
848 self._thread_holds: dict[int, tuple[FileLockContext, Thread]] = {}
849 #: Each thread's uncommitted acquisition, so a backend can finish it for an exited thread.
850 self._thread_pending: dict[int, tuple[FileLockContext, Thread]] = {}
851 _register_fork_object(self)
853 def _adopt_settings(self, settings: _LockSettings) -> None:
854 self._creator_pid = os.getpid()
855 #: Holder id in the deadlock registry; a stand-in that finishes the finalizer has another id(self).
856 self._holder_id = id(self)
857 self._transition_lock = RLock()
858 self._is_thread_local = settings.thread_local
859 self._is_singleton = settings.is_singleton
860 self._context_error_policy = settings.context_error_policy # already validated by the metaclass
861 self._close_error_policy = settings.close_error_policy # already validated by the metaclass
862 self._fallback_to_soft = settings.fallback_to_soft
863 self._preserve_lock_file = settings.preserve_lock_file # already validated by the metaclass
864 self._on_acquired = settings.on_acquired # already validated by the metaclass
866 def __enter__(self) -> Self:
867 """
868 Acquire the lock.
870 :returns: the lock object
872 """
873 self.acquire()
874 return self
876 def acquire(
877 self,
878 timeout: float | None = None,
879 poll_interval: float | None = None,
880 *,
881 poll_intervall: float | None = None,
882 blocking: bool | None = None,
883 cancel_check: Callable[[], bool] | None = None,
884 ) -> AcquireReturnProxy[Self]:
885 """
886 Try to acquire the file lock.
888 :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default :attr:`~timeout` is and
889 if ``timeout < 0``, there is no timeout and this method will block until the lock could be acquired
890 :param poll_interval: interval of trying to acquire the lock file, ``None`` means use the default
891 :attr:`~poll_interval`
892 :param poll_intervall: deprecated, kept for backwards compatibility, use ``poll_interval`` instead
894 :param blocking: whether to retry until this call holds the lock or the timeout expires, ``None`` means use the
895 default :attr:`~blocking`. ``False`` makes one attempt and raises :class:`~Timeout` on failure.
896 :param cancel_check: a callable returning ``True`` when the acquisition should be canceled. Checked on each poll
897 iteration. When triggered, raises :class:`~Timeout` just like an expired timeout.
899 :returns: a context object that will unlock the file when the context is exited
901 :raises Timeout: if fails to acquire lock within the timeout period
902 :raises RuntimeError: if the lock was inherited across a fork, or this thread already waits on or releases it
903 :raises TypeError: if *blocking* is not a :class:`bool`
904 :raises ValueError: if a blocking call gets a ``nan`` timeout, or *poll_interval* exceeds
905 :data:`threading.TIMEOUT_MAX`
907 .. code-block:: python
909 # You can use this method in the context manager (recommended)
910 with lock.acquire():
911 pass
913 # Or use an equivalent try-finally construct:
914 lock.acquire()
915 try:
916 pass
917 finally:
918 lock.release()
920 .. versionchanged:: 2.0.0
922 This method returns now a *proxy* object instead of *self*, so that it can be used in a with statement
923 without side effects.
925 """
926 self._raise_if_inherited()
927 if timeout is None:
928 timeout = self._context.timeout
930 if blocking is None:
931 blocking = self._context.blocking
933 # A truthy non-bool such as the string "false" would otherwise turn a single attempt into a blocking wait.
934 _check_blocking(blocking)
936 if poll_intervall is not None:
937 msg = "use poll_interval instead of poll_intervall"
938 warnings.warn(msg, DeprecationWarning, stacklevel=2)
939 poll_interval = poll_intervall
941 poll_interval = poll_interval if poll_interval is not None else self._context.poll_interval
942 poll_interval = _resolve_poll_interval(poll_interval)
943 timeout = _resolve_timeout(timeout, blocking=blocking)
945 start_time = time.perf_counter()
946 # Wait for admission before touching any state: a caller refused entry must leave the counter, the registry and
947 # the descriptor exactly as it found them.
948 with self._transition_admission(
949 blocking=blocking,
950 cancel_check=cancel_check,
951 timeout=timeout,
952 poll_interval=poll_interval,
953 start_time=start_time,
954 ):
955 # The interrupted acquire cannot run until the handler returns, so a second one here would wait on it
956 # forever or take the lock out from under it. Once the lock is held, as in an on_acquired hook, it nests.
957 if not self.is_locked and _on_this_stack(self._context.acquiring):
958 msg = f"Deadlock: lock '{self.lock_file}' is acquired again while this thread's acquire of it waits"
959 raise RuntimeError(msg)
960 # An interrupted release commits only the hold it started with and ignores a release above it on the
961 # stack. A hold taken here would nest into one the OS dropped, or stay held for good.
962 if _on_this_stack(self._context.releasing):
963 msg = f"lock '{self.lock_file}' is acquired again while this thread's release of it runs"
964 raise RuntimeError(msg)
965 canonical = _canonical(self.lock_file)
966 self._context.lock_counter += 1
968 self._raise_if_would_deadlock(canonical, timeout=timeout, blocking=blocking)
969 if not self.is_locked:
970 self._pin_lock_path(canonical)
972 try:
973 self._poll_until_acquired(
974 blocking=blocking,
975 cancel_check=cancel_check,
976 timeout=timeout,
977 poll_interval=poll_interval,
978 start_time=start_time,
979 )
980 except BaseException:
981 self._reconcile_failed_acquire(canonical)
982 raise
983 self._commit_acquire(canonical)
984 return AcquireReturnProxy(lock=self)
986 def _raise_if_inherited(self) -> None:
987 if self._creator_pid != os.getpid(): # pragma: forked child
988 msg = f"{type(self).__name__} on {self.lock_file} was inherited across fork; construct a new instance"
989 raise RuntimeError(msg)
991 @contextlib.contextmanager
992 def _transition_admission(
993 self,
994 *,
995 blocking: bool,
996 cancel_check: Callable[[], bool] | None,
997 timeout: float,
998 poll_interval: float,
999 start_time: float,
1000 ) -> Generator[None]:
1001 # One thread at a time drives the physical transition of a shared instance: its counter, descriptor and claim
1002 # state are plain attributes every thread reads and writes, so an unserialized loser clears the winner's
1003 # descriptor or double-closes it. Waiting in slices keeps cancel_check and the timeout live. A thread-local
1004 # context is never shared, so it skips the gate.
1005 if self._is_thread_local:
1006 yield
1007 return
1008 deadline: Final = None if timeout < 0 else start_time + timeout
1009 wait: Final = poll_interval if blocking else 0.0
1010 # wait leads the min() so a non-blocking call, whose nan timeout goes unchecked, waits 0 rather than nan.
1011 while not self._transition_lock.acquire(
1012 timeout=wait if deadline is None else min(wait, max(deadline - time.perf_counter(), 0.0))
1013 ):
1014 if not blocking or (cancel_check is not None and cancel_check()):
1015 raise Timeout(self.lock_file)
1016 if deadline is not None and time.perf_counter() >= deadline:
1017 raise Timeout(self.lock_file)
1018 try:
1019 yield
1020 finally:
1021 self._transition_lock.release()
1023 @property
1024 def is_locked(self) -> bool:
1025 """
1026 A boolean indicating if the lock file is holding the lock currently.
1028 .. versionchanged:: 2.0.0
1030 This was previously a method and is now a property.
1032 """
1033 _ensure_current_process()
1034 return self._context.lock_file_fd is not None
1036 @property
1037 def lock_file(self) -> str:
1038 """Path to the lock file."""
1039 return self._context.lock_file
1041 def _raise_if_would_deadlock(self, canonical: str, *, timeout: float, blocking: bool) -> None:
1042 # Fail fast when a *different* live instance already holds this path in the current deadlock scope.
1043 #
1044 # Only the first, indefinitely-blocking acquire can self-deadlock this way: waiting in the OS primitive would
1045 # block on a lock this flow already owns. A finite timeout or ``blocking=False`` keeps the normal Timeout path.
1046 would_block = self._context.lock_counter == 1 and not self.is_locked and timeout < 0 and blocking
1047 if would_block and _REGISTRY.held.get(self._registry_key(canonical)) not in {None, self._holder_id}:
1048 self._context.lock_counter -= 1
1049 msg = (
1050 f"Deadlock: lock '{self.lock_file}' is already held by a different {self._deadlock_holder_desc}. "
1051 f"Use is_singleton=True to enable reentrant locking across instances."
1052 )
1053 raise RuntimeError(msg)
1055 def _registry_key(self, canonical: str) -> Hashable:
1056 return canonical if (scope := self._deadlock_scope()) is None else (scope, canonical)
1058 @staticmethod
1059 def _deadlock_scope() -> Hashable | None:
1060 # Execution unit whose own hold would deadlock a new acquire.
1061 #
1062 # ``None`` scopes holders to the thread, which the thread-local registry already separates. Async locks
1063 # override it with the running task, since one event loop thread runs many tasks and only a reacquire from
1064 # the *same* task can self-deadlock.
1065 return None
1067 def _pin_lock_path(self, canonical: str) -> None:
1068 # A path without a final component names no file. Its canonical form is a directory, and pinning that would
1069 # change the error the backend raises.
1070 self._context.claim_root = None if canonical.endswith(os.sep) else canonical
1072 def _poll_until_acquired(
1073 self,
1074 *,
1075 blocking: bool,
1076 cancel_check: Callable[[], bool] | None,
1077 timeout: float,
1078 poll_interval: float,
1079 start_time: float,
1080 ) -> None:
1081 lock_id = id(self)
1082 lock_filename = self.lock_file
1083 attempt = 0
1084 self._context.acquiring = inspect.currentframe()
1085 try:
1086 # Created once, so a directory that disappears while the acquisition waits fails it rather than building a
1087 # second lock file beside the holder's, and not on re-entry, which joins a hold whose parent may be renamed.
1088 if not self.is_locked:
1089 ensure_directory_exists(self._lock_path)
1090 while True:
1091 self._raise_if_inherited()
1092 if not self.is_locked:
1093 self._try_break_expired_lock()
1094 _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
1095 self._acquire_with_fork_tracking()
1096 self._raise_if_inherited()
1097 if self.is_locked:
1098 _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename)
1099 return
1100 if self._check_give_up(
1101 blocking=blocking,
1102 cancel_check=cancel_check,
1103 timeout=timeout,
1104 start_time=start_time,
1105 ):
1106 raise Timeout(lock_filename, self._unprobeable_holder())
1107 attempt += 1
1108 delay = self._poll_delay(poll_interval, attempt)
1109 if timeout >= 0:
1110 delay = min(delay, max(start_time + timeout - time.perf_counter(), 0.0))
1111 _LOGGER.debug("Lock %s not acquired on %s, waiting %s seconds ...", lock_id, lock_filename, delay)
1112 time.sleep(delay)
1113 finally:
1114 self._context.acquiring = None
1116 @property
1117 def _lock_path(self) -> str:
1118 # The OS resolves a relative lock_file against the working directory at each call. A chdir during an
1119 # acquisition or a hold would otherwise make the backend wait on, lock or remove a different file.
1120 return self._context.claim_root or self._context.lock_file
1122 def _try_break_expired_lock(self) -> None:
1123 # Remove the lock file once it is older than :attr:`lifetime`; only an existence lock accepts one.
1124 ...
1126 def _acquire_with_fork_tracking(self) -> None:
1127 with _fork_transition(self):
1128 try:
1129 self._acquire()
1130 except BaseException as acquisition_error:
1131 self._rollback_failed_acquire(acquisition_error)
1132 raise
1133 try:
1134 self._register_context_descriptor()
1135 except BaseException as registration_error: # pragma: needs fork
1136 self._rollback_failed_registration(registration_error)
1137 raise
1138 if self.is_locked:
1139 self._invoke_on_acquired()
1141 @abstractmethod
1142 def _acquire(self) -> None:
1143 # If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file.
1144 raise NotImplementedError
1146 def _rollback_failed_acquire(self, acquisition_error: BaseException) -> None:
1147 if not self.is_locked:
1148 return
1149 registration_error: BaseException | None = None
1150 tracking_error: BaseException | None = None
1151 try:
1152 self._register_context_descriptor()
1153 except BaseException as error: # ruff:ignore[blind-except] # preserve registration and acquisition failures
1154 registration_error = error
1155 try:
1156 # Rollback may fail too; retain the fd so a child can close it without another identity probe.
1157 self._register_unverified_context_descriptor()
1158 except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback
1159 tracking_error = error
1160 try:
1161 self._release_with_fork_tracking()
1162 except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and acquisition failures
1163 _raise_cleanup_errors(
1164 "lock acquisition cleanup failed",
1165 acquisition_error,
1166 registration_error,
1167 tracking_error,
1168 rollback_error,
1169 )
1170 if registration_error is not None: # pragma: needs fork
1171 _raise_cleanup_errors(
1172 "lock acquisition cleanup failed", acquisition_error, registration_error, tracking_error
1173 )
1175 def _register_unverified_context_descriptor(self) -> None:
1176 # The rollback only reaches here still holding a descriptor it never managed to register.
1177 if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None: # pragma: no branch
1178 if not _HAS_REGISTER_AT_FORK: # pragma: lacks fork
1179 return
1180 with _fork_transition(): # pragma: needs fork
1181 self._context.lock_file_fd_token = _record_owned_descriptor(self._context.lock_file_fd, None)
1183 def _release_with_fork_tracking(self) -> None:
1184 with _fork_transition(self):
1185 try:
1186 self._release()
1187 except BaseException as release_error:
1188 # A backend unlocks first and clears the descriptor second. A signal handler that raises in between
1189 # leaves a hold the OS dropped. A failed system call alone shows the lock is still held, so it keeps it.
1190 if self._context.lock_file_fd is not None and not _is_system_call_failure(release_error):
1191 with contextlib.suppress(Exception):
1192 self._release()
1193 raise
1194 finally:
1195 self._unregister_released_descriptor()
1197 @abstractmethod
1198 def _release(self) -> None:
1199 # Releases the lock and sets self._context.lock_file_fd to None.
1200 raise NotImplementedError
1202 def _unregister_released_descriptor(self) -> None:
1203 if self._context.lock_file_fd is None:
1204 if (token := self._context.lock_file_fd_token) is not None: # pragma: needs fork
1205 _unregister_owned_descriptor(token)
1206 self._context.lock_file_fd_token = None
1207 self._context.lock_file_fd_identity = None
1209 def _register_context_descriptor(self) -> None:
1210 if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None:
1211 self._context.lock_file_fd_token = _register_owned_descriptor(
1212 self._context.lock_file_fd,
1213 self._context.lock_file_fd_identity,
1214 )
1216 def _rollback_failed_registration(self, registration_error: BaseException) -> None: # pragma: needs fork
1217 tracking_error: BaseException | None = None
1218 try:
1219 # Rollback may fail too; retain the fd so a child can close it without another identity probe.
1220 self._register_unverified_context_descriptor()
1221 except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback
1222 tracking_error = error
1223 try:
1224 self._release_with_fork_tracking()
1225 except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and registration failures
1226 _raise_cleanup_errors(
1227 "descriptor registration cleanup failed", registration_error, tracking_error, rollback_error
1228 )
1229 if tracking_error is not None: # pragma: no cover - requires failed in-memory fallback
1230 _raise_cleanup_errors("descriptor registration cleanup failed", registration_error, tracking_error)
1232 def _invoke_on_acquired(self) -> None:
1233 # The wrapper runs in the backend executor for async locks, preserving the callback's documented thread.
1234 if self._on_acquired is None or self._context.lock_counter != 1:
1235 return
1236 try:
1237 self._on_acquired(cast("int", self._context.lock_file_fd))
1238 except BaseException as callback_error: # arbitrary caller code; roll back on any failure
1239 callback_context = callback_error.__context__
1240 try:
1241 self._release_with_fork_tracking()
1242 except BaseException as release_error: # ruff:ignore[blind-except] # both errors surface via the group below
1243 _raise_body_and_release(callback_error, release_error)
1244 callback_error.__context__ = callback_context
1245 raise
1247 def _check_give_up(
1248 self,
1249 *,
1250 blocking: bool,
1251 cancel_check: Callable[[], bool] | None,
1252 timeout: float,
1253 start_time: float,
1254 ) -> bool:
1255 lock_id, lock_filename = id(self), self.lock_file
1256 if blocking is False:
1257 _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename)
1258 return True
1259 if cancel_check is not None and cancel_check():
1260 _LOGGER.debug("Cancellation requested for lock %s on %s", lock_id, lock_filename)
1261 return True
1262 if 0 <= timeout <= time.perf_counter() - start_time:
1263 _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename)
1264 return True
1265 return False
1267 def _unprobeable_holder(self) -> str | None: # ruff:ignore[no-self-use] # existence locks override it
1268 return None
1270 def _poll_delay(self, poll_interval: float, attempt: int) -> float:
1271 # A single-file lock retries on a fixed cadence. A backend that publishes several files per acquisition sets a
1272 # cap, and then contending processes back off across a jittered, exponentially widening window instead of
1273 # colliding on every poll; poll_interval stays the floor so a lone waiter is still responsive.
1274 if not self._poll_backoff_cap:
1275 return poll_interval
1276 # Cap the exponent before doubling: under heavy contention attempt reaches the thousands, and 2**attempt would
1277 # overflow the float multiply long before the window itself stops growing past the cap.
1278 window = min(
1279 self._poll_backoff_cap, poll_interval * 2 ** min(attempt, _MAX_BACKOFF_EXPONENT)
1280 ) # pragma: needs hard-link
1281 return max(poll_interval, secrets.randbelow(int(window * 1_000_000) + 1) / 1_000_000) # pragma: needs hard-link
1283 def _reconcile_failed_acquire(self, canonical: str) -> None:
1284 # An acquire that raised while still holding the native lock (a hook that failed and whose rollback could not
1285 # release) must keep the registry entry so a later release can retry the OS unlock; otherwise roll the counter
1286 # back. is_locked was already reconciled by whichever release ran.
1287 if self.is_locked:
1288 self._commit_acquire(canonical)
1289 else:
1290 self._undo_acquire()
1292 def _undo_acquire(self) -> None:
1293 # Roll back the counter after a failed acquire, dropping the registry entry once nothing holds the path.
1294 self._context.lock_counter = max(0, self._context.lock_counter - 1)
1295 if self._context.lock_counter == 0:
1296 self._drop_registry_entry()
1297 self._context.claim_root = None
1299 def _drop_registry_entry(self) -> None:
1300 # Forget the key owned by this hold without resolving a mutable path again.
1301 # Ordered so a run an interrupt cuts short repeats safely: the entry goes only while it is still ours, and the
1302 # key outlives the registry it indexes, so a later commit sees no registry and records the hold afresh.
1303 key, registry = self._context.lock_file_key, self._context.lock_file_registry
1304 if registry is not None and registry.get(key) == self._holder_id:
1305 del registry[key]
1306 self._context.lock_file_registry = None
1307 self._context.lock_file_key = None
1309 def _commit_acquire(self, canonical: str) -> None:
1310 # Record this instance as the holder once the first acquire succeeds, so peers can detect the deadlock.
1311 # Keyed on the registry rather than the counter: an on_acquired hook that acquires again bumps the counter
1312 # past 1 before this first commit runs, and must not leave the hold unregistered.
1313 if self._context.lock_file_registry is None:
1314 key = self._registry_key(canonical)
1315 # The holder scope is resolved once at commit so a later release from another flow drops the right entry.
1316 self._context.lock_file_key = key
1317 # A shared instance may be released from another thread, which must clear the entry in the acquirer's
1318 # registry: the releaser's own thread-local registry never held it.
1319 self._context.lock_file_registry = _REGISTRY.held
1320 _REGISTRY.held[key] = self._holder_id
1322 def __exit__(
1323 self,
1324 exc_type: type[BaseException] | None,
1325 exc_value: BaseException | None,
1326 traceback: TracebackType | None,
1327 ) -> None:
1328 """Release the lock, reconciling a release failure with any body failure per :attr:`context_error_policy`."""
1329 self._release_in_context(exc_value)
1331 def _release_in_context(self, body_error: BaseException | None) -> None:
1332 # Release from a context-manager exit. "chain" lets a release failure propagate with the body error already in
1333 # its __context__ (Python's default); "group" raises both as sibling leaves so neither one hides the other.
1334 try:
1335 self.release()
1336 except BaseException as release_error:
1337 if body_error is None or self._context_error_policy == "chain":
1338 raise
1339 _raise_body_and_release(body_error, release_error)
1341 def release(self, force: bool = False) -> None: # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility
1342 """
1343 Release the file lock. The lock is only completely released when the lock counter reaches 0. The lock file
1344 itself may be deleted automatically, the behavior is platform-specific.
1346 :param force: If true, the lock counter is ignored and the lock is released in every case. On a thread-local
1347 lock it also releases the holds of threads that exited without releasing; a hold of a thread still running
1348 stays with that thread. Dropping the last reference to the lock releases every thread's hold.
1350 """
1351 counter_before: int | None = None
1352 try:
1353 counter_before = self._context.lock_counter
1354 self._release_current(force=force)
1355 except BaseException as release_error:
1356 # A handler that raised after release() began and before the backend changed anything leaves the counter as
1357 # it was; only then does a second attempt release the hold the caller gave up. A failed system call keeps it
1358 # for an explicit retry, and a changed counter means a nested level or the backend's own retry already ran.
1359 if (
1360 counter_before is not None
1361 and counter_before == self._context.lock_counter
1362 and self._context.lock_file_fd is not None
1363 and not _is_system_call_failure(release_error)
1364 ):
1365 with contextlib.suppress(Exception):
1366 self._release_current(force=force)
1367 raise
1368 if force:
1369 self._release_thread_holds(exited_only=True)
1371 def _release_current(self, *, force: bool) -> None:
1372 # A shared instance releases under the same gate its acquisition ran through, so a thread entering the lock
1373 # never observes a partially torn-down owner. Only an acquirer that found the lock free holds the gate for
1374 # long, so an unheld lock returns without waiting behind it.
1375 if self._context.lock_file_fd is None:
1376 # Only a committed hold registers, so an entry without a descriptor is a release a signal handler stopped
1377 # after the unlock; left alone it refuses every other instance on this path in this thread.
1378 if self._context.lock_file_registry is not None:
1379 self._finish_interrupted_release()
1380 return
1381 with contextlib.nullcontext() if self._is_thread_local else self._transition_lock:
1382 state: Final = self._hold_state()
1383 # A signal handler can land between the backend's unlock and its clearing of the descriptor, where a second
1384 # release would close the number twice, maybe hitting a file that reused it. The flag holds the releasing
1385 # frame, so only a release still on this stack stops it, and a flag a cut-short frame left set does not.
1386 if self._creator_pid != os.getpid() or state.lock_file_fd is None or _on_this_stack(state.releasing):
1387 return
1388 if not force and state.lock_counter > 1:
1389 state.lock_counter -= 1
1390 return
1391 state.releasing = inspect.currentframe()
1392 try:
1393 _LOGGER.debug("Attempting to release lock %s on %s", id(self), self.lock_file)
1394 self._release_with_fork_tracking()
1395 finally:
1396 # A failure after the OS unlock (during close or unlink) still released the lock: the backend cleared
1397 # its descriptor, so commit the counter and registry to released even as the cleanup error propagates.
1398 # A failure that left the lock held keeps the counter so a later release can retry the OS unlock.
1399 if state.lock_file_fd is None: # pragma: no branch # a release that returns cleared the descriptor
1400 self._commit_release()
1401 state.releasing = None
1402 _LOGGER.debug("Lock %s released on %s", id(self), self.lock_file)
1404 def _finish_interrupted_release(self) -> None:
1405 # An unheld lock never waits behind an acquirer, whose own commit takes the entry over anyway.
1406 if not self._transition_lock.acquire(blocking=False):
1407 return # pragma: no cover # only a concurrent acquire of a shared instance holds the gate
1408 try:
1409 if self._context.lock_file_fd is None: # pragma: no branch # only a racing acquire fills it
1410 self._commit_release()
1411 finally:
1412 self._transition_lock.release()
1414 def _hold_state(self) -> FileLockContext:
1415 return self._context.current if isinstance(self._context, ThreadLocalFileContext) else self._context
1417 def _commit_release(self) -> None:
1418 # Record the lock as fully released: reset the recursion counter and drop the deadlock-registry entry.
1419 self._context.lock_counter = 0
1420 self._drop_registry_entry()
1421 self._context.claim_root = None
1423 def _release_thread_holds(self, *, exited_only: bool) -> None:
1424 if not isinstance(context := self._context, ThreadLocalFileContext):
1425 return
1426 # Serialized, or two threads would both close an exited thread's descriptor; copy() is atomic free-threaded.
1427 with self._transition_lock:
1428 own: Final = context.current
1429 for held, thread in self._thread_holds.copy().values():
1430 if exited_only and thread.is_alive():
1431 continue
1432 context.current = held
1433 try:
1434 self._release_current(force=True)
1435 finally:
1436 context.current = own
1438 def __del__(self) -> None:
1439 """Force-release so a dropped reference never leaks a held lock, including every thread's hold."""
1440 if self.__dict__.get("_creator_pid") != os.getpid():
1441 return # pragma: forked child
1442 # CPython runs __del__ before it clears weak references. Until then the singleton cache still hands this lock
1443 # out, and a thread that takes a hold through it would lose that hold to a release here. The cycle collector
1444 # clears the references first, and at exit no other thread runs.
1445 if (key := self._singleton_key) is not None and not sys.is_finalizing() and self._instances.get(key) is self:
1446 self._release_once_unreachable()
1447 else:
1448 self._release_abandoned_holds()
1450 def _release_once_unreachable(self) -> None:
1451 # The callback runs once the lock is gone, so it reads the state through a holder. The state alone keeps the
1452 # holder alive: a reference from the callback would keep the state, and through it the lock, alive.
1453 self._finalizer_state = _FinalizerState(state=self.__dict__)
1454 _UNREACHABLE_LOCKS.add(ref(self, partial(_finalize_unreachable, ref(self._finalizer_state), type(self))))
1456 def _release_abandoned_holds(self) -> None:
1457 # A finalizer must not raise. A release error during garbage collection would otherwise surface as an
1458 # unraisable-exception warning, attributed to whichever code triggered collection. The dropped lock still gets
1459 # best-effort cleanup; an explicit release() reports the same error to a caller who can act on it.
1460 with contextlib.suppress(Exception):
1461 self.release(force=True)
1462 # Nothing references the lock any more, so no thread can still be inside it through this instance.
1463 with contextlib.suppress(Exception):
1464 self._release_thread_holds(exited_only=False)
1466 @property
1467 def is_singleton(self) -> bool:
1468 """
1469 A flag indicating if this lock is singleton or not.
1471 .. versionadded:: 3.13.0
1473 """
1474 return self._is_singleton
1476 @property
1477 def context_error_policy(self) -> ContextErrorPolicy:
1478 """
1479 How a context manager reconciles a body failure with a release failure on exit.
1481 .. versionadded:: 3.30.0
1483 """
1484 return self._context_error_policy
1486 @property
1487 def close_error_policy(self) -> CloseErrorPolicy:
1488 """
1489 What a lock does with an ``os.close`` failure after relinquishing descriptor ownership.
1491 .. versionadded:: 3.30.0
1493 """
1494 return self._close_error_policy
1496 @property
1497 def fallback_to_soft(self) -> bool:
1498 """
1499 Whether a :class:`FileLock` falls back to :class:`SoftFileLock` when the filesystem lacks ``flock``.
1501 Only :class:`UnixFileLock` acts on it: when ``False`` an ``ENOSYS`` from ``flock`` propagates instead of
1502 switching to existence-lock semantics.
1504 .. versionadded:: 3.30.0
1506 """
1507 return self._fallback_to_soft
1509 @property
1510 def preserve_lock_file(self) -> bool:
1511 """
1512 Whether filelock promises not to unlink the lock pathname on release.
1514 When ``True``, Windows skips its post-release unlink and Unix refuses the ``ENOSYS`` soft fallback.
1515 :class:`SoftFileLock` rejects ``True`` because unlinking its marker is how it releases.
1517 .. versionadded:: 3.30.0
1519 """
1520 return self._preserve_lock_file
1522 @property
1523 def on_acquired(self) -> Callable[[int], None] | None:
1524 """
1525 The callback run with the borrowed lock descriptor once per physical acquisition, or ``None``.
1527 Native locks only. It runs after the native lock is held and backend initialization finished, before
1528 :meth:`~BaseFileLock.acquire` returns; a raise rolls back the acquisition. :class:`SoftFileLock` rejects it.
1530 .. versionadded:: 3.30.0
1532 """
1533 return self._on_acquired
1535 @property
1536 def timeout(self) -> float:
1537 """
1538 The default timeout value, in seconds.
1540 .. versionadded:: 2.0.0
1542 """
1543 return self._context.timeout
1545 @timeout.setter
1546 def timeout(self, value: float | str | bytes | bytearray) -> None:
1547 """
1548 Change the default timeout value.
1550 :param value: the new value, in seconds; a negative value or ``inf`` waits without a limit
1552 :raises TypeError: if *value* is a :class:`bool` or not a real number
1553 :raises ValueError: if *value* is too large to convert to :class:`float`
1555 .. deprecated:: 4.0.13
1557 Passing a numeric string; pass the number instead.
1559 """
1560 if isinstance(value, (str, bytes, bytearray)):
1561 warnings.warn(
1562 "setting timeout from a string is deprecated and will be removed; pass a number",
1563 DeprecationWarning,
1564 stacklevel=2,
1565 )
1566 value = float(value)
1567 self._context.timeout = _seconds("timeout", value)
1569 @property
1570 def blocking(self) -> bool:
1571 """
1572 Whether the locking is blocking or not.
1574 .. versionadded:: 3.14.0
1576 """
1577 return self._context.blocking
1579 @blocking.setter
1580 def blocking(self, value: bool) -> None:
1581 """
1582 Change the default blocking value.
1584 :param value: the new value as bool
1586 :raises TypeError: if *value* is not a bool
1588 """
1589 _check_blocking(value)
1590 self._context.blocking = value
1592 @property
1593 def poll_interval(self) -> float:
1594 """
1595 The default polling interval, in seconds.
1597 .. versionadded:: 3.24.0
1599 """
1600 return self._context.poll_interval
1602 @poll_interval.setter
1603 def poll_interval(self, value: float) -> None:
1604 """
1605 Change the default polling interval.
1607 :param value: the new value, in seconds
1609 :raises ValueError: if *value* is negative, not finite, or above :data:`threading.TIMEOUT_MAX`
1610 :raises TypeError: if *value* is a :class:`bool` or not a real number
1612 """
1613 self._context.poll_interval = _resolve_poll_interval(value)
1615 @property
1616 def lifetime(self) -> float | None:
1617 """
1618 The soft marker age in seconds that permits expiry, or ``None`` to disable age-based expiry.
1620 A non-``None`` value permits a waiter to enter while the previous holder remains active, so it does not provide
1621 strict mutual exclusion. Native locks ignore the value with a warning.
1623 .. versionadded:: 3.24.0
1625 """
1626 return self._context.lifetime
1628 @lifetime.setter
1629 def lifetime(self, value: float | None) -> None:
1630 """
1631 Change the legacy age-based expiry threshold.
1633 :param value: the new value in seconds, or ``None`` to disable expiration
1635 :raises ValueError: if *value* is negative or not finite
1636 :raises TypeError: if *value* is not ``None`` and not a real number
1638 """
1639 self._context.lifetime = _resolve_lifetime(value, type(self), stacklevel=3)
1641 @property
1642 def mode(self) -> int:
1643 """
1644 The file permissions requested for the lock file.
1646 Without an explicit ``mode`` this reports ``0o644`` for backwards compatibility, but the file is created with
1647 ``0o666`` filtered through the umask and default ACLs, so with a ``002`` umask it ends up ``0o664``. Check
1648 :attr:`has_explicit_mode` before relying on the value, or ``os.stat`` the file for its actual permissions.
1650 """
1651 return 0o644 if self._context.mode == _UNSET_FILE_MODE else self._context.mode
1653 @property
1654 def has_explicit_mode(self) -> bool:
1655 """Whether the file permissions were explicitly set."""
1656 return self._context.mode != _UNSET_FILE_MODE
1658 @property
1659 def lock_counter(self) -> int:
1660 """The number of times this lock has been acquired (but not yet released)."""
1661 _ensure_current_process()
1662 return self._context.lock_counter
1664 @classmethod
1665 def _reset_class_after_fork(cls) -> None: # pragma: forked child
1666 cls._instances = WeakValueDictionary()
1667 cls._instances_lock = RLock()
1668 cls._instances_under_construction = set()
1670 def _singleton_extra_mismatches( # ruff:ignore[no-self-use] # the base class adds no options of its own
1671 self, _kwargs: Mapping[str, _ExtraValue], /
1672 ) -> dict[str, tuple[str, str]]:
1673 # A subclass's own options reach the metaclass only as kwargs, and only the subclass knows their defaults and
1674 # how __init__ resolves them, so each override compares its options and merges what super() reports.
1675 return {}
1677 def is_thread_local(self) -> bool:
1678 """:returns: a flag indicating if this lock is thread local or not"""
1679 return self._is_thread_local
1681 def _close_released_fd(self, fd: int, *, default_suppresses: bool) -> None:
1682 # CPython never retries close() after EINTR because the descriptor number may already be reused, so neither does
1683 # this. close_error_policy decides the error's fate after the backend relinquishes descriptor ownership.
1684 try:
1685 os.close(fd)
1686 except OSError:
1687 if not self._suppresses_close_error(default_suppresses=default_suppresses):
1688 raise
1690 def _suppresses_close_error(self, *, default_suppresses: bool) -> bool:
1691 return self._close_error_policy == "suppress" or (self._close_error_policy == "default" and default_suppresses)
1693 def _open_mode(self) -> int:
1694 # Mode for ``os.open``: 0o666 when unset so umask and ACLs decide, otherwise the explicit mode.
1695 return 0o666 if self._context.mode == _UNSET_FILE_MODE else self._context.mode
1697 def _mark_descriptor_owned(self, fd: int, identity: tuple[int, int] | None = None) -> None:
1698 state: Final = self._hold_state()
1699 # Held first, pending cleared second: an interrupt between the two then leaves the descriptor recorded.
1700 state.lock_file_fd = fd
1701 state.lock_file_fd_identity = identity
1702 state.pending_lock_file_fd = None
1703 state.pending_lock_file_fd_identity = None
1704 if isinstance(self._context, ThreadLocalFileContext):
1705 self._thread_pending.pop(id(state), None)
1706 self._thread_holds[id(state)] = state, current_thread()
1708 def _mark_descriptor_pending(self, fd: int, identity: tuple[int, int] | None = None) -> None:
1709 state: Final = self._hold_state()
1710 state.pending_lock_file_fd = fd
1711 state.pending_lock_file_fd_identity = identity
1712 if isinstance(self._context, ThreadLocalFileContext):
1713 self._thread_pending[id(state)] = state, current_thread()
1715 def _mark_descriptor_released(self) -> None:
1716 state: Final = self._hold_state()
1717 state.pending_lock_file_fd = None
1718 state.pending_lock_file_fd_identity = None
1719 state.lock_file_fd = None
1720 state.lock_file_fd_identity = None
1721 if isinstance(self._context, ThreadLocalFileContext):
1722 self._thread_pending.pop(id(state), None)
1723 self._thread_holds.pop(id(state), None)
1725 def _reset_after_fork_in_child(self) -> None: # pragma: forked child
1726 # fork copies the lock in whatever state the parent's threads left it, so give the child an unheld one.
1727 self._transition_lock = RLock()
1728 if isinstance(self._context, ThreadLocalFileContext):
1729 self._thread_holds.clear()
1730 self._thread_pending.clear()
1731 self._context.owner_claim_paths = ()
1732 self._context.claim_root = None
1733 self._context.lock_file_fd = None
1734 self._context.lock_file_fd_token = None
1735 self._context.lock_file_fd_identity = None
1736 self._context.pending_lock_file_fd = None
1737 self._context.pending_lock_file_fd_identity = None
1738 self._context.lock_counter = 0
1739 self._context.releasing = None
1740 self._context.acquiring = None
1741 self._context.lock_file_key = None
1742 self._context.lock_file_registry = None
1743 self._context.lease_claim = None
1745 def _descriptors_for_fork(self) -> tuple[tuple[int, tuple[int, int] | None], ...]: # pragma: needs fork
1746 descriptors: list[tuple[int, tuple[int, int] | None]] = []
1747 if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None:
1748 descriptors.append((self._context.lock_file_fd, self._context.lock_file_fd_identity))
1749 if self._context.pending_lock_file_fd is not None:
1750 descriptors.append((self._context.pending_lock_file_fd, self._context.pending_lock_file_fd_identity))
1751 return tuple(descriptors)
1754def _resolve_read_write_timeout(timeout: float, *, blocking: bool) -> float:
1755 # 0 would still get SQLite's full busy timeout, and a truthy "false" would turn one attempt into a blocking wait.
1756 _check_blocking(blocking)
1757 if (timeout := _resolve_timeout(timeout, blocking=blocking)) < 0 and timeout != -1 and blocking:
1758 msg = f"timeout must be a non-negative number or -1, not {timeout!r}"
1759 raise ValueError(msg)
1760 return timeout
1763def _acquire_thread_lock(
1764 lock: Lock, *, blocking: bool, timeout: float, poll_interval: float, cancel_check: Callable[[], bool] | None
1765) -> bool:
1766 # Take a read-write lock's in-process lock; with *cancel_check*, ask it every *poll_interval* while waiting.
1767 if not blocking:
1768 return lock.acquire(blocking=False)
1769 if cancel_check is None:
1770 # Lock.acquire raises OverflowError past TIMEOUT_MAX, which is 49.7 days on Windows.
1771 return lock.acquire() if timeout == -1 else lock.acquire(timeout=min(timeout, TIMEOUT_MAX))
1772 deadline = None if timeout == -1 else time.perf_counter() + timeout
1773 while not lock.acquire(
1774 timeout=poll_interval if deadline is None else min(poll_interval, max(deadline - time.perf_counter(), 0))
1775 ):
1776 if cancel_check() or (deadline is not None and time.perf_counter() >= deadline):
1777 return False
1778 return True
1781def _resolve_timeout(timeout: float, *, blocking: bool) -> float:
1782 # Validate an acquire ``timeout``; every negative value waits without a limit.
1783 if math.isnan(timeout := _seconds("timeout", timeout)) and blocking:
1784 msg = "timeout must be a number of seconds, not nan"
1785 raise ValueError(msg)
1786 # The fail-fast deadlock check only sees a negative timeout as unlimited, so spell inf that way.
1787 return -1 if timeout == math.inf else timeout
1790@dataclass( # simplint: ignore[SMP701] # state is reset to None on finalize
1791 slots=True, weakref_slot=True, kw_only=True
1792)
1793class _FinalizerState:
1794 # A finalized lock's state, held for the release that runs once the lock is unreachable.
1795 state: dict[str, object] | None # simplint: ignore[SMP602] # the instance __dict__ holds any value
1798#: Weak references whose callback finishes a singleton's finalizer once the cache no longer finds it.
1799_UNREACHABLE_LOCKS: Final[set[ref[BaseFileLock]]] = set()
1802def _finalize_unreachable(holder: ref[_FinalizerState], cls: type[BaseFileLock], reference: ref[BaseFileLock]) -> None:
1803 _UNREACHABLE_LOCKS.discard(reference)
1804 if (finalizer_state := holder()) is None or (state := finalizer_state.state) is None:
1805 return # pragma: no cover # only a collector freeing a resurrected lock as part of a cycle drops the holder
1806 # The lock's dictionary points back at the holder; that cycle ends here.
1807 finalizer_state.state = None
1808 # The release methods only read and write this state, so a fresh instance takes it over. Dropping that instance
1809 # runs __del__ again, and the cache no longer finds the lock.
1810 stand_in = object.__new__(cls)
1811 stand_in.__dict__ = state
1812 del stand_in
1815def _is_system_call_failure(error: BaseException) -> bool:
1816 # A failed system call sets errno. A bare TimeoutError raised by a signal handler is an OSError without one.
1817 if _is_exception_group(error):
1818 return all(map(_is_system_call_failure, cast("_ExceptionGroupProtocol", error).exceptions))
1819 return isinstance(error, OSError) and error.errno is not None
1822def _on_this_stack(frame: FrameType | None) -> bool:
1823 if frame is None:
1824 return False
1825 caller = inspect.currentframe()
1826 while caller is not None and caller is not frame:
1827 caller = caller.f_back
1828 return caller is not None
1831# acquire() returns this wrapper instead of self so entering the with-statement does not call __enter__ a second
1832# time; returning self would re-acquire the lock in BaseFileLock.__enter__ without a matching release (issue #37).
1833class AcquireReturnProxy(Generic[_LockT]):
1834 """
1835 A context-aware object that will release the lock file when exiting.
1837 :param lock: the lock to release on exit
1838 """
1840 def __init__(self, lock: _LockT) -> None:
1841 self.lock: _LockT = lock
1843 def __enter__(self) -> _LockT:
1844 return self.lock
1846 def __exit__(
1847 self,
1848 exc_type: type[BaseException] | None,
1849 exc_value: BaseException | None,
1850 traceback: TracebackType | None,
1851 ) -> None:
1852 # Widen to the bound: ty narrows the else branch to the type variable minus BaseFileLock, not to the union rest.
1853 lock: Final = cast("BaseFileLock | ReadWriteLock | SoftReadWriteLock", self.lock)
1854 if isinstance(lock, BaseFileLock):
1855 lock._release_in_context(exc_value) # ruff:ignore[private-member-access] # forwards __exit__ to the owned lock's context release
1856 else: # a reader/writer lock does not carry a context_error_policy
1857 lock.release()
1860@dataclass # simplint: ignore[SMP702, SMP703] # local base rejects slots; public async context signature
1861class FileLockContext:
1862 # Holds the context for a ``BaseFileLock`` object.
1863 # A separate class so ThreadLocalFileContext can make the whole context thread-local.
1865 lock_file: str
1866 timeout: float
1867 mode: int
1868 blocking: bool
1869 poll_interval: float
1871 #: The lock lifetime in seconds; ``None`` means the lock never expires.
1872 lifetime: float | None = None
1874 #: File descriptor from os.open for the lock file; not None while the lock is held.
1875 lock_file_fd: int | None = None
1877 #: Registry token for the descriptor owned by this thread's context.
1878 lock_file_fd_token: int | None = None
1880 #: Identity captured by a backend that already inspected the descriptor.
1881 lock_file_fd_identity: tuple[int, int] | None = None
1883 #: Descriptor opened by a backend but not yet committed as the held lock.
1884 pending_lock_file_fd: int | None = None
1886 #: Identity captured for a descriptor whose acquisition has not committed.
1887 pending_lock_file_fd_identity: tuple[int, int] | None = None
1889 #: Depth of nested acquisitions; the lock is released only when it returns to 0.
1890 lock_counter: int = 0
1892 #: Canonical registry key captured when the first physical acquisition commits.
1893 lock_file_key: Hashable | None = None
1895 #: The acquiring thread's deadlock registry holding :attr:`lock_file_key`.
1896 lock_file_registry: dict[Hashable, int] | None = None
1898 #: Claim pathnames this owner published, removed by name on release so no holder ever unlinks a peer's claim.
1899 owner_claim_paths: tuple[str, ...] = ()
1901 #: The frame of the :meth:`BaseFileLock.release` tearing this hold down, so a re-entrant call from a signal handler
1902 #: on the same thread returns instead of closing the descriptor a second time.
1903 releasing: FrameType | None = None
1905 #: The frame of an acquire still polling for this hold, so one a signal handler starts on this thread fails fast.
1906 acquiring: FrameType | None = None
1908 #: Canonical lock path, resolved when an acquisition starts and kept until its hold ends. A waiter on a relative
1909 #: path stays in the directory it started in, and the release finds the file there, after a chdir in between.
1910 claim_root: str | None = None
1912 #: The lease claim of this hold, kept with it so a cross-thread release stops the right heartbeat.
1913 lease_claim: _LeaseClaim | None = None
1916class ThreadLocalFileContext(FileLockContext, local):
1917 # A thread local version of the ``FileLockContext`` class.
1918 #
1919 # Each thread's state lives in an ordinary :class:`FileLockContext` at :attr:`current`, which the lock also keeps,
1920 # so
1921 # ``release(force=True)`` and the finalizer can unlock a hold whose thread exited.
1922 current: FileLockContext
1924 # threading.local runs __init__ again with these arguments the first time each thread touches the context.
1925 def __init__( # ruff:ignore[too-many-arguments] # mirrors the FileLockContext fields set at construction
1926 self,
1927 *,
1928 lock_file: str,
1929 timeout: float,
1930 mode: int,
1931 blocking: bool,
1932 poll_interval: float,
1933 lifetime: float | None = None,
1934 ) -> None:
1935 self.current = FileLockContext(
1936 lock_file=lock_file,
1937 timeout=timeout,
1938 mode=mode,
1939 blocking=blocking,
1940 poll_interval=poll_interval,
1941 lifetime=lifetime,
1942 )
1945def _delegate_to_current(name: str) -> property:
1946 return property(
1947 lambda context: getattr(context.current, name),
1948 lambda context, value: setattr(context.current, name, value),
1949 )
1952for _field in fields(FileLockContext):
1953 setattr(ThreadLocalFileContext, _field.name, _delegate_to_current(_field.name))
1956@dataclass(frozen=True, slots=True, kw_only=True)
1957class _OwnedDescriptor:
1958 fd: int
1959 creator_pid: int
1960 device: int | None
1961 inode: int | None
1964class _ForkState:
1965 def __init__(self) -> None:
1966 self.gate = Condition(RLock())
1967 self.registry_lock = RLock()
1968 self.parameter_models_lock = RLock()
1969 self.transitions: dict[int, dict[int, _ForkDescriptorOwner | None]] = {}
1970 self.active_transitions = 0
1971 self.admission_closed = False
1972 self.fork_owner_depths: dict[int, int] = {}
1973 self.pinned_objects: dict[int, list[tuple[_ForkResettable, ...]]] = {}
1974 self.pinned_classes: dict[int, list[tuple[_ForkResettableClass, ...]]] = {}
1975 self.provisional_descriptor_tokens: dict[int, list[tuple[int, ...]]] = {}
1976 self.pid = os.getpid()
1978 def reset_synchronization(self) -> None: # pragma: forked child
1979 self.gate = Condition(RLock())
1980 self.registry_lock = RLock()
1981 self.parameter_models_lock = RLock()
1982 self.transitions = {}
1983 self.active_transitions = 0
1984 self.admission_closed = False
1985 self.fork_owner_depths = {}
1986 self.pinned_objects = {}
1987 self.pinned_classes = {}
1988 self.provisional_descriptor_tokens = {}
1991_FORK_OBJECTS: Final[WeakValueDictionary[int, _ForkResettable]] = WeakValueDictionary()
1992_FORK_CLASSES: Final[WeakValueDictionary[int, _ForkResettableClass]] = WeakValueDictionary()
1993_OWNED_DESCRIPTORS: Final[dict[int, _OwnedDescriptor]] = {}
1994_DESCRIPTOR_TOKENS: Final[count[int]] = count()
1995_TRANSITION_TOKENS: Final[count[int]] = count()
1996_FORK_STATE: Final[_ForkState] = _ForkState()
1997_FORK_AUDIT_EVENTS: Final[frozenset[str]] = frozenset({"os.fork", "os.forkpty"})
1998# CPython before 3.12 guards sys.settrace with one process-wide flag that stays set while audit hooks run, so a thread
1999# switch inside a Python hook makes a concurrent settrace raise. Coverage and debuggers call it on every thread start.
2000_SETTRACE_SURVIVES_AUDIT_HOOKS: Final[bool] = sys.implementation.name != "cpython" or sys.version_info >= (3, 12)
2003@cache
2004def _resolve_lazy_imports_before_shutdown() -> None:
2005 if hasattr(types, "LazyImportType"): # pragma: >=3.15 cover
2006 atexit.register(_resolve_lazy_imports)
2009def _resolve_lazy_imports() -> None: # pragma: >=3.15 cover
2010 # Interpreter shutdown clears sys.modules and sys.meta_path before it finalizes a lock still alive at exit, so a
2011 # lazy import (PEP 810) that the release path in __del__ reaches first can no longer load. Load them while it can.
2012 for name, module in list(sys.modules.items()):
2013 if name == "filelock" or name.startswith("filelock."):
2014 for attribute, value in list(module.__dict__.items()):
2015 if type(value) is types.LazyImportType:
2016 getattr(module, attribute)
2019def _register_fork_hooks() -> None:
2020 if _REGISTER_AT_FORK is None:
2021 return # pragma: lacks fork
2022 if _SETTRACE_SURVIVES_AUDIT_HOOKS: # pragma: needs fork # pragma: needs settrace-safe-audit-hooks
2023 sys.addaudithook(_audit_fork_safety)
2024 _REGISTER_AT_FORK( # pragma: needs fork
2025 before=_pin_fork_objects,
2026 after_in_parent=_resume_parent_after_fork,
2027 after_in_child=_reset_child_after_fork,
2028 )
2031@contextmanager
2032def _fork_transition(descriptor_owner: _ForkDescriptorOwner | None = None) -> Generator[None]:
2033 # No fork follows exit, and a daemon thread stopped inside this bookkeeping holds its lock forever.
2034 if not _HAS_REGISTER_AT_FORK or sys.is_finalizing():
2035 yield # pragma: lacks fork
2036 return # pragma: lacks fork
2037 _ensure_current_process() # pragma: needs fork
2038 creator_pid = os.getpid() # pragma: needs fork
2039 # A generator holding this transition can be finalized on another thread, which must close the entering thread's.
2040 thread_id: Final = get_ident() # pragma: needs fork
2041 with _FORK_STATE.gate: # pragma: needs fork
2042 while (
2043 _FORK_STATE.admission_closed
2044 and thread_id not in _FORK_STATE.fork_owner_depths
2045 and thread_id not in _FORK_STATE.transitions
2046 ):
2047 _FORK_STATE.gate.wait() # pragma: no cover - exercised in isolated interpreter
2048 token: Final = next(_TRANSITION_TOKENS)
2049 _FORK_STATE.transitions.setdefault(thread_id, {})[token] = descriptor_owner
2050 _FORK_STATE.active_transitions += 1
2051 try: # pragma: needs fork
2052 yield
2053 finally:
2054 if os.getpid() == creator_pid: # pragma: needs fork
2055 with _FORK_STATE.gate:
2056 owned: Final = _FORK_STATE.transitions[thread_id]
2057 del owned[token]
2058 if not owned:
2059 del _FORK_STATE.transitions[thread_id]
2060 _FORK_STATE.active_transitions -= 1
2061 _FORK_STATE.gate.notify_all()
2064def _register_fork_object(instance: _ForkResettable) -> None:
2065 if not _HAS_REGISTER_AT_FORK:
2066 return # pragma: lacks fork
2067 with _fork_transition(), _FORK_STATE.registry_lock: # pragma: needs fork
2068 _FORK_OBJECTS[id(instance)] = instance
2069 _refresh_owner_pins()
2072def _register_fork_class(cls: _ForkResettableClass) -> None:
2073 if not _HAS_REGISTER_AT_FORK:
2074 return # pragma: lacks fork
2075 with _fork_transition(), _FORK_STATE.registry_lock: # pragma: needs fork
2076 _FORK_CLASSES[id(cls)] = cls
2077 _refresh_owner_pins()
2080def _register_owned_descriptor(fd: int, identity: tuple[int, int] | None) -> int | None:
2081 if not _HAS_REGISTER_AT_FORK:
2082 return None # pragma: lacks fork
2083 with _fork_transition(): # pragma: needs fork
2084 if identity is None:
2085 stat_result = os.fstat(fd)
2086 identity = stat_result.st_dev, stat_result.st_ino
2087 return _record_owned_descriptor(fd, identity)
2090def _record_owned_descriptor(fd: int, identity: tuple[int, int] | None) -> int: # pragma: needs fork
2091 with _FORK_STATE.registry_lock:
2092 token = next(_DESCRIPTOR_TOKENS)
2093 _OWNED_DESCRIPTORS[token] = _OwnedDescriptor(
2094 fd=fd,
2095 creator_pid=os.getpid(),
2096 device=None if identity is None else identity[0],
2097 inode=None if identity is None else identity[1],
2098 )
2099 return token
2102def _unregister_owned_descriptor(token: int) -> None: # pragma: needs fork
2103 # No fork follows exit, and a daemon thread stopped inside this bookkeeping holds its lock forever.
2104 if sys.is_finalizing(): # pragma: no cover # only exit reaches it, after coverage stops measuring
2105 return
2106 with _fork_transition(), _FORK_STATE.registry_lock:
2107 _OWNED_DESCRIPTORS.pop(token, None)
2110def _pin_fork_objects() -> None: # pragma: needs fork
2111 _ensure_current_process()
2112 thread_id = get_ident()
2113 provisional_descriptor_tokens: list[int] = []
2114 # A signal handler that raises during the wait does not stop the fork. The after hooks pop one snapshot per
2115 # admission closed here, so the finally records the snapshot on failure too.
2116 try:
2117 with _FORK_STATE.gate:
2118 _FORK_STATE.fork_owner_depths[thread_id] = _FORK_STATE.fork_owner_depths.get(thread_id, 0) + 1
2119 _FORK_STATE.admission_closed = True
2120 while _FORK_STATE.active_transitions > len(_FORK_STATE.transitions.get(thread_id, ())):
2121 _FORK_STATE.gate.wait()
2122 transition_owners = tuple(_FORK_STATE.transitions.get(thread_id, {}).values())
2123 with _FORK_STATE.registry_lock:
2124 for token, descriptor in tuple(_OWNED_DESCRIPTORS.items()):
2125 if descriptor.creator_pid != os.getpid() or descriptor.device is not None:
2126 continue
2127 try:
2128 stat_result = os.fstat(descriptor.fd)
2129 except OSError:
2130 continue
2131 _OWNED_DESCRIPTORS[token] = _OwnedDescriptor(
2132 fd=descriptor.fd,
2133 creator_pid=descriptor.creator_pid,
2134 device=stat_result.st_dev,
2135 inode=stat_result.st_ino,
2136 )
2137 owners = {id(owner): owner for owner in transition_owners if owner is not None}
2138 provisional_descriptor_tokens.extend(
2139 starmap(
2140 _snapshot_descriptor_for_fork,
2141 (descriptor for owner in owners.values() for descriptor in owner._descriptors_for_fork()), # ruff:ignore[private-member-access] # snapshots each owner's own fork descriptors
2142 )
2143 )
2144 finally:
2145 with _FORK_STATE.registry_lock:
2146 _FORK_STATE.provisional_descriptor_tokens.setdefault(thread_id, []).append(
2147 tuple(provisional_descriptor_tokens)
2148 )
2149 _FORK_STATE.pinned_objects.setdefault(thread_id, []).append(tuple(_FORK_OBJECTS.values()))
2150 _FORK_STATE.pinned_classes.setdefault(thread_id, []).append(tuple(_FORK_CLASSES.values()))
2153def _resume_parent_after_fork() -> None: # pragma: needs fork
2154 thread_id = get_ident()
2155 try:
2156 with _FORK_STATE.registry_lock:
2157 for token in _FORK_STATE.provisional_descriptor_tokens[thread_id].pop():
2158 _OWNED_DESCRIPTORS.pop(token, None)
2159 _FORK_STATE.pinned_objects[thread_id].pop()
2160 _FORK_STATE.pinned_classes[thread_id].pop()
2161 if not _FORK_STATE.provisional_descriptor_tokens[thread_id]: # pragma: needs fork
2162 del _FORK_STATE.provisional_descriptor_tokens[thread_id]
2163 del _FORK_STATE.pinned_objects[thread_id]
2164 del _FORK_STATE.pinned_classes[thread_id]
2165 finally:
2166 # No other code reopens admission. A failure above would leave the other threads waiting for good.
2167 with _FORK_STATE.gate:
2168 # A handler that raised before the depth was stored left no entry to decrement.
2169 if (depth := _FORK_STATE.fork_owner_depths.get(thread_id, 1)) <= 1:
2170 _FORK_STATE.fork_owner_depths.pop(thread_id, None)
2171 else: # pragma: no cover - earlier at-fork callbacks may deadlock first
2172 _FORK_STATE.fork_owner_depths[thread_id] = depth - 1
2173 _FORK_STATE.admission_closed = bool(_FORK_STATE.fork_owner_depths)
2174 _FORK_STATE.gate.notify_all()
2177def _ensure_current_process() -> None:
2178 _reset_child_after_fork()
2181def _reset_child_after_fork() -> None: # pragma: forked child
2182 if (pid := os.getpid()) == _FORK_STATE.pid:
2183 return
2184 thread_id = get_ident()
2185 pinned_objects = _FORK_STATE.pinned_objects.get(thread_id, [()])[-1]
2186 pinned_classes = _FORK_STATE.pinned_classes.get(thread_id, [()])[-1]
2187 descriptors: list[_OwnedDescriptor] = []
2188 for token, descriptor in tuple(_OWNED_DESCRIPTORS.items()):
2189 if descriptor.creator_pid != pid:
2190 descriptors.append(_OWNED_DESCRIPTORS.pop(token))
2191 _FORK_STATE.reset_synchronization()
2192 _FORK_STATE.pid = pid
2193 _INIT_PARAMETER_MODELS.clear()
2194 _detach_child_state(descriptors, pinned_objects, pinned_classes)
2197def _snapshot_descriptor_for_fork(fd: int, identity: tuple[int, int] | None) -> int: # pragma: needs fork
2198 if identity is None: # pragma: needs fork
2199 try:
2200 stat_result = os.fstat(fd)
2201 except OSError:
2202 pass
2203 else:
2204 identity = stat_result.st_dev, stat_result.st_ino
2205 return _record_owned_descriptor(fd, identity)
2208def _detach_child_state( # pragma: forked child
2209 descriptors: list[_OwnedDescriptor],
2210 pinned_objects: tuple[_ForkResettable, ...],
2211 pinned_classes: tuple[_ForkResettableClass, ...],
2212) -> None:
2213 for descriptor in descriptors:
2214 if descriptor.device is None:
2215 continue
2216 try:
2217 stat_result = os.fstat(descriptor.fd)
2218 except OSError:
2219 continue
2220 if (stat_result.st_dev, stat_result.st_ino) != (descriptor.device, descriptor.inode):
2221 continue
2222 with contextlib.suppress(OSError):
2223 os.close(descriptor.fd)
2224 for instance in pinned_objects:
2225 instance._reset_after_fork_in_child() # ruff:ignore[private-member-access] # resets each pinned instance in the fork child
2226 for cls in pinned_classes:
2227 cls._reset_class_after_fork()
2228 _REGISTRY.held.clear()
2231def _refresh_owner_pins() -> None: # pragma: needs fork
2232 with _FORK_STATE.gate:
2233 fork_owner_thread_ids = tuple(_FORK_STATE.fork_owner_depths)
2234 if get_ident() not in fork_owner_thread_ids: # pragma: no cover - at-fork callbacks disable tracing
2235 return
2236 objects = tuple(_FORK_OBJECTS.values())
2237 classes = tuple(_FORK_CLASSES.values())
2238 for thread_id in fork_owner_thread_ids:
2239 if object_snapshots := _FORK_STATE.pinned_objects.get(thread_id): # pragma: needs fork
2240 object_snapshots[:] = [objects] * len(object_snapshots)
2241 _FORK_STATE.pinned_classes[thread_id][:] = [classes] * len(object_snapshots)
2244# The defaults capture the module globals: the hook outlives them at interpreter shutdown, where CPython wipes the
2245# module dict to None before the final audit events fire.
2246def _audit_fork_safety( # pragma: no cover - CPython disables tracing while Python audit hooks run
2247 event: str,
2248 _args: Unused,
2249 *,
2250 _fork_events: frozenset[str] = _FORK_AUDIT_EVENTS,
2251 _state: _ForkState = _FORK_STATE,
2252 _get_ident: Callable[[], int] = get_ident,
2253) -> None:
2254 if event in _fork_events:
2255 if _get_ident() in _state.transitions or _state.fork_owner_depths:
2256 msg = f"{event} is unsafe while filelock is changing descriptor ownership"
2257 raise RuntimeError(msg)
2258 elif event == "_posixsubprocess.fork_exec" and _get_ident() in _state.transitions:
2259 msg = "fork_exec is unsafe while filelock is changing descriptor ownership"
2260 raise RuntimeError(msg)
2263_register_fork_hooks()
2266__all__ = [
2267 "_UNSET_FILE_MODE",
2268 "AcquireReturnProxy",
2269 "BaseFileLock",
2270 "CloseErrorPolicy",
2271 "ContextErrorPolicy",
2272 "FileLockContext",
2273 "FileLockMeta",
2274 "LockOptions",
2275 "ThreadLocalFileContext",
2276 "_acquire_thread_lock",
2277 "_append_exception_context",
2278 "_canonical",
2279 "_check_blocking",
2280 "_check_timeout_max",
2281 "_delegate_to_current",
2282 "_ensure_current_process",
2283 "_fork_transition",
2284 "_grouped_errors",
2285 "_raise_body_and_release",
2286 "_raise_chained_errors",
2287 "_raise_cleanup_errors",
2288 "_raise_grouped_errors",
2289 "_register_fork_class",
2290 "_register_fork_object",
2291 "_register_owned_descriptor",
2292 "_resolve_poll_interval",
2293 "_resolve_read_write_timeout",
2294 "_resolve_timeout",
2295 "_seconds",
2296 "_timeout_behavior",
2297 "_unregister_owned_descriptor",
2298]