Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions mwclient_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@
from typing import Any

import html2text
import mwclient.errors as mwclient_errors

try:
import requests.exceptions as requests_exceptions
except ImportError:
requests_exceptions = None # type: ignore[assignment]


def _handle_runtime_error(e: Exception) -> int:
"""Print a graceful CLI message for mwclient/requests errors; return exit code 1."""
if isinstance(e, mwclient_errors.MwClientError):
print(str(e), file=sys.stderr)
return 1
if requests_exceptions is not None and isinstance(
e, requests_exceptions.RequestException
):
print(str(e), file=sys.stderr)
return 1
print(f"Error: {e}", file=sys.stderr)
return 1

_ENTITY_LOCATIONS = {
"site": ("mwclient.client", "Site"),
Expand Down Expand Up @@ -361,22 +381,32 @@ def run(argv: list[str] | None = None) -> int:
except ValueError as exc:
parser.error(str(exc))

target = build_target(build_site(args), args)
try:
target = build_target(build_site(args), args)
except Exception as e:
return _handle_runtime_error(e)

methods = list_public_methods(args.command)
if args.method not in methods:
parser.error(f"Unknown method {args.command}.{args.method}")
method = getattr(target, args.method)

result = method(*positionals, **kwargs)
result = maybe_convert_markdown(args, target, result)
try:
result = method(*positionals, **kwargs)
result = maybe_convert_markdown(args, target, result)
except Exception as e:
return _handle_runtime_error(e)
if isinstance(result, Iterator):
max_items = args.max_items
count = 0
for item in result:
print_json(item, args.indent)
count += 1
if max_items is not None and count >= max_items:
break
try:
for item in result:
print_json(item, args.indent)
count += 1
if max_items is not None and count >= max_items:
break
except Exception as e:
return _handle_runtime_error(e)
return 0

if args.stream and isinstance(result, (list, tuple)):
Expand Down
43 changes: 43 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import pytest

import mwclient.errors as mwclient_errors

from mwclient_cli import cli


Expand Down Expand Up @@ -58,6 +60,47 @@ def test_parse_keyword_args_rejects_invalid():
cli.parse_keyword_args(["broken"])


def test_mwclient_error_prints_gracefully_and_returns_1(capsys):
"""Uncaught mwclient/requests errors become a clean stderr message and exit 1."""
with patch.object(cli, "build_site", side_effect=mwclient_errors.APIError("badparam", "Invalid parameter 'x'", None)):
rc = cli.run(["--host", "example.org", "site", "ping"])
assert rc == 1
err = capsys.readouterr().err
assert "Invalid parameter" in err or "badparam" in err


def test_requests_connection_error_prints_gracefully(capsys):
"""requests.exceptions (e.g. ConnectionError) get a clean stderr message and exit 1."""
import requests.exceptions

with patch.object(
cli, "build_site", side_effect=requests.exceptions.ConnectionError("Connection refused")
):
rc = cli.run(["--host", "example.org", "site", "ping"])
assert rc == 1
assert "Connection refused" in capsys.readouterr().err


def test_iterator_error_mid_stream(capsys):
"""Generator that raises mid-iteration is caught and reported gracefully."""

def failing_generator(self):
yield {"n": 0}
raise mwclient_errors.APIError("internal", "Server error", None)

DummyPage.failing = failing_generator
try:
with patch.object(cli, "build_site", return_value=DummySite()), patch.object(
cli, "list_public_methods", return_value={"failing": failing_generator}
):
rc = cli.run(["--host", "example.org", "page", "Test", "failing"])
assert rc == 1
err = capsys.readouterr().err
assert "Server error" in err or "internal" in err
finally:
del DummyPage.failing


def test_site_method_call_prints_json(capsys):
with patch.object(cli, "build_site", return_value=DummySite()), patch.object(
cli, "list_public_methods", return_value={"ping": DummySite.ping}
Expand Down