From 87fe58c239f1074f0622b126a832241f2330953c Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 1 May 2017 20:51:21 -0700 Subject: [PATCH] fall back to built-in command when loading fails print traceback when loading fails don't print traceback multiple times modify test to set command on app instead of group add docs and changelog --- CHANGES | 3 +++ docs/cli.rst | 38 +++++++++++++++++++++++++++++++++ flask/cli.py | 54 ++++++++++++++++++++++++----------------------- tests/test_cli.py | 15 +++++++------ 4 files changed, 78 insertions(+), 32 deletions(-) diff --git a/CHANGES b/CHANGES index ddab541f..a943602d 100644 --- a/CHANGES +++ b/CHANGES @@ -34,11 +34,14 @@ Major release, unreleased type is invalid. (`#2256`_) - Add ``routes`` CLI command to output routes registered on the application. (`#2259`_) +- Allow overriding built-in commands using ``app.cli.command`` instead of + creating a separate ``FlaskGroup``. (`#2217`_) .. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1898: https://github.com/pallets/flask/pull/1898 .. _#1936: https://github.com/pallets/flask/pull/1936 .. _#2017: https://github.com/pallets/flask/pull/2017 +.. _#2217: https://github.com/pallets/flask/pull/2217 .. _#2223: https://github.com/pallets/flask/pull/2223 .. _#2254: https://github.com/pallets/flask/pull/2254 .. _#2256: https://github.com/pallets/flask/pull/2256 diff --git a/docs/cli.rst b/docs/cli.rst index d0b033f6..62da8488 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -159,6 +159,44 @@ pick it up:: From this point onwards :command:`flask` will find your application. +Overriding Built-In Commands +---------------------------- + +Flask comes with some built in commands which can be overridden. Use +``app.cli.command(name)`` with the name of the built-in command being +overridden. Use :meth:`~click.Context.forward` to call the built-in command +from the custom command. + +For example, to override the built-in ``run`` command and add a ``--ssl`` +option for enabling SSL on the dev server:: + + import click + from flask import Flask + from flask.cli import run_command as builtin_run_command + + app = Flask(__name__) + + @app.cli.command( + 'run', + # copy the built-in command's help text + help=builtin_run_command.help, + short_help=builtin_run_command.short_help + ) + @click.option('--ssl', 'ssl_context', flag_value='adhoc') + @click.pass_context + def run_command(ctx, *args, ssl_context=None, **kwargs): + if ssl_context == 'adhoc': + click.echo(' * Enabling adhoc SSL') + + ctx.forward(builtin_run_command) + + # copy the built-in command's options + run_command.params[:0] = builtin_run_command.params + +Copying the built-in function's code to your own version is also viable, and +may be more useful if the behavior you want to change can't be done before the +command is forwarded to. + .. _custom-scripts: Custom Scripts diff --git a/flask/cli.py b/flask/cli.py index 1ebee200..4ef26d90 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -341,43 +341,40 @@ class FlaskGroup(AppGroup): def get_command(self, ctx, name): self._load_plugin_commands() - - # We load built-in commands first as these should always be the - # same no matter what the app does. If the app does want to - # override this it needs to make a custom instance of this group - # and not attach the default commands. - # - # This also means that the script stays functional in case the - # application completely fails. - builtin_rv = AppGroup.get_command(self, ctx, name) - info = ctx.ensure_object(ScriptInfo) + try: rv = info.load_app().cli.get_command(ctx, name) + if rv is not None: return rv - elif builtin_rv is not None: - return builtin_rv - except NoAppException: - pass + except Exception: + # Intentionally swallow all exceptions and try to get the built-in + # version of the command. If this didn't fail in list_commands + # first, print the traceback. + if not info.data.get('_list_commands_failed', False): + traceback.print_exc() + + # If the app couldn't be loaded, or didn't have the given command, fall + # back to built-in commands. + return super(FlaskGroup, self).get_command(ctx, name) def list_commands(self, ctx): self._load_plugin_commands() - # The commands available is the list of both the application (if # available) plus the builtin commands. - rv = set(click.Group.list_commands(self, ctx)) + rv = set(super(FlaskGroup, self).list_commands(ctx)) info = ctx.ensure_object(ScriptInfo) + try: rv.update(info.load_app().cli.list_commands(ctx)) except Exception: - # Here we intentionally swallow all exceptions as we don't - # want the help page to break if the app does not exist. - # If someone attempts to use the command we try to create - # the app again and this will give us the error. - # However, we will not do so silently because that would confuse - # users. + # Intentionally swallow all exceptions so the help page doesn't + # break if the app does not exist. Set a flag so that the error is + # only printed once while printing each command's help. + info.data['_list_commands_failed'] = True traceback.print_exc() + return sorted(rv) def main(self, *args, **kwargs): @@ -406,8 +403,9 @@ class FlaskGroup(AppGroup): @click.option('--with-threads/--without-threads', default=False, help='Enable or disable multithreading.') @pass_script_info -def run_command(info, host, port, reload, debugger, eager_loading, - with_threads): +def run_command( + info, host, port, reload, debugger, eager_loading, with_threads, **kwargs +): """Runs a local development server for the Flask application. This local server is recommended for development purposes only but it @@ -450,8 +448,12 @@ def run_command(info, host, port, reload, debugger, eager_loading, if debug is not None: print(' * Forcing debug mode %s' % (debug and 'on' or 'off')) - run_simple(host, port, app, use_reloader=reload, - use_debugger=debugger, threaded=with_threads) + run_simple( + host, port, app, + use_reloader=reload, use_debugger=debugger, + threaded=with_threads, + **kwargs + ) @click.command('shell', short_help='Runs a shell in the app context.') diff --git a/tests/test_cli.py b/tests/test_cli.py index 21c59649..65520270 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -271,13 +271,16 @@ class TestRoutes: def test_override_builtin_cli(): - cli = FlaskGroup(create_app=lambda info: Flask('override_builtin')) + def create_app(info): + app = Flask(__name__) - @cli.command('run', help="foo") - def run(): - click.echo(current_app.name) + @app.cli.command('routes') + def routes_command(): + click.echo('test') + + return app runner = CliRunner() - result = runner.invoke(cli, ['run']) + result = runner.invoke(FlaskGroup(create_app=create_app), ['routes']) assert result.exit_code == 0 - assert result.output == 'override_builtin\n' + assert result.output == 'test\n'