Merge pull request #5063 from pallets/cli-routes-domain

show subdomain or host in routes output
This commit is contained in:
David Lord 2023-04-14 09:45:20 -07:00 committed by GitHub
commit 6931b25293
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 69 additions and 49 deletions

View file

@ -44,6 +44,8 @@ Unreleased
to set the domain, which modern browsers interpret as an exact match rather than
a subdomain match. Warnings about ``localhost`` and IP addresses are also removed.
:issue:`5051`
- The ``routes`` command shows each rule's ``subdomain`` or ``host`` when domain
matching is in use. :issue:`5004`
Version 2.2.4

View file

@ -9,7 +9,7 @@ import sys
import traceback
import typing as t
from functools import update_wrapper
from operator import attrgetter
from operator import itemgetter
import click
from click.core import ParameterSource
@ -989,49 +989,62 @@ def shell_command() -> None:
@click.option(
"--sort",
"-s",
type=click.Choice(("endpoint", "methods", "rule", "match")),
type=click.Choice(("endpoint", "methods", "domain", "rule", "match")),
default="endpoint",
help=(
'Method to sort routes by. "match" is the order that Flask will match '
"routes when dispatching a request."
"Method to sort routes by. 'match' is the order that Flask will match routes"
" when dispatching a request."
),
)
@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.")
@with_appcontext
def routes_command(sort: str, all_methods: bool) -> None:
"""Show all registered routes with endpoints and methods."""
rules = list(current_app.url_map.iter_rules())
if not rules:
click.echo("No routes were registered.")
return
ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS"))
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 = []
if sort in ("endpoint", "rule"):
rules = sorted(rules, key=attrgetter(sort))
elif sort == "methods":
rules = sorted(rules, key=lambda rule: sorted(rule.methods)) # type: ignore
for rule in rules:
row = [
rule.endpoint,
", ".join(sorted((rule.methods or set()) - ignored_methods)),
]
rule_methods = [
", ".join(sorted(rule.methods - ignored_methods)) # type: ignore
for rule in rules
]
if has_domain:
row.append((rule.host if host_matching else rule.subdomain) or "")
headers = ("Endpoint", "Methods", "Rule")
widths = (
max(len(rule.endpoint) for rule in rules),
max(len(methods) for methods in rule_methods),
max(len(rule.rule) for rule in rules),
)
widths = [max(len(h), w) for h, w in zip(headers, widths)]
row = "{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}".format(*widths)
row.append(rule.rule)
rows.append(row)
click.echo(row.format(*headers).strip())
click.echo(row.format(*("-" * width for width in widths)))
headers = ["Endpoint", "Methods"]
sorts = ["endpoint", "methods"]
for rule, methods in zip(rules, rule_methods):
click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip())
if has_domain:
headers.append("Host" if host_matching else "Subdomain")
sorts.append("domain")
headers.append("Rule")
sorts.append("rule")
try:
rows.sort(key=itemgetter(sorts.index(sort)))
except ValueError:
pass
rows.insert(0, headers)
widths = [max(len(row[i]) for row in rows) for i in range(len(headers))]
rows.insert(1, ["-" * w for w in widths])
template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths))
for row in rows:
click.echo(template.format(*row))
cli = FlaskGroup(

View file

@ -433,16 +433,12 @@ class TestRoutes:
@pytest.fixture
def app(self):
app = Flask(__name__)
app.testing = True
@app.route("/get_post/<int:x>/<int:y>", methods=["GET", "POST"])
def yyy_get_post(x, y):
pass
@app.route("/zzz_post", methods=["POST"])
def aaa_post():
pass
app.add_url_rule(
"/get_post/<int:x>/<int:y>",
methods=["GET", "POST"],
endpoint="yyy_get_post",
)
app.add_url_rule("/zzz_post", methods=["POST"], endpoint="aaa_post")
return app
@pytest.fixture
@ -450,17 +446,6 @@ class TestRoutes:
cli = FlaskGroup(create_app=lambda: app)
return partial(runner.invoke, cli)
@pytest.fixture
def invoke_no_routes(self, runner):
def create_app():
app = Flask(__name__, static_folder=None)
app.testing = True
return app
cli = FlaskGroup(create_app=create_app)
return partial(runner.invoke, cli)
def expect_order(self, order, output):
# skip the header and match the start of each row
for expect, line in zip(order, output.splitlines()[2:]):
@ -493,11 +478,31 @@ class TestRoutes:
output = invoke(["routes", "--all-methods"]).output
assert "GET, HEAD, OPTIONS, POST" in output
def test_no_routes(self, invoke_no_routes):
result = invoke_no_routes(["routes"])
def test_no_routes(self, runner):
app = Flask(__name__, static_folder=None)
cli = FlaskGroup(create_app=lambda: app)
result = runner.invoke(cli, ["routes"])
assert result.exit_code == 0
assert "No routes were registered." in result.output
def test_subdomain(self, runner):
app = Flask(__name__, static_folder=None)
app.add_url_rule("/a", subdomain="a", endpoint="a")
app.add_url_rule("/b", subdomain="b", endpoint="b")
cli = FlaskGroup(create_app=lambda: app)
result = runner.invoke(cli, ["routes"])
assert result.exit_code == 0
assert "Subdomain" in result.output
def test_host(self, runner):
app = Flask(__name__, static_folder=None, host_matching=True)
app.add_url_rule("/a", host="a", endpoint="a")
app.add_url_rule("/b", host="b", endpoint="b")
cli = FlaskGroup(create_app=lambda: app)
result = runner.invoke(cli, ["routes"])
assert result.exit_code == 0
assert "Host" in result.output
def dotenv_not_available():
try: