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
+2 -2
View File
@@ -120,11 +120,11 @@ async def get_indexed_doc_types(user_id: str) -> set[str]:
if point.payload and point.payload.get("doc_type")
}
logger.debug(f"Found indexed document types for user {user_id}: {doc_types}")
logger.debug("Found indexed document types for user %s: %s", user_id, doc_types)
return doc_types
except Exception as e:
logger.warning(f"Failed to query Qdrant for doc_types: {e}")
logger.warning("Failed to query Qdrant for doc_types: %s", e)
return set()
+16 -11
View File
@@ -100,9 +100,13 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
score_threshold = kwargs.get("score_threshold", self.score_threshold)
logger.info(
f"BM25 hybrid search: query='{query}', user={user_id}, "
f"limit={limit}, score_threshold={score_threshold}, doc_type={doc_type}, "
f"fusion={self.fusion_name}"
"BM25 hybrid search: query='%s', user=%s, limit=%s, score_threshold=%s, doc_type=%s, fusion=%s",
query,
user_id,
limit,
score_threshold,
doc_type,
self.fusion_name,
)
# Generate dense embedding for semantic search
@@ -112,7 +116,7 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
dense_embedding = await embedding_service.embed(query)
# Store for reuse by callers (e.g., viz_routes PCA visualization)
self.query_embedding = dense_embedding
logger.debug(f"Generated dense embedding (dimension={len(dense_embedding)})")
logger.debug("Generated dense embedding (dimension=%s)", len(dense_embedding))
# Generate sparse embedding for BM25 keyword search
with trace_operation("search.get_bm25_service"):
@@ -120,8 +124,8 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
with trace_operation("search.sparse_embedding_bm25"):
sparse_embedding = await bm25_service.encode_async(query)
logger.debug(
f"Generated sparse embedding "
f"({len(sparse_embedding['indices'])} non-zero terms)"
"Generated sparse embedding (%s non-zero terms)",
len(sparse_embedding["indices"]),
)
# Build Qdrant filter
@@ -189,15 +193,16 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
raise
logger.info(
f"Qdrant {self.fusion_name.upper()} fusion returned {len(search_response.points)} results "
f"(before deduplication)"
"Qdrant %s fusion returned %s results (before deduplication)",
self.fusion_name.upper(),
len(search_response.points),
)
if search_response.points:
# Log top 3 fusion scores to help with threshold tuning
top_scores = [p.score for p in search_response.points[:3]]
logger.debug(
f"Top 3 {self.fusion_name.upper()} fusion scores: {top_scores}"
"Top 3 %s fusion scores: %s", self.fusion_name.upper(), top_scores
)
# Deduplicate by (doc_id, doc_type, chunk_start, chunk_end)
@@ -233,12 +238,12 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
if len(results) >= limit:
break
logger.info(f"Returning {len(results)} unverified results after deduplication")
logger.info("Returning %s unverified results after deduplication", len(results))
if results:
result_details = [
f"{r.doc_type}_{r.id} (score={r.score:.3f}, title='{r.title}')"
for r in results[:5] # Show top 5
]
logger.debug(f"Top results: {', '.join(result_details)}")
logger.debug("Top results: %s", ", ".join(result_details))
return results
+64 -32
View File
@@ -67,20 +67,26 @@ async def _get_chunk_from_qdrant(
excerpt = point.payload.get("excerpt")
if excerpt:
logger.debug(
f"Retrieved chunk from Qdrant for {doc_type} {doc_id}: "
f"{len(excerpt)} chars"
"Retrieved chunk from Qdrant for %s %s: %s chars",
doc_type,
doc_id,
len(excerpt),
)
return str(excerpt)
logger.debug(
f"Chunk not found in Qdrant for {doc_type} {doc_id}, "
f"chunk [{chunk_start}:{chunk_end}]. Will fall back to document fetch."
"Chunk not found in Qdrant for %s %s, chunk [%s:%s]. Will fall back to document fetch.",
doc_type,
doc_id,
chunk_start,
chunk_end,
)
return None
except Exception as e:
logger.error(
f"Error querying Qdrant for chunk: {e}. Falling back to document fetch.",
"Error querying Qdrant for chunk: %s. Falling back to document fetch.",
e,
exc_info=True,
)
return None
@@ -129,8 +135,11 @@ async def _get_chunk_by_index_from_qdrant(
excerpt = point.payload.get("excerpt")
if excerpt:
logger.debug(
f"Retrieved adjacent chunk {chunk_index} from Qdrant for "
f"{doc_type} {doc_id}: {len(excerpt)} chars"
"Retrieved adjacent chunk %s from Qdrant for %s %s: %s chars",
chunk_index,
doc_type,
doc_id,
len(excerpt),
)
return str(excerpt)
@@ -138,8 +147,11 @@ async def _get_chunk_by_index_from_qdrant(
except Exception as e:
logger.debug(
f"Could not retrieve adjacent chunk {chunk_index} for "
f"{doc_type} {doc_id}: {e}"
"Could not retrieve adjacent chunk %s for %s %s: %s",
chunk_index,
doc_type,
doc_id,
e,
)
return None
@@ -181,19 +193,21 @@ async def _get_deck_metadata_from_qdrant(
stack_id = point.payload.get("stack_id")
if board_id is not None and stack_id is not None:
logger.debug(
f"Retrieved deck metadata for card {card_id}: "
f"board_id={board_id}, stack_id={stack_id}"
"Retrieved deck metadata for card %s: board_id=%s, stack_id=%s",
card_id,
board_id,
stack_id,
)
return {"board_id": int(board_id), "stack_id": int(stack_id)}
logger.debug(
f"Could not find deck metadata in Qdrant for card {card_id} "
f"(might be legacy data without board_id/stack_id)"
"Could not find deck metadata in Qdrant for card %s (might be legacy data without board_id/stack_id)",
card_id,
)
return None
except Exception as e:
logger.debug(f"Error querying Qdrant for deck metadata: {e}")
logger.debug("Error querying Qdrant for deck metadata: %s", e)
return None
@@ -389,8 +403,9 @@ async def get_chunk_with_context(
if chunk_text:
logger.info(
f"Retrieved chunk from Qdrant cache for {doc_type} {doc_id} "
f"(avoids document re-fetch/re-parse)"
"Retrieved chunk from Qdrant cache for %s %s (avoids document re-fetch/re-parse)",
doc_type,
doc_id,
)
# Fetch adjacent chunks for context expansion
@@ -495,24 +510,30 @@ async def get_chunk_with_context(
return None
logger.info(
f"Falling back to document fetch for {doc_type} {doc_id} "
f"(Qdrant cache miss, possibly legacy data)"
"Falling back to document fetch for %s %s (Qdrant cache miss, possibly legacy data)",
doc_type,
doc_id,
)
# Fetch full document text (notes, deck cards, news items, etc.)
full_text = await _fetch_document_text(nc_client, doc_id, doc_type, user_id)
if full_text is None:
logger.warning(
f"Could not fetch document text for {doc_type} {doc_id}, "
"skipping context expansion"
"Could not fetch document text for %s %s, skipping context expansion",
doc_type,
doc_id,
)
return None
# Validate offsets
if chunk_start < 0 or chunk_end > len(full_text) or chunk_start >= chunk_end:
logger.warning(
f"Invalid chunk offsets for {doc_type} {doc_id}: "
f"start={chunk_start}, end={chunk_end}, doc_len={len(full_text)}"
"Invalid chunk offsets for %s %s: start=%s, end=%s, doc_len=%s",
doc_type,
doc_id,
chunk_start,
chunk_end,
len(full_text),
)
return None
@@ -650,13 +671,18 @@ async def _fetch_document_text(
board_id=board_id, stack_id=stack_id, card_id=int(doc_id)
)
logger.debug(
f"Retrieved deck card {doc_id} using metadata "
f"(board_id={board_id}, stack_id={stack_id})"
"Retrieved deck card %s using metadata (board_id=%s, stack_id=%s)",
doc_id,
board_id,
stack_id,
)
except Exception as e:
logger.warning(
f"Failed to fetch card with metadata (board_id={board_id}, "
f"stack_id={stack_id}, card_id={doc_id}): {e}, falling back to iteration"
"Failed to fetch card with metadata (board_id=%s, stack_id=%s, card_id=%s): %s, falling back to iteration",
board_id,
stack_id,
doc_id,
e,
)
# Fallback: Iterate through all boards/stacks (for legacy data or if fast path failed)
@@ -671,7 +697,9 @@ async def _fetch_document_text(
# Skip deleted boards (soft delete: deletedAt > 0)
if board.deletedAt > 0:
logger.debug(
f"Skipping deleted board {board.id} while searching for card {doc_id}"
"Skipping deleted board %s while searching for card %s",
board.id,
doc_id,
)
continue
@@ -686,13 +714,15 @@ async def _fetch_document_text(
card = c
card_found = True
logger.debug(
f"Found deck card {doc_id} in board {board.id}, "
f"stack {stack.id} (fallback iteration)"
"Found deck card %s in board %s, stack %s (fallback iteration)",
doc_id,
board.id,
stack.id,
)
break
if not card_found:
logger.warning(f"Deck card {doc_id} not found in any board/stack")
logger.warning("Deck card %s not found in any board/stack", doc_id)
return None
# Type narrowing: card is set if we reach here
@@ -705,10 +735,12 @@ async def _fetch_document_text(
content_parts.append(card.description)
return "\n\n".join(content_parts)
else:
logger.warning(f"Unsupported doc_type for context expansion: {doc_type}")
logger.warning("Unsupported doc_type for context expansion: %s", doc_type)
return None
except Exception as e:
logger.error(f"Error fetching document {doc_type} {doc_id}: {e}", exc_info=True)
logger.error(
"Error fetching document %s %s: %s", doc_type, doc_id, e, exc_info=True
)
return None
+64 -37
View File
@@ -118,7 +118,7 @@ class PDFHighlighter:
try:
shutil.rmtree(temp_dir)
except Exception as e:
logger.warning(f"Failed to clean up temp directory {temp_dir}: {e}")
logger.warning("Failed to clean up temp directory %s: %s", temp_dir, e)
return full_text, page_boundaries
@@ -202,7 +202,7 @@ class PDFHighlighter:
try:
page_words = page.get_text("words")
except Exception as e:
logger.error(f"Failed to extract words from page: {e}")
logger.error("Failed to extract words from page: %s", e)
return 0
if not page_words:
@@ -225,8 +225,12 @@ class PDFHighlighter:
)
]
logger.debug(
f"Filtered to {len(page_words)} words in region "
f"({rx0:.0f}, {ry0:.0f}, {rx1:.0f}, {ry1:.0f})"
"Filtered to %s words in region (%s, %s, %s, %s)",
len(page_words),
format(rx0, ".0f"),
format(ry0, ".0f"),
format(rx1, ".0f"),
format(ry1, ".0f"),
)
if not page_words:
@@ -286,17 +290,19 @@ class PDFHighlighter:
if len(current_matches) >= len(chunk_words) * 0.5:
matches = current_matches
logger.debug(
f"Found match at position {start_pos}: "
f"{len(matches)}/{len(chunk_words)} words"
"Found match at position %s: %s/%s words",
start_pos,
len(matches),
len(chunk_words),
)
break # Take FIRST match, not best/longest
if not matches:
logger.debug(f"No word matches found (chunk has {len(chunk_words)} words)")
logger.debug("No word matches found (chunk has %s words)", len(chunk_words))
return 0
logger.debug(
f"Matched {len(matches)} words out of {len(chunk_words)} chunk words"
"Matched %s words out of %s chunk words", len(matches), len(chunk_words)
)
# Build rectangles from matched words
@@ -324,8 +330,9 @@ class PDFHighlighter:
# A chunk should be mostly contiguous text
if large_gaps > len(matches) * 0.3: # More than 30% have gaps
logger.debug(
f"Rejecting scattered matches: {large_gaps} large gaps "
f"out of {len(matches)} matches"
"Rejecting scattered matches: %s large gaps out of %s matches",
large_gaps,
len(matches),
)
return 0
@@ -512,12 +519,12 @@ class PDFHighlighter:
rects = page.search_for(phrase.strip())
if rects:
anchor_rect = rects[0] # Use first match
logger.debug(f"Found chunk anchor using phrase: '{phrase[:30]}...'")
logger.debug("Found chunk anchor using phrase: '%s...'", phrase[:30])
break
if not anchor_rect:
page_num = page.number + 1 if page.number is not None else "unknown"
logger.warning(f"Could not find chunk text on page {page_num}")
logger.warning("Could not find chunk text on page %s", page_num)
return 0
# Calculate chunk height based on character count
@@ -558,8 +565,10 @@ class PDFHighlighter:
fill_shape.commit()
logger.debug(
f"Added bounding box at y={chunk_rect.y0:.0f}-{chunk_rect.y1:.0f} "
f"(estimated {estimated_lines:.1f} lines)"
"Added bounding box at y=%s-%s (estimated %s lines)",
format(chunk_rect.y0, ".0f"),
format(chunk_rect.y1, ".0f"),
format(estimated_lines, ".1f"),
)
return 1
@@ -626,7 +635,9 @@ class PDFHighlighter:
# Log if page differs from stored metadata
if stored_page_number and stored_page_number != page_num:
logger.info(
f"Chunk primarily on page {page_num}, metadata says {stored_page_number}"
"Chunk primarily on page %s, metadata says %s",
page_num,
stored_page_number,
)
# Extract page text
@@ -644,8 +655,12 @@ class PDFHighlighter:
page_text_length = page_end - page_start
logger.debug(
f"Extracted {len(chunk_text)} chars on page {page_num} "
f"(offsets {page_relative_start}-{page_relative_end} of {page_text_length})"
"Extracted %s chars on page %s (offsets %s-%s of %s)",
len(chunk_text),
page_num,
page_relative_start,
page_relative_end,
page_text_length,
)
# Get page and add highlights
@@ -672,13 +687,15 @@ class PDFHighlighter:
doc.close()
logger.info(
f"Generated {len(png_bytes):,} byte image with {highlight_count} highlights"
"Generated %s byte image with %s highlights",
format(len(png_bytes), ","),
highlight_count,
)
return (png_bytes, page_num, highlight_count)
except Exception as e:
logger.error(f"Error highlighting chunk: {e}", exc_info=True)
logger.error("Error highlighting chunk: %s", e, exc_info=True)
return None
finally:
@@ -688,7 +705,9 @@ class PDFHighlighter:
shutil.rmtree(temp_pdf_path.parent)
except Exception as e:
logger.warning(
f"Failed to delete temp directory {temp_pdf_path.parent}: {e}"
"Failed to delete temp directory %s: %s",
temp_pdf_path.parent,
e,
)
@staticmethod
@@ -791,11 +810,11 @@ class PDFHighlighter:
)
results[chunk_index] = ([normalized], page_num)
logger.info(f"Computed bboxes for {len(results)}/{len(chunks)} chunks")
logger.info("Computed bboxes for %s/%s chunks", len(results), len(chunks))
return results
except Exception as e:
logger.error(f"Error computing chunk bboxes: {e}", exc_info=True)
logger.error("Error computing chunk bboxes: %s", e, exc_info=True)
return results
finally:
@@ -805,7 +824,7 @@ class PDFHighlighter:
try:
shutil.rmtree(temp_pdf_path.parent)
except Exception as e:
logger.warning(f"Failed to clean up temp dir: {e}")
logger.warning("Failed to clean up temp dir: %s", e)
@staticmethod
def highlight_chunks_batch(
@@ -853,8 +872,9 @@ class PDFHighlighter:
doc = pymupdf.open(temp_pdf_path)
logger.debug(
f"Batch highlighting: {len(chunks)} chunks, "
f"{len(page_boundaries)} pages"
"Batch highlighting: %s chunks, %s pages",
len(chunks),
len(page_boundaries),
)
# Group chunks by their target page for efficient rendering
@@ -873,7 +893,7 @@ class PDFHighlighter:
)
if not chunk_page_info:
logger.warning(f"Chunk {chunk_index}: not found on any page")
logger.warning("Chunk %s: not found on any page", chunk_index)
continue
page_num = chunk_page_info["page_num"]
@@ -881,8 +901,10 @@ class PDFHighlighter:
# Log if page differs from stored metadata
if stored_page_num and stored_page_num != page_num:
logger.debug(
f"Chunk {chunk_index}: found on page {page_num}, "
f"metadata says {stored_page_num}"
"Chunk %s: found on page %s, metadata says %s",
chunk_index,
page_num,
stored_page_num,
)
# Extract page-relative portion of chunk text
@@ -905,7 +927,7 @@ class PDFHighlighter:
)
logger.debug(
f"Chunks distributed across {len(chunks_by_page)} unique pages"
"Chunks distributed across %s unique pages", len(chunks_by_page)
)
# OPTIMIZATION: Render each page ONCE, then draw highlights using PIL
@@ -929,7 +951,9 @@ class PDFHighlighter:
page_rect = page.rect
logger.debug(
f"Page {page_num}: rendered once, processing {len(page_chunks)} chunks"
"Page %s: rendered once, processing %s chunks",
page_num,
len(page_chunks),
)
for (
@@ -949,7 +973,7 @@ class PDFHighlighter:
)
if bbox is None:
logger.warning(f"Chunk {chunk_index}: could not find bbox")
logger.warning("Chunk %s: could not find bbox", chunk_index)
continue
# Copy base image for this chunk
@@ -985,24 +1009,27 @@ class PDFHighlighter:
results[chunk_index] = (png_bytes, page_num, 1)
logger.debug(
f"Chunk {chunk_index}: {len(png_bytes):,} bytes, "
f"page {page_num}, bbox {pil_bbox}"
"Chunk %s: %s bytes, page %s, bbox %s",
chunk_index,
format(len(png_bytes), ","),
page_num,
pil_bbox,
)
except Exception as e:
logger.error(f"Chunk {chunk_index}: error - {e}")
logger.error("Chunk %s: error - %s", chunk_index, e)
continue
doc.close()
logger.info(
f"Batch highlighted {len(results)}/{len(chunks)} chunks successfully"
"Batch highlighted %s/%s chunks successfully", len(results), len(chunks)
)
return results
except Exception as e:
logger.error(f"Error in batch highlighting: {e}", exc_info=True)
logger.error("Error in batch highlighting: %s", e, exc_info=True)
return results
finally:
@@ -1011,4 +1038,4 @@ class PDFHighlighter:
try:
shutil.rmtree(temp_pdf_path.parent)
except Exception as e:
logger.warning(f"Failed to clean up temp dir: {e}")
logger.warning("Failed to clean up temp dir: %s", e)
+12 -8
View File
@@ -77,8 +77,12 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
score_threshold = kwargs.get("score_threshold", self.score_threshold)
logger.info(
f"Semantic search: query='{query}', user={user_id}, "
f"limit={limit}, score_threshold={score_threshold}, doc_type={doc_type}"
"Semantic search: query='%s', user=%s, limit=%s, score_threshold=%s, doc_type=%s",
query,
user_id,
limit,
score_threshold,
doc_type,
)
# Generate embedding for query
@@ -87,7 +91,7 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
# Store for reuse by callers (e.g., viz_routes PCA visualization)
self.query_embedding = query_embedding
logger.debug(
f"Generated embedding for query (dimension={len(query_embedding)})"
"Generated embedding for query (dimension=%s)", len(query_embedding)
)
# Build Qdrant filter
@@ -127,14 +131,14 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
raise
logger.info(
f"Qdrant returned {len(search_response.points)} results "
f"(before deduplication)"
"Qdrant returned %s results (before deduplication)",
len(search_response.points),
)
if search_response.points:
# Log top 3 scores to help with threshold tuning
top_scores = [p.score for p in search_response.points[:3]]
logger.debug(f"Top 3 similarity scores: {top_scores}")
logger.debug("Top 3 similarity scores: %s", top_scores)
# Deduplicate by (doc_id, doc_type, chunk_start, chunk_end)
# This allows multiple chunks from same doc, but removes duplicate chunks
@@ -155,12 +159,12 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
if len(results) >= limit:
break
logger.info(f"Returning {len(results)} unverified results after deduplication")
logger.info("Returning %s unverified results after deduplication", len(results))
if results:
result_details = [
f"{r.doc_type}_{r.id} (score={r.score:.3f}, title='{r.title}')"
for r in results[:5] # Show top 5
]
logger.debug(f"Top results: {', '.join(result_details)}")
logger.debug("Top results: %s", ", ".join(result_details))
return results