fix(webdav): drop anyio.Lock and add integration tests for tag exclusion
Addresses two points from the latest PR #764 review: 1. The anyio.Lock in get_excluded_file_paths bought nothing under anyio's cooperative multitasking model (single-threaded between awaits, raw set mutations are already safe). _resolve_one_tag now builds a local set of paths and appends it to a shared list — list append between awaits is safe without a lock — and the caller merges via set().union(*results) after the task group completes. This removes the cognitive overhead the reviewer flagged without changing the public API. 2. Adds tests/integration/test_tag_exclusion.py exercising the resolution pipeline end-to-end against a real Nextcloud instance: creates a system tag, tags a real file and a real directory, verifies get_excluded_file_paths resolves both via real PROPFIND + REPORT calls, and verifies is_path_excluded correctly classifies exact matches, descendants of tagged directories, and unrelated paths. Includes the disabled-feature short-circuit case. Cleanup runs in reverse order (untag, delete files); per-run uuid suffix avoids cross-run interference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d179ca8c8b
commit
35abfb2e3a
@@ -42,13 +42,15 @@ def get_excluded_tag_names() -> list[str]:
|
||||
async def _resolve_one_tag(
|
||||
tag_name: str,
|
||||
webdav: WebDAVClient,
|
||||
excluded: set[str],
|
||||
lock: anyio.Lock,
|
||||
results: list[set[str]],
|
||||
) -> None:
|
||||
"""Resolve a single tag's paths and merge into *excluded* under *lock*.
|
||||
"""Resolve a single tag's paths and append them as a set to *results*.
|
||||
|
||||
Swallows its own exceptions so a failure for one tag does not abort
|
||||
the surrounding task group (preserves fail-open per-tag semantics).
|
||||
Each task writes to a distinct slot in the shared list — append from
|
||||
cooperative tasks is safe under anyio (single-threaded between
|
||||
awaits) without an explicit lock. Swallows its own exceptions so a
|
||||
failure for one tag does not abort the surrounding task group
|
||||
(preserves fail-open per-tag semantics).
|
||||
"""
|
||||
try:
|
||||
tag = await webdav.get_tag_by_name(tag_name)
|
||||
@@ -76,16 +78,17 @@ async def _resolve_one_tag(
|
||||
)
|
||||
return
|
||||
|
||||
async with lock:
|
||||
for f in files:
|
||||
path = _normalise_path(f["path"])
|
||||
excluded.add(path)
|
||||
if f.get("is_directory"):
|
||||
logger.debug(
|
||||
"Excluding directory %r (tag %r) — descendants will be hidden",
|
||||
path,
|
||||
tag_name,
|
||||
)
|
||||
paths: set[str] = set()
|
||||
for f in files:
|
||||
path = _normalise_path(f["path"])
|
||||
paths.add(path)
|
||||
if f.get("is_directory"):
|
||||
logger.debug(
|
||||
"Excluding directory %r (tag %r) — descendants will be hidden",
|
||||
path,
|
||||
tag_name,
|
||||
)
|
||||
results.append(paths)
|
||||
|
||||
|
||||
async def get_excluded_file_paths(webdav: WebDAVClient) -> set[str]:
|
||||
@@ -96,7 +99,9 @@ async def get_excluded_file_paths(webdav: WebDAVClient) -> set[str]:
|
||||
|
||||
Per-tag resolution is fanned out via ``anyio.create_task_group`` so
|
||||
that the 2N network calls (1 PROPFIND + 1 REPORT per tag) run
|
||||
concurrently rather than serially.
|
||||
concurrently rather than serially. No lock is needed: each task
|
||||
appends its own ``set`` to a shared list, and append is atomic
|
||||
between awaits under anyio's cooperative single-threaded model.
|
||||
|
||||
**Failure mode is fail-open per tag**: if the systemtags endpoint is
|
||||
unreachable or returns an error for a given tag, that tag is skipped
|
||||
@@ -110,12 +115,12 @@ async def get_excluded_file_paths(webdav: WebDAVClient) -> set[str]:
|
||||
if not tag_names:
|
||||
return set()
|
||||
|
||||
excluded: set[str] = set()
|
||||
lock = anyio.Lock()
|
||||
results: list[set[str]] = []
|
||||
async with anyio.create_task_group() as tg:
|
||||
for tag_name in tag_names:
|
||||
tg.start_soon(_resolve_one_tag, tag_name, webdav, excluded, lock)
|
||||
tg.start_soon(_resolve_one_tag, tag_name, webdav, results)
|
||||
|
||||
excluded: set[str] = set().union(*results)
|
||||
if excluded:
|
||||
# `len(excluded)` counts directly-tagged entries — descendants of
|
||||
# tagged directories are hidden too but resolved at check time.
|
||||
|
||||
Reference in New Issue
Block a user