FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(cli): add --json output to pn doctor by shramanb113 · Pull Request #76 · pythonnative/pythonnative · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .md  (2) .py  (4) All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
4 changes: 3 additions & 1 deletion docs/api/cli.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ the documented behavior never drifts from the code.
- `pn doctor [android|ios]`: diagnose the local toolchain and validate
`pythonnative.toml`, including that a `python3.X` matching
`[app].python_version` is available for package resolution. Exits
non-zero when something will block a build.
non-zero when something will block a build. Flag: `--json` to print a
JSON array of check results to stdout for scripting; the verdict line
goes to stderr instead, and the exit status is unchanged.
- `pn deps [android|ios]`: resolve `[requirements].packages` for every
device target (iOS device, iOS Simulator, and each Android ABI)
without installing anything, and report the wheel each package would
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/testing.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ See [Testing async code](async.md#testing-async-code).

## Going lower level

### A minimal fake backend

`render` is a thin layer over
[`Reconciler`][pythonnative.reconciler.Reconciler] and
[`FakeBackend`][pythonnative.testing.FakeBackend]. Tests that need to
Expand Down
48 changes: 37 additions & 11 deletions src/pythonnative/cli/pn.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
- `pn init [name]`: scaffold a new project (``pythonnative.toml`` +
``app/``) into ``./name/``, or into the current directory when no name
is given.
- `pn doctor [platform]`: diagnose the local toolchain and config.
- `pn doctor [platform]`: diagnose the local toolchain and config, as a
report or as JSON with `--json`.
- `pn deps [platform]`: resolve ``[requirements].packages`` for every
device target and report which wheels would be used (or why a package
can't be installed), without building anything.
Expand Down Expand Up @@ -264,26 +265,48 @@ def init_project(args: argparse.Namespace) -> None:
# ======================================================================


def _print_doctor_summary(level: str, stream: TextIO) -> None:
"""Print the one-line verdict for ``level`` to ``stream``.

Shared by both output modes so the wording can't drift between the
report (stdout) and ``--json`` (stderr).
"""
if level == doctor_mod.ERROR:
print("Found problems that will block builds. Address the [x] items above.", file=stream)
elif level == doctor_mod.WARN:
print("Ready, with warnings. Review the [!] items above.", file=stream)
else:
print("Everything looks good.", file=stream)


def doctor_command(args: argparse.Namespace) -> None:
"""Run toolchain/config diagnostics and exit non-zero on errors.

With ``--json``, stdout carries a JSON array and nothing else, one
object per check (see ``CheckResult.to_dict``), and the verdict line
goes to stderr. The exit status stays keyed to the worst check level
either way.

Args:
args: Parsed namespace with optional ``platform``.
args: Parsed namespace with optional ``platform`` and ``json``.
"""
platform: Optional[str] = getattr(args, "platform", None)
as_json: bool = getattr(args, "json", False)
results = doctor_mod.run_doctor(Path.cwd(), platform=platform)
print("PythonNative doctor\n")
for result in results:
print(result.format())
level = doctor_mod.worst_level(results)
print()

if as_json:
print(json.dumps([result.to_dict() for result in results], indent=2))
_print_doctor_summary(level, sys.stderr)
else:
print("PythonNative doctor\n")
for result in results:
print(result.format())
print()
_print_doctor_summary(level, sys.stdout)

if level == doctor_mod.ERROR:
print("Found problems that will block builds. Address the [x] items above.")
sys.exit(1)
if level == doctor_mod.WARN:
print("Ready, with warnings. Review the [!] items above.")
else:
print("Everything looks good.")


def app_id_command(args: argparse.Namespace) -> None:
Expand Down Expand Up @@ -1069,6 +1092,9 @@ def _build_parser() -> argparse.ArgumentParser:

parser_doctor = subparsers.add_parser("doctor", help="Diagnose the local toolchain and config")
parser_doctor.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict checks to a platform")
parser_doctor.add_argument(
"--json", action="store_true", help="Print a JSON array to stdout for scripting (the verdict goes to stderr)"
)
parser_doctor.set_defaults(func=doctor_command)

parser_deps = subparsers.add_parser(
Expand Down
4 changes: 4 additions & 0 deletions src/pythonnative/project/doctor.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def format(self) -> str:
suffix = f": {self.detail}" if self.detail else ""
return f" {symbol} {self.name}{suffix}"

def to_dict(self) -> dict[str, str]:
"""Return a JSON-serializable view of this check for ``--json``."""
return {"name": self.name, "level": self.level, "message": self.detail}


def _which_version(tool: str, version_args: List[str]) -> Optional[str]:
path = shutil.which(tool)
Expand Down
13 changes: 13 additions & 0 deletions tests/project/test_doctor.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,16 @@ def test_check_result_format() -> None:
line = doctor.CheckResult("Thing", doctor.OK, "all good").format()
assert "Thing" in line
assert "all good" in line


def test_check_result_to_dict() -> None:
assert doctor.CheckResult("Thing", doctor.WARN, "heads up").to_dict() == {
"name": "Thing",
"level": doctor.WARN,
"message": "heads up",
}
assert doctor.CheckResult("Bare", doctor.OK).to_dict() == {
"name": "Bare",
"level": doctor.OK,
"message": "",
}
82 changes: 82 additions & 0 deletions tests/test_cli.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pythonnative.cli.pn as pn_cli
from pythonnative.project.devices import Device
from pythonnative.project.doctor import CheckResult


def run_pn(args: List[str], cwd: str, env: Optional[Dict[str, str]] = None) -> "subprocess.CompletedProcess[str]":
Expand Down Expand Up @@ -772,6 +773,87 @@ def test_devices_json_serializes_awkward_field_values(
}


# `pn doctor` tests mirror the `pn devices --json` ones: run doctor_command()
# in-process with run_doctor stubbed, so the result doesn't depend on whatever
# toolchain the test machine has. The one run_pn test drives a real project.
_FAKE_CHECKS = [
CheckResult("pythonnative.toml", "ok", "com.example.demo (v0.1.0)"),
CheckResult("Host Python", "ok", "3.13.0"),
CheckResult("adb (Android platform-tools)", "warn", "not found on PATH"),
]

_FAKE_CHECKS_WITH_ERROR = _FAKE_CHECKS + [CheckResult("Xcode (xcodebuild)", "error", "not found on PATH")]


def _fake_run_doctor(
results: List[CheckResult], calls: Optional[List[Optional[str]]] = None
) -> Callable[..., List[CheckResult]]:
def _run(project_root: Path, *, platform: Optional[str] = None) -> List[CheckResult]:
if calls is not None:
calls.append(platform)
return list(results)

return _run


def test_doctor_json_emits_parseable_array(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
monkeypatch.setattr(pn_cli.doctor_mod, "run_doctor", _fake_run_doctor(_FAKE_CHECKS))

pn_cli.doctor_command(argparse.Namespace(platform=None, json=True))

payload = json.loads(capsys.readouterr().out)
assert [entry["name"] for entry in payload] == [
"pythonnative.toml",
"Host Python",
"adb (Android platform-tools)",
]
for entry in payload:
assert set(entry) == {"name", "level", "message"}
assert payload[2] == {"name": "adb (Android platform-tools)", "level": "warn", "message": "not found on PATH"}


def test_doctor_json_keeps_human_text_off_stdout(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
calls: List[Optional[str]] = []
monkeypatch.setattr(pn_cli.doctor_mod, "run_doctor", _fake_run_doctor(_FAKE_CHECKS, calls))

pn_cli.doctor_command(argparse.Namespace(platform="android", json=True))

captured = capsys.readouterr()
json.loads(captured.out)
assert calls == ["android"]
assert "PythonNative doctor" not in captured.out
for check in _FAKE_CHECKS:
assert check.format() not in captured.out
assert "Ready, with warnings" not in captured.out
assert "Ready, with warnings" in captured.err


def test_doctor_json_exit_code_and_summary_track_worst_level(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
monkeypatch.setattr(pn_cli.doctor_mod, "run_doctor", _fake_run_doctor(_FAKE_CHECKS_WITH_ERROR))

with pytest.raises(SystemExit) as info:
pn_cli.doctor_command(argparse.Namespace(platform=None, json=True))

assert info.value.code == 1
captured = capsys.readouterr()
assert [entry["level"] for entry in json.loads(captured.out)][-1] == "error"
assert "Found problems that will block builds" in captured.err
assert "Found problems" not in captured.out


def test_doctor_json_flag_is_wired_through_argparse(tmp_path: Path) -> None:
assert run_pn(["init", "my_app"], str(tmp_path)).returncode == 0
result = run_pn(["doctor", "android", "--json"], str(tmp_path / "my_app"))

assert result.returncode in (0, 1)
payload = json.loads(result.stdout)
assert {entry["level"] for entry in payload} <= {"ok", "warn", "error", "info"}


class _FakeCompletedProc:
"""Stand-in for subprocess.Popen that returns immediately from wait()."""

Expand Down
Loading

Back | FazBrowse Home | New Git URL