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
+42 -42
View File
@@ -19,7 +19,7 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
logger.info("Available MCP tools:")
tool_names = []
for tool in tools.tools:
logger.info(f" - {tool.name}: {tool.description}")
logger.info(" - %s: %s", tool.name, tool.description)
tool_names.append(tool.name)
# Verify expected tools are present
@@ -88,7 +88,7 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
logger.info("\nAvailable resource templates:")
template_uris = []
for template in templates.resourceTemplates:
logger.info(f" - {template.uriTemplate}")
logger.info(" - %s", template.uriTemplate)
template_uris.append(template.uriTemplate)
# Verify expected resource templates
@@ -105,7 +105,7 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
logger.info("\nAvailable resources:")
resource_uris = []
for resource in resources.resources:
logger.info(f" - {resource.uri}: {resource.name}")
logger.info(" - %s: %s", resource.uri, resource.name)
resource_uris.append(str(resource.uri)) # Convert to string for comparison
# Verify expected resources
@@ -126,7 +126,7 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
prompts = await nc_mcp_client.list_prompts()
logger.info("\nAvailable prompts:")
for prompt in prompts.prompts:
logger.info(f" - {prompt.name}")
logger.info(" - %s", prompt.name)
async def test_mcp_notes_crud_workflow(
@@ -143,7 +143,7 @@ async def test_mcp_notes_crud_workflow(
try:
# 1. Create note via MCP
logger.info(f"Creating note via MCP: {test_title}")
logger.info("Creating note via MCP: %s", test_title)
create_result = await nc_mcp_client.call_tool(
"nc_notes_create_note",
{"title": test_title, "content": test_content, "category": test_category},
@@ -157,7 +157,7 @@ async def test_mcp_notes_crud_workflow(
note_id = note_data["id"]
create_etag = note_data["etag"] # Verify create response includes ETag
logger.info(f"Note created via MCP with ID: {note_id}, ETag: {create_etag}")
logger.info("Note created via MCP with ID: %s, ETag: %s", note_id, create_etag)
assert "etag" in note_data, "Create response should include ETag"
assert create_etag, "Create ETag should not be empty"
@@ -170,7 +170,7 @@ async def test_mcp_notes_crud_workflow(
assert direct_note["category"] == test_category, "Category mismatch"
# 3. Read note via MCP
logger.info(f"Reading note via MCP: {note_id}")
logger.info("Reading note via MCP: %s", note_id)
read_result = await nc_mcp_client.call_tool(
"nc_notes_get_note", {"note_id": note_id}
)
@@ -186,7 +186,7 @@ async def test_mcp_notes_crud_workflow(
updated_content = f"Updated content: {test_content}"
etag = read_note_data["etag"]
logger.info(f"Updating note via MCP: {note_id}")
logger.info("Updating note via MCP: %s", note_id)
update_result = await nc_mcp_client.call_tool(
"nc_notes_update_note",
{
@@ -206,7 +206,7 @@ async def test_mcp_notes_crud_workflow(
updated_note_data = json.loads(update_result.content[0].text)
update_etag = updated_note_data["etag"]
logger.info(f"Note updated via MCP, new ETag: {update_etag}")
logger.info("Note updated via MCP, new ETag: %s", update_etag)
assert "etag" in updated_note_data, "Update response should include ETag"
assert update_etag, "Update ETag should not be empty"
assert update_etag != etag, "ETag should change after update"
@@ -218,7 +218,7 @@ async def test_mcp_notes_crud_workflow(
# 6. Append content via MCP
append_content = "\n\nThis is appended content via MCP."
logger.info(f"Appending content to note via MCP: {note_id}")
logger.info("Appending content to note via MCP: %s", note_id)
append_result = await nc_mcp_client.call_tool(
"nc_notes_append_content", {"note_id": note_id, "content": append_content}
)
@@ -231,7 +231,7 @@ async def test_mcp_notes_crud_workflow(
appended_note_data = json.loads(append_result.content[0].text)
append_etag = appended_note_data["etag"]
logger.info(f"Content appended via MCP, new ETag: {append_etag}")
logger.info("Content appended via MCP, new ETag: %s", append_etag)
assert "etag" in appended_note_data, "Append response should include ETag"
assert append_etag, "Append ETag should not be empty"
assert append_etag != update_etag, "ETag should change after append"
@@ -241,7 +241,7 @@ async def test_mcp_notes_crud_workflow(
assert append_content in appended_direct_note["content"]
# 8. Search for note via MCP
logger.info(f"Searching for note via MCP with query: {unique_suffix}")
logger.info("Searching for note via MCP with query: %s", unique_suffix)
search_result = await nc_mcp_client.call_tool(
"nc_notes_search_notes", {"query": unique_suffix}
)
@@ -250,7 +250,7 @@ async def test_mcp_notes_crud_workflow(
f"MCP note search failed: {search_result.content}"
)
search_notes_text = search_result.content[0].text
logger.info(f"Search result text: {search_notes_text}")
logger.info("Search result text: %s", search_notes_text)
search_response = json.loads(search_notes_text)
# Expect structured response with Pydantic format
@@ -282,7 +282,7 @@ async def test_mcp_notes_crud_workflow(
assert found_note["title"] == updated_title
# 9. Delete note via MCP
logger.info(f"Deleting note via MCP: {note_id}")
logger.info("Deleting note via MCP: %s", note_id)
delete_result = await nc_mcp_client.call_tool(
"nc_notes_delete_note", {"note_id": note_id}
)
@@ -297,7 +297,7 @@ async def test_mcp_notes_crud_workflow(
pytest.fail("Note should have been deleted but was still found")
except Exception:
# Expected - note should be deleted
logger.info(f"Successfully verified note {note_id} was deleted")
logger.info("Successfully verified note %s was deleted", note_id)
created_note = None # Mark as cleaned up
finally:
@@ -306,9 +306,9 @@ async def test_mcp_notes_crud_workflow(
try:
note_data = json.loads(created_note)
await nc_client.notes.delete_note(note_data["id"])
logger.info(f"Cleaned up note {note_data['id']} after test failure")
logger.info("Cleaned up note %s after test failure", note_data["id"])
except Exception as e:
logger.warning(f"Failed to cleanup note: {e}")
logger.warning("Failed to cleanup note: %s", e)
async def test_mcp_notes_etag_conflict(
@@ -325,7 +325,7 @@ async def test_mcp_notes_etag_conflict(
try:
# 1. Create note via MCP
logger.info(f"Creating note for ETag conflict test: {test_title}")
logger.info("Creating note for ETag conflict test: %s", test_title)
create_result = await nc_mcp_client.call_tool(
"nc_notes_create_note",
{"title": test_title, "content": test_content, "category": test_category},
@@ -355,7 +355,7 @@ async def test_mcp_notes_etag_conflict(
assert new_etag != original_etag, "ETag should have changed after update"
# 3. Try to update with the stale (original) ETag - this should fail
logger.info(f"Attempting update with stale ETag: {original_etag}")
logger.info("Attempting update with stale ETag: %s", original_etag)
conflict_result = await nc_mcp_client.call_tool(
"nc_notes_update_note",
{
@@ -382,9 +382,9 @@ async def test_mcp_notes_etag_conflict(
if created_note is not None:
try:
await nc_client.notes.delete_note(created_note["id"])
logger.info(f"Cleaned up test note {created_note['id']}")
logger.info("Cleaned up test note %s", created_note["id"])
except Exception as e:
logger.warning(f"Failed to cleanup test note: {e}")
logger.warning("Failed to cleanup test note: %s", e)
async def test_mcp_webdav_workflow(
@@ -400,7 +400,7 @@ async def test_mcp_webdav_workflow(
try:
# 1. Create directory via MCP
logger.info(f"Creating directory via MCP: {test_dir}")
logger.info("Creating directory via MCP: %s", test_dir)
create_dir_result = await nc_mcp_client.call_tool(
"nc_webdav_create_directory", {"path": test_dir}
)
@@ -415,7 +415,7 @@ async def test_mcp_webdav_workflow(
assert test_dir in dir_names, f"Directory {test_dir} not found in root listing"
# 3. Write file via MCP
logger.info(f"Writing file via MCP: {test_file_path}")
logger.info("Writing file via MCP: %s", test_file_path)
write_result = await nc_mcp_client.call_tool(
"nc_webdav_write_file",
{
@@ -437,7 +437,7 @@ async def test_mcp_webdav_workflow(
)
# 5. Read file via MCP
logger.info(f"Reading file via MCP: {test_file_path}")
logger.info("Reading file via MCP: %s", test_file_path)
read_result = await nc_mcp_client.call_tool(
"nc_webdav_read_file", {"path": test_file_path}
)
@@ -458,7 +458,7 @@ async def test_mcp_webdav_workflow(
assert direct_content.decode("utf-8") == test_content
# 7. List directory via MCP
logger.info(f"Listing directory via MCP: {test_dir}")
logger.info("Listing directory via MCP: %s", test_dir)
list_result = await nc_mcp_client.call_tool(
"nc_webdav_list_directory", {"path": test_dir}
)
@@ -467,7 +467,7 @@ async def test_mcp_webdav_workflow(
f"MCP directory listing failed: {list_result.content}"
)
listing_text = list_result.content[0].text
logger.info(f"Directory listing response: {listing_text}")
logger.info("Directory listing response: %s", listing_text)
listing_data = json.loads(listing_text)
# Extract files from DirectoryListing response
@@ -498,17 +498,17 @@ async def test_mcp_webdav_workflow(
finally:
# Cleanup
try:
logger.info(f"Cleaning up test file: {test_file_path}")
logger.info("Cleaning up test file: %s", test_file_path)
await nc_mcp_client.call_tool(
"nc_webdav_delete_resource", {"path": test_file_path}
)
logger.info(f"Cleaning up test directory: {test_dir}")
logger.info("Cleaning up test directory: %s", test_dir)
await nc_mcp_client.call_tool(
"nc_webdav_delete_resource", {"path": test_dir}
)
except Exception as e:
logger.warning(f"Failed to cleanup WebDAV resources: {e}")
logger.warning("Failed to cleanup WebDAV resources: %s", e)
async def test_mcp_resources_access(
@@ -576,8 +576,8 @@ async def test_mcp_calendar_workflow(
calendars_response = json.loads(calendars_result.content[0].text)
# Debug output to understand the structure
logger.info(f"calendars_response type: {type(calendars_response)}")
logger.info(f"calendars_response content: {calendars_response}")
logger.info("calendars_response type: %s", type(calendars_response))
logger.info("calendars_response content: %s", calendars_response)
# Expect structured response with Pydantic format
assert isinstance(calendars_response, dict), (
@@ -600,7 +600,7 @@ async def test_mcp_calendar_workflow(
# Use the first available calendar
calendar_name = calendars_list[0]["name"]
logger.info(f"Using calendar: {calendar_name}")
logger.info("Using calendar: %s", calendar_name)
# 2. Create event via MCP
from datetime import datetime, timedelta
@@ -621,7 +621,7 @@ async def test_mcp_calendar_workflow(
"priority": 5,
}
logger.info(f"Creating event via MCP: {test_event_title}")
logger.info("Creating event via MCP: %s", test_event_title)
create_result = await nc_mcp_client.call_tool(
"nc_calendar_create_event", event_data
)
@@ -634,7 +634,7 @@ async def test_mcp_calendar_workflow(
event_uid = created_event_data["uid"]
created_event = {"uid": event_uid, "calendar_name": calendar_name}
logger.info(f"Event created via MCP with UID: {event_uid}")
logger.info("Event created via MCP with UID: %s", event_uid)
# 3. Verify creation via direct NextcloudClient
direct_event, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
@@ -643,7 +643,7 @@ async def test_mcp_calendar_workflow(
assert "testing" in direct_event.get("categories", "")
# 4. Get event via MCP
logger.info(f"Getting event via MCP: {event_uid}")
logger.info("Getting event via MCP: %s", event_uid)
get_result = await nc_mcp_client.call_tool(
"nc_calendar_get_event",
{"calendar_name": calendar_name, "event_uid": event_uid},
@@ -686,8 +686,8 @@ async def test_mcp_calendar_workflow(
events_response = json.loads(list_result.content[0].text)
# Debug output to understand what nc_calendar_list_events returns
logger.info(f"list_events result type: {type(events_response)}")
logger.info(f"list_events result content: {events_response}")
logger.info("list_events result type: %s", type(events_response))
logger.info("list_events result content: %s", events_response)
# Response is now a ListEventsResponse with an "events" field
assert isinstance(events_response, dict), "Expected response dict"
@@ -748,7 +748,7 @@ async def test_mcp_calendar_workflow(
"priority": 1,
}
logger.info(f"Updating event via MCP: {event_uid}")
logger.info("Updating event via MCP: %s", event_uid)
update_result = await nc_mcp_client.call_tool(
"nc_calendar_update_event", update_data
)
@@ -784,7 +784,7 @@ async def test_mcp_calendar_workflow(
assert isinstance(upcoming_events, list), "Expected upcoming events list"
# 10. Delete event via MCP
logger.info(f"Deleting event via MCP: {event_uid}")
logger.info("Deleting event via MCP: %s", event_uid)
delete_result = await nc_mcp_client.call_tool(
"nc_calendar_delete_event",
{"calendar_name": calendar_name, "event_uid": event_uid},
@@ -800,7 +800,7 @@ async def test_mcp_calendar_workflow(
pytest.fail("Event should have been deleted but was still found")
except Exception:
# Expected - event should be deleted
logger.info(f"Successfully verified event {event_uid} was deleted")
logger.info("Successfully verified event %s was deleted", event_uid)
created_event = None # Mark as cleaned up
except Exception as e:
@@ -818,7 +818,7 @@ async def test_mcp_calendar_workflow(
created_event["calendar_name"], created_event["uid"]
)
logger.info(
f"Cleaned up event {created_event['uid']} after test failure"
"Cleaned up event %s after test failure", created_event["uid"]
)
except Exception as e:
logger.warning(f"Failed to cleanup event: {e}")
logger.warning("Failed to cleanup event: %s", e)