feat(cli): add --json flag to flask routes command
Adds a machine-readable output mode to `flask routes`. When --json is passed, the route table is emitted as a JSON array instead of the text table. Each entry has `endpoint`, `methods` (sorted list), and `rule`; subdomain apps also emit `subdomain`, host-matching apps emit `host`. HEAD/OPTIONS are filtered unless --all-methods is also passed. Empty apps produce `[]`. The --sort flag is honoured in JSON mode. Backwards compatible: the flag is optional and all existing invocations of `flask routes` are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ac89889f4
commit
9536b1aeef
3 changed files with 120 additions and 5 deletions
|
|
@ -3,6 +3,8 @@ Version 3.2.0
|
|||
|
||||
Unreleased
|
||||
|
||||
- The ``flask routes`` command has a ``--json`` flag to output the route
|
||||
table as a JSON array, suitable for scripting and tooling.
|
||||
- Drop support for Python 3.9. :pr:`5730`
|
||||
- Remove previously deprecated code: ``__version__``. :pr:`5648`
|
||||
- ``RequestContext`` has merged with ``AppContext``. ``RequestContext`` is now
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import ast
|
|||
import collections.abc as cabc
|
||||
import importlib.metadata
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
|
|
@ -1057,18 +1058,57 @@ def shell_command() -> None:
|
|||
),
|
||||
)
|
||||
@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.")
|
||||
@click.option("--json", "output_json", is_flag=True, help="Output routes as JSON.")
|
||||
@with_appcontext
|
||||
def routes_command(sort: str, all_methods: bool) -> None:
|
||||
"""Show all registered routes with endpoints and methods."""
|
||||
def routes_command(sort: str, all_methods: bool, output_json: bool) -> None:
|
||||
"""Show all registered routes with endpoints and methods.
|
||||
|
||||
:param sort: Column to sort by.
|
||||
:param all_methods: Include HEAD and OPTIONS in the methods column.
|
||||
:param output_json: Emit JSON instead of a text table.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
Added the ``--json`` flag.
|
||||
"""
|
||||
rules = list(current_app.url_map.iter_rules())
|
||||
ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"}
|
||||
host_matching = current_app.url_map.host_matching
|
||||
has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules)
|
||||
|
||||
if output_json:
|
||||
data: list[dict[str, t.Any]] = []
|
||||
|
||||
for rule in rules:
|
||||
entry: dict[str, t.Any] = {
|
||||
"endpoint": rule.endpoint,
|
||||
"methods": sorted((rule.methods or set()) - ignored_methods),
|
||||
"rule": rule.rule,
|
||||
}
|
||||
|
||||
if has_domain:
|
||||
if host_matching:
|
||||
entry["host"] = rule.host or ""
|
||||
else:
|
||||
entry["subdomain"] = rule.subdomain or ""
|
||||
|
||||
data.append(entry)
|
||||
|
||||
sort_key_map: dict[str, t.Callable[[dict[str, t.Any]], str]] = {
|
||||
"endpoint": lambda e: e["endpoint"],
|
||||
"methods": lambda e: ", ".join(e["methods"]),
|
||||
"domain": lambda e: e.get("host", e.get("subdomain", "")),
|
||||
"rule": lambda e: e["rule"],
|
||||
}
|
||||
if sort in sort_key_map:
|
||||
data.sort(key=sort_key_map[sort])
|
||||
|
||||
click.echo(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
if not rules:
|
||||
click.echo("No routes were registered.")
|
||||
return
|
||||
|
||||
ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"}
|
||||
host_matching = current_app.url_map.host_matching
|
||||
has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules)
|
||||
rows = []
|
||||
|
||||
for rule in rules:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# This file was part of Flask-CLI and was modified under the terms of
|
||||
# its Revised BSD License. Copyright © 2015 CERN.
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import ssl
|
||||
|
|
@ -518,6 +519,78 @@ class TestRoutes:
|
|||
assert result.exit_code == 0
|
||||
assert "Host" in result.output
|
||||
|
||||
def test_json_output(self, invoke):
|
||||
result = invoke(["routes", "--json"])
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert isinstance(data, list)
|
||||
endpoints = {r["endpoint"] for r in data}
|
||||
assert "yyy_get_post" in endpoints
|
||||
assert "aaa_post" in endpoints
|
||||
for route in data:
|
||||
assert "endpoint" in route
|
||||
assert "methods" in route
|
||||
assert "rule" in route
|
||||
assert isinstance(route["methods"], list)
|
||||
|
||||
def test_json_no_routes(self, runner):
|
||||
app = Flask(__name__, static_folder=None)
|
||||
cli = FlaskGroup(create_app=lambda: app)
|
||||
result = runner.invoke(cli, ["routes", "--json"])
|
||||
assert result.exit_code == 0
|
||||
assert json.loads(result.output) == []
|
||||
|
||||
def test_json_excludes_head_options_by_default(self, invoke):
|
||||
result = invoke(["routes", "--json"])
|
||||
data = json.loads(result.output)
|
||||
for route in data:
|
||||
assert "HEAD" not in route["methods"]
|
||||
assert "OPTIONS" not in route["methods"]
|
||||
|
||||
def test_json_all_methods(self, invoke):
|
||||
result = invoke(["routes", "--json", "--all-methods"])
|
||||
data = json.loads(result.output)
|
||||
get_post_route = next(r for r in data if r["endpoint"] == "yyy_get_post")
|
||||
assert "HEAD" in get_post_route["methods"]
|
||||
assert "OPTIONS" in get_post_route["methods"]
|
||||
|
||||
def test_json_with_subdomain(self, runner):
|
||||
app = Flask(__name__, static_folder=None)
|
||||
app.add_url_rule("/a", subdomain="a", endpoint="a")
|
||||
cli = FlaskGroup(create_app=lambda: app)
|
||||
result = runner.invoke(cli, ["routes", "--json"])
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
route_a = next(r for r in data if r["endpoint"] == "a")
|
||||
assert route_a["subdomain"] == "a"
|
||||
|
||||
def test_json_with_host_matching(self, runner):
|
||||
app = Flask(__name__, static_folder=None)
|
||||
app.url_map.host_matching = True
|
||||
app.add_url_rule("/a", host="example.com", endpoint="a")
|
||||
cli = FlaskGroup(create_app=lambda: app)
|
||||
result = runner.invoke(cli, ["routes", "--json"])
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
route_a = next(r for r in data if r["endpoint"] == "a")
|
||||
assert route_a["host"] == "example.com"
|
||||
|
||||
@pytest.mark.parametrize("sort_key,extract", [
|
||||
("endpoint", lambda r: r["endpoint"]),
|
||||
("rule", lambda r: r["rule"]),
|
||||
])
|
||||
def test_json_sort(self, invoke, sort_key, extract):
|
||||
result = invoke(["routes", "--json", "-s", sort_key])
|
||||
data = json.loads(result.output)
|
||||
values = [extract(r) for r in data]
|
||||
assert values == sorted(values)
|
||||
|
||||
def test_json_sort_match_preserves_iter_rules_order(self, app, invoke):
|
||||
match_result = invoke(["routes", "--json", "-s", "match"])
|
||||
data = json.loads(match_result.output)
|
||||
expected_order = [rule.endpoint for rule in app.url_map.iter_rules()]
|
||||
assert [r["endpoint"] for r in data] == expected_order
|
||||
|
||||
|
||||
def dotenv_not_available():
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue