Browse Source

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
pull/2259/head
David Lord 8 years ago
parent
commit
7ad79583b9
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
  1. 3
      CHANGES
  2. 74
      flask/cli.py
  3. 18
      tests/test_apps/cliapp/routesapp.py
  4. 84
      tests/test_cli.py

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
--------------

74
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 . import __version__
from ._compat import iteritems, reraise
from .globals import current_app
from .helpers import get_debug_flag
from . import __version__
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
order_key = lambda rule: getattr(rule, order_by)
sorted_rules = sorted(app.url_map.iter_rules(), key=order_key)
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'))
def routes_command(sort, all_methods):
"""Show all registered routes with endpoints and methods."""
rules = list(current_app.url_map.iter_rules())
ignored_methods = set(() if all_methods else ('HEAD', 'OPTIONS'))
if sort in ('endpoint', 'rule'):
rules = sorted(rules, key=attrgetter(sort))
elif sort == 'methods':
rules = sorted(rules, key=lambda rule: sorted(rule.methods))
rule_methods = [
', '.join(sorted(rule.methods - ignored_methods)) for rule in rules
]
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)
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)
click.echo(row.format(*headers).strip())
click.echo(row.format(*('-' * width for width in widths)))
for rule in sorted_rules:
methods = ', '.join(rule.methods)
click.echo(columnformat.format(rule.rule, rule.endpoint, methods))
for rule, methods in zip(rules, rule_methods):
click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip())
cli = FlaskGroup(help="""\

18
tests/test_apps/cliapp/routesapp.py

@ -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

84
tests/test_cli.py

@ -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…
Cancel
Save