diff --git a/CHANGES.rst b/CHANGES.rst index 0dea8e3a..42d91ea3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,6 +33,9 @@ Unreleased - Add ``--env-file`` option to the ``flask`` CLI. This allows specifying a dotenv file to load in addition to ``.env`` and ``.flaskenv``. :issue:`3108` +- It is no longer required to decorate custom CLI commands on + ``app.cli`` or ``blueprint.cli`` with ``@with_appcontext``, an app + context will already be active at that point. :issue:`2410` Version 2.1.3 diff --git a/docs/cli.rst b/docs/cli.rst index 7bab8fac..b5724d49 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -437,12 +437,14 @@ commands directly to the application's level: Application Context ~~~~~~~~~~~~~~~~~~~ -Commands added using the Flask app's :attr:`~Flask.cli` -:meth:`~cli.AppGroup.command` decorator will be executed with an application -context pushed, so your command and extensions have access to the app and its -configuration. If you create a command using the Click :func:`~click.command` -decorator instead of the Flask decorator, you can use -:func:`~cli.with_appcontext` to get the same behavior. :: +Commands added using the Flask app's :attr:`~Flask.cli` or +:class:`~flask.cli.FlaskGroup` :meth:`~cli.AppGroup.command` decorator +will be executed with an application context pushed, so your custom +commands and parameters have access to the app and its configuration. The +:func:`~cli.with_appcontext` decorator can be used to get the same +behavior, but is not needed in most cases. + +.. code-block:: python import click from flask.cli import with_appcontext @@ -454,12 +456,6 @@ decorator instead of the Flask decorator, you can use app.cli.add_command(do_work) -If you're sure a command doesn't need the context, you can disable it:: - - @app.cli.command(with_appcontext=False) - def do_work(): - ... - Plugins ------- diff --git a/docs/tutorial/database.rst b/docs/tutorial/database.rst index b2852197..934f6008 100644 --- a/docs/tutorial/database.rst +++ b/docs/tutorial/database.rst @@ -40,7 +40,6 @@ response is sent. import click from flask import current_app, g - from flask.cli import with_appcontext def get_db(): @@ -128,7 +127,6 @@ Add the Python functions that will run these SQL commands to the @click.command('init-db') - @with_appcontext def init_db_command(): """Clear the existing data and create new tables.""" init_db() diff --git a/examples/tutorial/flaskr/db.py b/examples/tutorial/flaskr/db.py index f1e2dc30..acaa4ae3 100644 --- a/examples/tutorial/flaskr/db.py +++ b/examples/tutorial/flaskr/db.py @@ -3,7 +3,6 @@ import sqlite3 import click from flask import current_app from flask import g -from flask.cli import with_appcontext def get_db(): @@ -39,7 +38,6 @@ def init_db(): @click.command("init-db") -@with_appcontext def init_db_command(): """Clear existing data and create new tables.""" init_db() diff --git a/src/flask/cli.py b/src/flask/cli.py index 40f1de54..321794b6 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -410,15 +410,25 @@ pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) def with_appcontext(f): """Wraps a callback so that it's guaranteed to be executed with the - script's application context. If callbacks are registered directly - to the ``app.cli`` object then they are wrapped with this function - by default unless it's disabled. + script's application context. + + Custom commands (and their options) registered under ``app.cli`` or + ``blueprint.cli`` will always have an app context available, this + decorator is not required in that case. + + .. versionchanged:: 2.2 + The app context is active for subcommands as well as the + decorated callback. The app context is always available to + ``app.cli`` command and parameter callbacks. """ @click.pass_context def decorator(__ctx, *args, **kwargs): - with __ctx.ensure_object(ScriptInfo).load_app().app_context(): - return __ctx.invoke(f, *args, **kwargs) + if not current_app: + app = __ctx.ensure_object(ScriptInfo).load_app() + __ctx.with_resource(app.app_context()) + + return __ctx.invoke(f, *args, **kwargs) return update_wrapper(decorator, f) @@ -587,6 +597,10 @@ class FlaskGroup(AppGroup): Added the ``-A/--app``, ``-E/--env``, ``--debug/--no-debug``, and ``-e/--env-file`` options. + .. versionchanged:: 2.2 + An app context is pushed when running ``app.cli`` commands, so + ``@with_appcontext`` is no longer required for those commands. + .. versionchanged:: 1.0 If installed, python-dotenv will be used to load environment variables from :file:`.env` and :file:`.flaskenv` files. @@ -660,9 +674,18 @@ class FlaskGroup(AppGroup): # Look up commands provided by the app, showing an error and # continuing if the app couldn't be loaded. try: - return info.load_app().cli.get_command(ctx, name) + app = info.load_app() except NoAppException as e: click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + return None + + # Push an app context for the loaded app unless it is already + # active somehow. This makes the context available to parameter + # and command callbacks without needing @with_appcontext. + if not current_app or current_app._get_current_object() is not app: + ctx.with_resource(app.app_context()) + + return app.cli.get_command(ctx, name) def list_commands(self, ctx): self._load_plugin_commands() diff --git a/tests/test_cli.py b/tests/test_cli.py index 7a8e9af9..7d83d865 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,6 +13,7 @@ import pytest from _pytest.monkeypatch import notset from click.testing import CliRunner +from flask import _app_ctx_stack from flask import Blueprint from flask import current_app from flask import Flask @@ -310,6 +311,26 @@ def test_lazy_load_error(monkeypatch): lazy._flush_bg_loading_exception() +def test_app_cli_has_app_context(app, runner): + def _param_cb(ctx, param, value): + # current_app should be available in parameter callbacks + return bool(current_app) + + @app.cli.command() + @click.argument("value", callback=_param_cb) + def check(value): + app = click.get_current_context().obj.load_app() + # the loaded app should be the same as current_app + same_app = current_app._get_current_object() is app + # only one app context should be pushed + stack_size = len(_app_ctx_stack._local.stack) + return same_app, stack_size, value + + cli = FlaskGroup(create_app=lambda: app) + result = runner.invoke(cli, ["check", "x"], standalone_mode=False) + assert result.return_value == (True, 1, True) + + def test_with_appcontext(runner): @click.command() @with_appcontext @@ -323,12 +344,12 @@ def test_with_appcontext(runner): assert result.output == "testapp\n" -def test_appgroup(runner): +def test_appgroup_app_context(runner): @click.group(cls=AppGroup) def cli(): pass - @cli.command(with_appcontext=True) + @cli.command() def test(): click.echo(current_app.name) @@ -336,7 +357,7 @@ def test_appgroup(runner): def subgroup(): pass - @subgroup.command(with_appcontext=True) + @subgroup.command() def test2(): click.echo(current_app.name) @@ -351,7 +372,7 @@ def test_appgroup(runner): assert result.output == "testappgroup\n" -def test_flaskgroup(runner): +def test_flaskgroup_app_context(runner): def create_app(): return Flask("flaskgroup")