Skip to content

📝 docs: say acquire() falls back to the lock's blocking attribute - #733

Open
hxperl wants to merge 2 commits into
tox-dev:mainfrom
hxperl:docs-acquire-blocking-default
Open

📝 docs: say acquire() falls back to the lock's blocking attribute#733
hxperl wants to merge 2 commits into
tox-dev:mainfrom
hxperl:docs-acquire-blocking-default

Conversation

@hxperl

@hxperl hxperl commented Sep 11, 2026

Copy link
Copy Markdown

The blocking parameter of BaseFileLock.acquire and BaseAsyncFileLock.acquire is documented as "defaults to True". The signature default is None, and None does not mean True — it means "use the lock's own blocking attribute":

if blocking is None:
    blocking = self._context.blocking

(_api.py:1032, asyncio.py:295)

That attribute is configurable at construction (FileLock(path, blocking=False)) and settable at runtime (lock.blocking = False, the setter added in 3.14.0). So for any lock that was configured as non-blocking, acquire() with no arguments is non-blocking — the opposite of what the docstring promises.

The two sibling parameters in the very same docstring already describe this correctly:

:param timeout: ..., ``None`` means use the default :attr:`~timeout` ...
:param poll_interval: ..., ``None`` means use the default :attr:`~poll_interval`

blocking was the only one of the three that stated a literal default instead of the fallback.

Reproduction

Against main @ 4efd93e, before changing anything:

import time, tempfile, os
from filelock import FileLock, Timeout

path = os.path.join(tempfile.mkdtemp(), "repro.lock")
holder = FileLock(path)
holder.acquire()                       # keep the lock held

# docstring says acquire()'s `blocking` "defaults to True",
# so this should block until `timeout` expires
lock = FileLock(path, blocking=False, timeout=5)
t0 = time.perf_counter()
try:
    lock.acquire()                     # no arguments at all
except Timeout:
    print(f"Timeout after {time.perf_counter() - t0:.3f}s")

lock2 = FileLock(path, timeout=5)
lock2.blocking = False                 # the runtime setter
t0 = time.perf_counter()
try:
    lock2.acquire()
except Timeout:
    print(f"after `lock.blocking = False`: Timeout after {time.perf_counter() - t0:.3f}s")

lock3 = FileLock(path, timeout=2)      # control: blocking left at its default
t0 = time.perf_counter()
try:
    lock3.acquire()
except Timeout:
    print(f"control: Timeout after {time.perf_counter() - t0:.3f}s")
Timeout after 0.000s  (docs imply ~5.000s of blocking)
after `lock.blocking = False`: Timeout after 0.000s
control (blocking left at its default): Timeout after 2.017s

The control case blocks for its full timeout, which is what isolates the difference to the inherited blocking value rather than to anything about the timeout path.

The change

One docstring sentence in each of _api.py and asyncio.py, reworded to the "None means use the default :attr:..." form the neighbouring parameters already use. No behaviour change, no signature change.

What I verified

  • pytest tests/test_filelock.py tests/test_async_filelock.py tests/test_subclass_options.py -p no:randomly --no-cov: 405 passed, 32 skipped — identical to the same command on the unmodified commit (I ran the baseline first, 31.28s vs 32.70s).
  • ruff format --check2 files already formatted; ruff checkAll checks passed!.
  • Full docs build with warnings-as-errors under this repo's nitpicky = True: build succeeded. The new :attr: renders as a resolved link to #filelock.BaseFileLock.blocking in both the sync and async entries, which is the only real risk in a change like this.

What I did not verify

macOS arm64, Python 3.14 only; CI will need to confirm other platforms. I did not run the full suite — it is dominated by real-time sleeps and was heading for well over an hour on this machine, so I ran the three files covering acquire/blocking semantics instead. Given the change is a docstring with no code path attached, I judged that proportionate, but say so plainly rather than implying a full green run.

A judgement call, and one thing deliberately left out

I used :attr:~blocking in `_api.py` and `:attr:`~BaseFileLock.blocking in asyncio.py, matching how each file already spells its timeout/poll_interval references rather than making the two identical. Happy to unify them if you would rather.

Separately, while checking the rendered output I noticed ReadWriteLock's blocking parameter is documented as "when True (default), retry the nonblocking attempt every poll_interval seconds...". That is a different function with a different default and I have not verified it, so it is not part of this PR — flagging it only in case it is worth a look.

hxperl and others added 2 commits September 11, 2026 13:47
The `blocking` parameter of `BaseFileLock.acquire` and
`BaseAsyncFileLock.acquire` was documented as "defaults to True", but the
signature default is `None` and `None` resolves to `self._context.blocking`,
not to `True`. That attribute is settable both at construction
(`FileLock(path, blocking=False)`) and at runtime (`lock.blocking = False`),
so for any lock configured as non-blocking the documented default is wrong.

The two sibling parameters in the same docstring already describe this
correctly ("``None`` means use the default :attr:`~timeout`" /
":attr:`~poll_interval`"); `blocking` was the only one of the three that
described a literal default instead of the fallback. Reworded to match.

No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbQSLKhdfSRcBeGWhaaV6T
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbQSLKhdfSRcBeGWhaaV6T
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant