add sort by match order
sort by endpoint by default combine sort flags sort methods ignore HEAD and OPTIONS methods by default rearrange columns use format to build row format string rework tests add changelog
This commit is contained in:
parent
717e45ab15
commit
7ad79583b9
4 changed files with 103 additions and 74 deletions
3
CHANGES
3
CHANGES
|
|
@ -32,6 +32,8 @@ Major release, unreleased
|
|||
- ``Flask.make_response`` raises ``TypeError`` instead of ``ValueError`` for
|
||||
bad response types. The error messages have been improved to describe why the
|
||||
type is invalid. (`#2256`_)
|
||||
- Add ``routes`` CLI command to output routes registered on the application.
|
||||
(`#2259`_)
|
||||
|
||||
.. _#1489: https://github.com/pallets/flask/pull/1489
|
||||
.. _#1898: https://github.com/pallets/flask/pull/1898
|
||||
|
|
@ -40,6 +42,7 @@ Major release, unreleased
|
|||
.. _#2223: https://github.com/pallets/flask/pull/2223
|
||||
.. _#2254: https://github.com/pallets/flask/pull/2254
|
||||
.. _#2256: https://github.com/pallets/flask/pull/2256
|
||||
.. _#2259: https://github.com/pallets/flask/pull/2259
|
||||
|
||||
Version 0.12.1
|
||||
--------------
|
||||
|
|
|
|||
72
flask/cli.py
72
flask/cli.py
|
|
@ -12,14 +12,17 @@
|
|||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from threading import Lock, Thread
|
||||
from functools import update_wrapper
|
||||
from operator import attrgetter
|
||||
from threading import Lock, Thread
|
||||
|
||||
import click
|
||||
|
||||
from ._compat import iteritems, reraise
|
||||
from .helpers import get_debug_flag
|
||||
from . import __version__
|
||||
from ._compat import iteritems, reraise
|
||||
from .globals import current_app
|
||||
from .helpers import get_debug_flag
|
||||
|
||||
|
||||
class NoAppException(click.UsageError):
|
||||
"""Raised if an application cannot be found or loaded."""
|
||||
|
|
@ -485,34 +488,51 @@ def shell_command():
|
|||
code.interact(banner=banner, local=ctx)
|
||||
|
||||
|
||||
@click.command('routes', short_help='Show routes for the app.')
|
||||
@click.option('-r', 'order_by', flag_value='rule', default=True, help='Order by route')
|
||||
@click.option('-e', 'order_by', flag_value='endpoint', help='Order by endpoint')
|
||||
@click.option('-m', 'order_by', flag_value='methods', help='Order by methods')
|
||||
@click.command('routes', short_help='Show the routes for the app.')
|
||||
@click.option(
|
||||
'--sort', '-s',
|
||||
type=click.Choice(('endpoint', 'methods', 'rule', 'match')),
|
||||
default='endpoint',
|
||||
help=(
|
||||
'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(order_by):
|
||||
"""Show all routes with endpoints and methods."""
|
||||
from flask.globals import _app_ctx_stack
|
||||
app = _app_ctx_stack.top.app
|
||||
def routes_command(sort, all_methods):
|
||||
"""Show all registered routes with endpoints and methods."""
|
||||
|
||||
order_key = lambda rule: getattr(rule, order_by)
|
||||
sorted_rules = sorted(app.url_map.iter_rules(), key=order_key)
|
||||
rules = list(current_app.url_map.iter_rules())
|
||||
ignored_methods = set(() if all_methods else ('HEAD', 'OPTIONS'))
|
||||
|
||||
max_rule = max(len(rule.rule) for rule in sorted_rules)
|
||||
max_rule = max(max_rule, len('Route'))
|
||||
max_ep = max(len(rule.endpoint) for rule in sorted_rules)
|
||||
max_ep = max(max_ep, len('Endpoint'))
|
||||
max_meth = max(len(', '.join(rule.methods)) for rule in sorted_rules)
|
||||
max_meth = max(max_meth, len('Methods'))
|
||||
if sort in ('endpoint', 'rule'):
|
||||
rules = sorted(rules, key=attrgetter(sort))
|
||||
elif sort == 'methods':
|
||||
rules = sorted(rules, key=lambda rule: sorted(rule.methods))
|
||||
|
||||
columnformat = '{:<%s} {:<%s} {:<%s}' % (max_rule, max_ep, max_meth)
|
||||
click.echo(columnformat.format('Route', 'Endpoint', 'Methods'))
|
||||
under_count = max_rule + max_ep + max_meth + 4
|
||||
click.echo('-' * under_count)
|
||||
rule_methods = [
|
||||
', '.join(sorted(rule.methods - ignored_methods)) for rule in rules
|
||||
]
|
||||
|
||||
for rule in sorted_rules:
|
||||
methods = ', '.join(rule.methods)
|
||||
click.echo(columnformat.format(rule.rule, rule.endpoint, methods))
|
||||
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)
|
||||
|
||||
click.echo(row.format(*headers).strip())
|
||||
click.echo(row.format(*('-' * width for width in widths)))
|
||||
|
||||
for rule, methods in zip(rules, rule_methods):
|
||||
click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip())
|
||||
|
||||
|
||||
cli = FlaskGroup(help="""\
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
from __future__ import absolute_import, print_function
|
||||
|
||||
from flask import Flask
|
||||
|
||||
|
||||
noroute_app = Flask('noroute app')
|
||||
simpleroute_app = Flask('simpleroute app')
|
||||
only_POST_route_app = Flask('GET route app')
|
||||
|
||||
|
||||
@simpleroute_app.route('/simpleroute')
|
||||
def simple():
|
||||
pass
|
||||
|
||||
|
||||
@only_POST_route_app.route('/only-post', methods=['POST'])
|
||||
def only_post():
|
||||
pass
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
from __future__ import absolute_import, print_function
|
||||
import os
|
||||
import sys
|
||||
from functools import partial
|
||||
|
||||
import click
|
||||
import pytest
|
||||
|
|
@ -195,7 +196,7 @@ def test_flaskgroup(runner):
|
|||
assert result.output == 'flaskgroup\n'
|
||||
|
||||
|
||||
def test_print_exceptions():
|
||||
def test_print_exceptions(runner):
|
||||
"""Print the stacktrace if the CLI."""
|
||||
def create_app(info):
|
||||
raise Exception("oh no")
|
||||
|
|
@ -205,7 +206,6 @@ def test_print_exceptions():
|
|||
def cli(**params):
|
||||
pass
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['--help'])
|
||||
assert result.exit_code == 0
|
||||
assert 'Exception: oh no' in result.output
|
||||
|
|
@ -213,34 +213,58 @@ def test_print_exceptions():
|
|||
|
||||
|
||||
class TestRoutes:
|
||||
def test_no_route(self, runner, monkeypatch):
|
||||
monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:noroute_app')
|
||||
result = runner.invoke(cli, ['routes'], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert result.output == """\
|
||||
Route Endpoint Methods
|
||||
-----------------------------------------------------
|
||||
/static/<path:filename> static HEAD, OPTIONS, GET
|
||||
"""
|
||||
@pytest.fixture
|
||||
def invoke(self, runner):
|
||||
def create_app(info):
|
||||
app = Flask(__name__)
|
||||
app.testing = True
|
||||
|
||||
def test_simple_route(self, runner, monkeypatch):
|
||||
monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:simpleroute_app')
|
||||
result = runner.invoke(cli, ['routes'], catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert result.output == """\
|
||||
Route Endpoint Methods
|
||||
-----------------------------------------------------
|
||||
/simpleroute simple HEAD, OPTIONS, GET
|
||||
/static/<path:filename> static HEAD, OPTIONS, GET
|
||||
"""
|
||||
@app.route('/get_post/<int:x>/<int:y>', methods=['GET', 'POST'])
|
||||
def yyy_get_post(x, y):
|
||||
pass
|
||||
|
||||
def test_only_POST_route(self, runner, monkeypatch):
|
||||
monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:only_POST_route_app')
|
||||
result = runner.invoke(cli, ['routes'], catch_exceptions=False)
|
||||
@app.route('/zzz_post', methods=['POST'])
|
||||
def aaa_post():
|
||||
pass
|
||||
|
||||
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:]):
|
||||
# do this instead of startswith for nicer pytest output
|
||||
assert line[:len(expect)] == expect
|
||||
|
||||
def test_simple(self, invoke):
|
||||
result = invoke(['routes'])
|
||||
assert result.exit_code == 0
|
||||
assert result.output == """\
|
||||
Route Endpoint Methods
|
||||
------------------------------------------------------
|
||||
/only-post only_post POST, OPTIONS
|
||||
/static/<path:filename> static HEAD, OPTIONS, GET
|
||||
"""
|
||||
self.expect_order(
|
||||
['aaa_post', 'static', 'yyy_get_post'],
|
||||
result.output
|
||||
)
|
||||
|
||||
def test_sort(self, invoke):
|
||||
default_output = invoke(['routes']).output
|
||||
endpoint_output = invoke(['routes', '-s', 'endpoint']).output
|
||||
assert default_output == endpoint_output
|
||||
self.expect_order(
|
||||
['static', 'yyy_get_post', 'aaa_post'],
|
||||
invoke(['routes', '-s', 'methods']).output
|
||||
)
|
||||
self.expect_order(
|
||||
['yyy_get_post', 'static', 'aaa_post'],
|
||||
invoke(['routes', '-s', 'rule']).output
|
||||
)
|
||||
self.expect_order(
|
||||
['aaa_post', 'yyy_get_post', 'static'],
|
||||
invoke(['routes', '-s', 'match']).output
|
||||
)
|
||||
|
||||
def test_all_methods(self, invoke):
|
||||
output = invoke(['routes']).output
|
||||
assert 'GET, HEAD, OPTIONS, POST' not in output
|
||||
output = invoke(['routes', '--all-methods']).output
|
||||
assert 'GET, HEAD, OPTIONS, POST' in output
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue