From d6c38346299c04fd321ba1c7dafb1153019bc661 Mon Sep 17 00:00:00 2001 From: Jiya3177 Date: Wed, 27 May 2026 13:14:15 +0530 Subject: [PATCH] feat: add json output for cli commands --- src/oss_dev/cli/app.py | 60 ++++++++++++++++++++++++++++ tests/cli/test_new_cli.py | 83 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/src/oss_dev/cli/app.py b/src/oss_dev/cli/app.py index efe45fc..969dc99 100644 --- a/src/oss_dev/cli/app.py +++ b/src/oss_dev/cli/app.py @@ -3,6 +3,7 @@ Professional Typer-based CLI with consistent UX, rich output, and actionable errors. """ +import json import re from pathlib import Path from typing import Optional @@ -43,6 +44,10 @@ def _parse_issue_number(issue_url: str) -> int: return int(match.group(1)) +def _echo_json(data: object) -> None: + typer.echo(json.dumps(data)) + + @app.callback(invoke_without_command=True) def main( version: bool = typer.Option(False, "--version", "-V", help="Show version.", callback=_version_callback, is_eager=True), @@ -57,8 +62,22 @@ def discover_repos( language: Optional[str] = typer.Option(None, "--language", "-l", help="Filter by language."), good_first_issues: bool = typer.Option(False, "--good-first-issues", "-g", help="Only repos with good first issues."), limit: int = typer.Option(10, "--limit", help="Maximum results."), + json_output: bool = typer.Option(False, "--json", help="Output structured JSON."), ) -> None: """Discover open source repositories to contribute to.""" + if json_output: + _echo_json( + { + "repositories": [], + "filters": { + "language": language, + "good_first_issues": good_first_issues, + "limit": limit, + }, + } + ) + return + typer.echo("Discovering repositories...") @@ -68,8 +87,23 @@ def discover_issues( good_first: bool = typer.Option(False, "--good-first", "-g", help="Good first issues only."), label: Optional[str] = typer.Option(None, "--label", "-l", help="Filter by label."), limit: int = typer.Option(10, "--limit", help="Maximum results."), + json_output: bool = typer.Option(False, "--json", help="Output structured JSON."), ) -> None: """Discover issues to work on.""" + if json_output: + _echo_json( + { + "issues": [], + "filters": { + "repo": repo, + "good_first": good_first, + "label": label, + "limit": limit, + }, + } + ) + return + typer.echo("Discovering issues...") @@ -79,8 +113,21 @@ def issues_list( state: str = typer.Option("open", "--state", "-s", help="Issue state: open, closed, all."), label: Optional[str] = typer.Option(None, "--label", "-l", help="Filter by label."), limit: int = typer.Option(10, "--limit", help="Maximum results."), + json_output: bool = typer.Option(False, "--json", help="Output structured JSON."), ) -> None: """List issues for a repository.""" + if json_output: + _echo_json( + { + "repo": repo, + "state": state, + "label": label, + "limit": limit, + "issues": [], + } + ) + return + typer.echo(f"Listing issues for {repo}...") @@ -97,8 +144,21 @@ def issues_show( def analyze( target: str = typer.Argument(..., help="Repository path or URL to analyze."), output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file for analysis results."), + json_output: bool = typer.Option(False, "--json", help="Output structured JSON."), ) -> None: """Analyze a repository or issue for contribution readiness.""" + if json_output: + _echo_json( + { + "target": target, + "output": str(output) if output else None, + "analysis": { + "status": "pending", + }, + } + ) + return + typer.echo(f"Analyzing {target}...") diff --git a/tests/cli/test_new_cli.py b/tests/cli/test_new_cli.py index 0023466..3617ce3 100644 --- a/tests/cli/test_new_cli.py +++ b/tests/cli/test_new_cli.py @@ -1,3 +1,5 @@ +import json + from typer.testing import CliRunner from oss_dev.cli.app import app @@ -36,3 +38,84 @@ def test_mentor_rejects_non_positive_issue_number(): assert result.exit_code != 0 assert "positive issue" in result.output assert "number" in result.output + + +def test_discover_repos_json_output(): + result = runner.invoke( + app, + ["discover", "repos", "--language", "Python", "--good-first-issues", "--limit", "5", "--json"], + ) + + assert result.exit_code == 0 + data = json.loads(result.output) + assert data == { + "repositories": [], + "filters": { + "language": "Python", + "good_first_issues": True, + "limit": 5, + }, + } + + +def test_discover_issues_json_output(): + result = runner.invoke( + app, + ["discover", "issues", "--repo", "owner/repo", "--good-first", "--label", "bug", "--limit", "7", "--json"], + ) + + assert result.exit_code == 0 + data = json.loads(result.output) + assert data == { + "issues": [], + "filters": { + "repo": "owner/repo", + "good_first": True, + "label": "bug", + "limit": 7, + }, + } + + +def test_issues_list_json_output(): + result = runner.invoke( + app, + ["issues", "list", "owner/repo", "--state", "closed", "--label", "help wanted", "--limit", "3", "--json"], + ) + + assert result.exit_code == 0 + data = json.loads(result.output) + assert data == { + "repo": "owner/repo", + "state": "closed", + "label": "help wanted", + "limit": 3, + "issues": [], + } + + +def test_analyze_json_output(): + result = runner.invoke( + app, + ["analyze", "https://github.com/owner/repo", "--output", "analysis.json", "--json"], + ) + + assert result.exit_code == 0 + data = json.loads(result.output) + assert data == { + "target": "https://github.com/owner/repo", + "output": "analysis.json", + "analysis": { + "status": "pending", + }, + } + + +def test_discover_repos_text_output_unchanged_without_json(): + result = runner.invoke( + app, + ["discover", "repos"], + ) + + assert result.exit_code == 0 + assert result.output == "Discovering repositories...\n"