chore: ruff cleanup — fix 7 long-standing lint findings

Mechanical, no behavior change. `ruff check src/ tests/` now passes
with zero findings.

- cli.py:147 (SIM105) — replace `try/except SynologyError/pass` around
  the cleanup logout with `contextlib.suppress(SynologyError)`.
- compose.py:271 (B007) — drop the unused `i` from the env_list
  preview-detection loop (the apply loop below still uses enumerate).
- compose.py:329 (E501) — extract `verb = "Updated" if … else "Added"`
  into a local before the return so the f-string fits in 100 cols.
- images.py:237 (E501) — extract `stopped_name = in_use_stopped[0]`
  before the return and split the message across two f-strings.
- test_auth.py:38, 127, 140 (SIM117) — combine nested `with patch(…):`
  / `with pytest.raises(…):` into single parenthesised with-statements.

236 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 09:15:50 +02:00
parent 8878eda0b2
commit 6ba4c7ca92
4 changed files with 23 additions and 15 deletions
+2 -3
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import contextlib
import json import json
import logging import logging
import sys import sys
@@ -144,10 +145,8 @@ async def _run_setup() -> None:
client.sid = sid client.sid = sid
click.echo(click.style("Login successful!", fg="green")) click.echo(click.style("Login successful!", fg="green"))
# Logout cleanly # Logout cleanly
try: with contextlib.suppress(SynologyError):
await client.request("SYNO.API.Auth", "logout", version=6, params={}) await client.request("SYNO.API.Auth", "logout", version=6, params={})
except SynologyError:
pass
else: else:
click.echo(click.style("Login failed: no session ID returned.", fg="red"), err=True) click.echo(click.style("Login failed: no session ID returned.", fg="red"), err=True)
sys.exit(1) sys.exit(1)
@@ -268,7 +268,7 @@ def register_compose(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None
# Determine previous value and build description # Determine previous value and build description
old_value: str | None = None old_value: str | None = None
if isinstance(env_list, list): if isinstance(env_list, list):
for i, entry in enumerate(env_list): for entry in env_list:
if isinstance(entry, str) and entry.startswith(f"{var_name}="): if isinstance(entry, str) and entry.startswith(f"{var_name}="):
old_value = entry.split("=", 1)[1] old_value = entry.split("=", 1)[1]
break break
@@ -325,8 +325,9 @@ def register_compose(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None
except Exception as e: except Exception as e:
return f"Error writing compose file: {e}" return f"Error writing compose file: {e}"
verb = "Updated" if action == "update" else "Added"
return ( return (
f"{'Updated' if action == 'update' else 'Added'} env var in '{service_name}' ({project_name}):\n" f"{verb} env var in '{service_name}' ({project_name}):\n"
f" {var_name}={var_value}\n\n" f" {var_name}={var_value}\n\n"
f"Tip: Run redeploy_project('{project_name}', confirmed=True) to apply the change." f"Tip: Run redeploy_project('{project_name}', confirmed=True) to apply the change."
) )
+3 -1
View File
@@ -233,8 +233,10 @@ def register_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None:
) )
if in_use_stopped: if in_use_stopped:
stopped_name = in_use_stopped[0]
return ( return (
f"Cannot delete '{display_name}': image is used by stopped container '{in_use_stopped[0]}'.\n" f"Cannot delete '{display_name}': image is used by stopped container "
f"'{stopped_name}'.\n"
f"Delete the container first or run system_prune to clean up stopped containers." f"Delete the container first or run system_prune to clean up stopped containers."
) )
+15 -9
View File
@@ -35,9 +35,11 @@ def test_resolve_credentials_no_credentials(monkeypatch):
config = make_config() config = make_config()
auth = AuthManager(config) auth = AuthManager(config)
with patch("keyring.get_password", return_value=None): with (
with pytest.raises(AuthenticationError, match="No credentials found"): patch("keyring.get_password", return_value=None),
auth.resolve_credentials() pytest.raises(AuthenticationError, match="No credentials found"),
):
auth.resolve_credentials()
def test_resolve_credentials_from_keyring(monkeypatch): def test_resolve_credentials_from_keyring(monkeypatch):
@@ -124,9 +126,11 @@ async def test_login_2fa_required():
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.request.side_effect = SynologyError("2FA required", code=403) mock_client.request.side_effect = SynologyError("2FA required", code=403)
with patch.object(auth, "resolve_credentials", return_value=("user", "pass", None)): with (
with pytest.raises(AuthenticationError, match="2FA is required"): patch.object(auth, "resolve_credentials", return_value=("user", "pass", None)),
await auth.login(mock_client) pytest.raises(AuthenticationError, match="2FA is required"),
):
await auth.login(mock_client)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -137,9 +141,11 @@ async def test_login_no_sid_returned():
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.request.return_value = {} # No 'sid' key mock_client.request.return_value = {} # No 'sid' key
with patch.object(auth, "resolve_credentials", return_value=("user", "pass", None)): with (
with pytest.raises(AuthenticationError, match="no session ID"): patch.object(auth, "resolve_credentials", return_value=("user", "pass", None)),
await auth.login(mock_client) pytest.raises(AuthenticationError, match="no session ID"),
):
await auth.login(mock_client)
@pytest.mark.asyncio @pytest.mark.asyncio