Browse Source

Merge pull request #2259 from davidism/routes-command

Add routes command
pull/2077/head
David Lord 8 years ago committed by GitHub
parent
commit
39d55525ac
  1. 3
      CHANGES
  2. 55
      flask/cli.py
  3. 78
      tests/test_cli.py

3
CHANGES

@ -32,6 +32,8 @@ Major release, unreleased
- ``Flask.make_response`` raises ``TypeError`` instead of ``ValueError`` for - ``Flask.make_response`` raises ``TypeError`` instead of ``ValueError`` for
bad response types. The error messages have been improved to describe why the bad response types. The error messages have been improved to describe why the
type is invalid. (`#2256`_) type is invalid. (`#2256`_)
- Add ``routes`` CLI command to output routes registered on the application.
(`#2259`_)
.. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1489: https://github.com/pallets/flask/pull/1489
.. _#1898: https://github.com/pallets/flask/pull/1898 .. _#1898: https://github.com/pallets/flask/pull/1898
@ -40,6 +42,7 @@ Major release, unreleased
.. _#2223: https://github.com/pallets/flask/pull/2223 .. _#2223: https://github.com/pallets/flask/pull/2223
.. _#2254: https://github.com/pallets/flask/pull/2254 .. _#2254: https://github.com/pallets/flask/pull/2254
.. _#2256: https://github.com/pallets/flask/pull/2256 .. _#2256: https://github.com/pallets/flask/pull/2256
.. _#2259: https://github.com/pallets/flask/pull/2259
Version 0.12.1 Version 0.12.1
-------------- --------------

55
flask/cli.py

@ -12,14 +12,17 @@
import os import os
import sys import sys
import traceback import traceback
from threading import Lock, Thread
from functools import update_wrapper from functools import update_wrapper
from operator import attrgetter
from threading import Lock, Thread
import click import click
from . import __version__
from ._compat import iteritems, reraise from ._compat import iteritems, reraise
from .globals import current_app
from .helpers import get_debug_flag from .helpers import get_debug_flag
from . import __version__
class NoAppException(click.UsageError): class NoAppException(click.UsageError):
"""Raised if an application cannot be found or loaded.""" """Raised if an application cannot be found or loaded."""
@ -319,6 +322,7 @@ class FlaskGroup(AppGroup):
if add_default_commands: if add_default_commands:
self.add_command(run_command) self.add_command(run_command)
self.add_command(shell_command) self.add_command(shell_command)
self.add_command(routes_command)
self._loaded_plugin_commands = False self._loaded_plugin_commands = False
@ -484,6 +488,53 @@ def shell_command():
code.interact(banner=banner, local=ctx) code.interact(banner=banner, local=ctx)
@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(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)
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="""\ cli = FlaskGroup(help="""\
This shell command acts as general utility script for Flask applications. This shell command acts as general utility script for Flask applications.

78
tests/test_cli.py

@ -14,17 +14,23 @@
from __future__ import absolute_import, print_function from __future__ import absolute_import, print_function
import os import os
import sys import sys
from functools import partial
import click import click
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from flask import Flask, current_app from flask import Flask, current_app
from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ from flask.cli import cli, AppGroup, FlaskGroup, NoAppException, ScriptInfo, \
find_best_app, locate_app, with_appcontext, prepare_exec_for_file, \ find_best_app, locate_app, with_appcontext, prepare_exec_for_file, \
find_default_import_path, get_version find_default_import_path, get_version
@pytest.fixture
def runner():
return CliRunner()
def test_cli_name(test_apps): def test_cli_name(test_apps):
"""Make sure the CLI object's name is the app's name and not the app itself""" """Make sure the CLI object's name is the app's name and not the app itself"""
from cliapp.app import testapp from cliapp.app import testapp
@ -129,7 +135,7 @@ def test_scriptinfo(test_apps):
assert obj.load_app() == app assert obj.load_app() == app
def test_with_appcontext(): def test_with_appcontext(runner):
"""Test of with_appcontext.""" """Test of with_appcontext."""
@click.command() @click.command()
@with_appcontext @with_appcontext
@ -138,13 +144,12 @@ def test_with_appcontext():
obj = ScriptInfo(create_app=lambda info: Flask("testapp")) obj = ScriptInfo(create_app=lambda info: Flask("testapp"))
runner = CliRunner()
result = runner.invoke(testcmd, obj=obj) result = runner.invoke(testcmd, obj=obj)
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'testapp\n' assert result.output == 'testapp\n'
def test_appgroup(): def test_appgroup(runner):
"""Test of with_appcontext.""" """Test of with_appcontext."""
@click.group(cls=AppGroup) @click.group(cls=AppGroup)
def cli(): def cli():
@ -164,7 +169,6 @@ def test_appgroup():
obj = ScriptInfo(create_app=lambda info: Flask("testappgroup")) obj = ScriptInfo(create_app=lambda info: Flask("testappgroup"))
runner = CliRunner()
result = runner.invoke(cli, ['test'], obj=obj) result = runner.invoke(cli, ['test'], obj=obj)
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'testappgroup\n' assert result.output == 'testappgroup\n'
@ -174,7 +178,7 @@ def test_appgroup():
assert result.output == 'testappgroup\n' assert result.output == 'testappgroup\n'
def test_flaskgroup(): def test_flaskgroup(runner):
"""Test FlaskGroup.""" """Test FlaskGroup."""
def create_app(info): def create_app(info):
return Flask("flaskgroup") return Flask("flaskgroup")
@ -187,13 +191,12 @@ def test_flaskgroup():
def test(): def test():
click.echo(current_app.name) click.echo(current_app.name)
runner = CliRunner()
result = runner.invoke(cli, ['test']) result = runner.invoke(cli, ['test'])
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'flaskgroup\n' assert result.output == 'flaskgroup\n'
def test_print_exceptions(): def test_print_exceptions(runner):
"""Print the stacktrace if the CLI.""" """Print the stacktrace if the CLI."""
def create_app(info): def create_app(info):
raise Exception("oh no") raise Exception("oh no")
@ -203,8 +206,65 @@ def test_print_exceptions():
def cli(**params): def cli(**params):
pass pass
runner = CliRunner()
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ['--help'])
assert result.exit_code == 0 assert result.exit_code == 0
assert 'Exception: oh no' in result.output assert 'Exception: oh no' in result.output
assert 'Traceback' in result.output assert 'Traceback' in result.output
class TestRoutes:
@pytest.fixture
def invoke(self, runner):
def create_app(info):
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
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
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