refactor: convert f-string logging to lazy %-style format (G004)

Sweep all 1676 G004 violations across 112 files, converting
`logger.<level>(f"…{x}…")` to `logger.<level>("…%s…", x)`.

Why: ruff rule G004 was added to pyproject.toml to enforce lazy
%-style logging — defers formatting until the log level is enabled
and lets structured log tooling match the unformatted template.

Conversion preserves rendered output byte-for-byte:
- `{x}` → `%s` + `x`
- `{x!r}` / `{x!s}` / `{x!a}` → `%r` / `%s` / `%a`
- Format specs (`{x:.2f}`, `{x:>10}`) → `%s` + `format(x, 'spec')`
  (printf-style specs aren't 1:1 with Python format specs, so we
  delegate to `format()` to keep identical output)
- Literal `%` → `%%`
- Concatenated f-strings (`f"a {x} " "b"`) flattened
- Trailing kwargs (`exc_info=True`) preserved

Verified:
- `uv run ruff check --select G004` → 0 violations
- `uv run ty check -- nextcloud_mcp_server` → passes
- `uv run pytest tests/unit/` → 1010 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-13 01:12:17 +02:00
co-authored by Claude Opus 4.7
parent a4e6125d28
commit 665cb9b1eb
112 changed files with 2534 additions and 1859 deletions
+18 -16
View File
@@ -534,7 +534,7 @@ async def get_qdrant_client() -> AsyncQdrantClient:
# Detect mode and initialize client accordingly
if settings.qdrant_url:
# Network mode
logger.info(f"Using Qdrant network mode: {settings.qdrant_url}")
logger.info("Using Qdrant network mode: %s", settings.qdrant_url)
provisional = AsyncQdrantClient(
url=settings.qdrant_url,
api_key=settings.qdrant_api_key,
@@ -548,7 +548,7 @@ async def get_qdrant_client() -> AsyncQdrantClient:
else:
# Persistent local mode - use path parameter
logger.info(
f"Using Qdrant persistent mode: {settings.qdrant_location}"
"Using Qdrant persistent mode: %s", settings.qdrant_location
)
provisional = AsyncQdrantClient(path=settings.qdrant_location)
else:
@@ -580,14 +580,14 @@ async def get_qdrant_client() -> AsyncQdrantClient:
# `get_collection()`) is the only existence-probe permitted on a
# collection-scoped JWT — it returns 200 with the collection
# detail on hit and 404 on miss.
logger.debug(f"Fetching collection '{collection_name}' details...")
logger.debug("Fetching collection '%s' details...", collection_name)
collection_info = None
try:
collection_info = await provisional.get_collection(collection_name)
except UnexpectedResponse as exc:
if exc.status_code != 404:
raise
logger.debug(f"Collection '{collection_name}' not found (404).")
logger.debug("Collection '%s' not found (404).", collection_name)
except ValueError as exc:
# Local/in-memory qdrant_client raises ValueError(f"Collection
# {name} not found") instead of UnexpectedResponse — see
@@ -602,12 +602,12 @@ async def get_qdrant_client() -> AsyncQdrantClient:
# multi-user-basic CI jobs all exercise this path.
if "not found" not in str(exc):
raise
logger.debug(f"Collection '{collection_name}' not found (local mode).")
logger.debug("Collection '%s' not found (local mode).", collection_name)
if collection_info is not None:
# Collection exists - validate dimensions
logger.debug(
f"Collection '{collection_name}' found, validating dimensions..."
"Collection '%s' found, validating dimensions...", collection_name
)
# Handle both named vectors (dict) and legacy single vector
vectors = collection_info.config.params.vectors
@@ -633,8 +633,10 @@ async def get_qdrant_client() -> AsyncQdrantClient:
)
logger.info(
f"Using existing Qdrant collection: {collection_name} "
f"(dimension={actual_dimension}, model={settings.get_embedding_model_name()})"
"Using existing Qdrant collection: %s (dimension=%s, model=%s)",
collection_name,
actual_dimension,
settings.get_embedding_model_name(),
)
# Existing collections may pre-date the doc_id normalization /
@@ -658,8 +660,10 @@ async def get_qdrant_client() -> AsyncQdrantClient:
# Collection doesn't exist - create it
embedding_model = settings.get_embedding_model_name()
logger.info(
f"Collection '{collection_name}' not found, creating with "
f"dimension={expected_dimension}, model={embedding_model}..."
"Collection '%s' not found, creating with dimension=%s, model=%s...",
collection_name,
expected_dimension,
embedding_model,
)
await provisional.create_collection(
collection_name=collection_name,
@@ -678,12 +682,10 @@ async def get_qdrant_client() -> AsyncQdrantClient:
},
)
logger.info(
f"Created Qdrant collection: {collection_name}\n"
f" Dense vector dimension: {expected_dimension}\n"
f" Dense embedding model: {embedding_model}\n"
f" Sparse vectors: BM25 (for hybrid search)\n"
f" Distance: COSINE\n"
f"Background sync will index all documents with dense + sparse vectors."
"Created Qdrant collection: %s\\n Dense vector dimension: %s\\n Dense embedding model: %s\\n Sparse vectors: BM25 (for hybrid search)\\n Distance: COSINE\\nBackground sync will index all documents with dense + sparse vectors.",
collection_name,
expected_dimension,
embedding_model,
)
# Freshly created collection has no payload schema yet; pass
# {} explicitly to skip the otherwise-redundant