with_appcontext lasts for the lifetime of the click context

This commit is contained in:
David Lord 2022-06-17 11:14:22 -07:00
parent ae547270e9
commit c9e000b9ce
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
6 changed files with 65 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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