diff --git a/litestar/cli/commands/core.py b/litestar/cli/commands/core.py index b0a9753325..fc7cccd3b9 100644 --- a/litestar/cli/commands/core.py +++ b/litestar/cli/commands/core.py @@ -308,8 +308,9 @@ def run_command( @click.command(name="routes") @click.option("--schema", help="Include schema routes", is_flag=True, default=False) -@click.option("--exclude", help="routes to exclude via regex", type=str, is_flag=False, multiple=True) -def routes_command(app: Litestar, exclude: tuple[str, ...], schema: bool) -> None: # pragma: no cover +@click.option("--exclude", help="Routes to exclude via regex", type=str, is_flag=False, multiple=True) +@click.option("--format", "output_format", help="Output format (e.g., json, text)", default="text") +def routes_command(app: Litestar, exclude: tuple[str, ...], schema: bool, output_format: str) -> None: """Display information about the application's routes.""" sorted_routes = sorted(app.routes, key=lambda r: r.path) @@ -319,7 +320,38 @@ def routes_command(app: Litestar, exclude: tuple[str, ...], schema: bool) -> Non if exclude is not None: sorted_routes = remove_routes_with_patterns(sorted_routes, exclude) - console.print(_RouteTree(sorted_routes)) + if output_format == "json": + import inspect + import json + + routes_list = [] + for route in sorted_routes: + route_info: dict[str, Any] = { + "path": route.path, + "type": route.__class__.__name__, + "methods": sorted(getattr(route, "http_methods", [])), + } + + if isinstance(route, HTTPRoute): # pragma: no cover + route_info["handlers"] = [ + { + "name": h.name, + "path": h.paths, + "handler": h.handler_name, + "methods": sorted(h.http_methods), + "async": inspect.iscoroutinefunction(unwrap_partial(h.fn)), + } + for h in route.route_handlers + ] + + routes_list.append(route_info) + + click.echo(json.dumps(routes_list, indent=4, default=lambda o: list(o) if isinstance(o, set) else o)) + + elif output_format == "text": + console.print(_RouteTree(sorted_routes)) + else: + click.echo(f"Unsupported format: {output_format}") class _RouteTree(Tree): diff --git a/tests/unit/test_cli/test_core_commands.py b/tests/unit/test_cli/test_core_commands.py index 7ffb139980..c85b1532d7 100644 --- a/tests/unit/test_cli/test_core_commands.py +++ b/tests/unit/test_cli/test_core_commands.py @@ -1,4 +1,5 @@ import io +import json import os import re import sys @@ -668,3 +669,48 @@ def test_remove_routes_with_patterns() -> None: assert len(paths) == 2 for route in ["/", "/foo"]: assert route in paths + + +@pytest.mark.usefixtures("unset_env") +def test_routes_command_json_output(runner: CliRunner, create_app_file: CreateAppFileFixture) -> None: + """Test that the routes command supports --format=json output.""" + create_app_file("app.py", content=APP_FILE_CONTENT_ROUTES_EXAMPLE) + + result = runner.invoke(cli_command, ["routes", "--format=json"]) + + assert result.exit_code == 0 + assert result.exception is None + + try: + data = json.loads(result.output) + except json.JSONDecodeError: + pytest.fail("Output is not valid JSON") + + assert isinstance(data, list) + assert all("path" in route and "methods" in route for route in data) + assert len(data) > 0 + + +@pytest.mark.usefixtures("unset_env") +def test_routes_command_text_output(runner: CliRunner, create_app_file: CreateAppFileFixture) -> None: + """Test that the routes command supports --format=text output.""" + create_app_file("app.py", content=APP_FILE_CONTENT_ROUTES_EXAMPLE) + + result = runner.invoke(cli_command, ["routes", "--format=text"]) + + assert result.exit_code == 0 + assert result.exception is None + assert "└──" in result.output or "/" in result.output + assert "path" not in result.output.lower() + + +@pytest.mark.usefixtures("unset_env") +def test_routes_command_unsupported_format(runner: CliRunner, create_app_file: CreateAppFileFixture) -> None: + """Test that unsupported formats are handled gracefully.""" + create_app_file("app.py", content=APP_FILE_CONTENT_ROUTES_EXAMPLE) + + result = runner.invoke(cli_command, ["routes", "--format=yaml"]) + + assert result.exit_code == 0 + assert "Unsupported format" in result.output + return