with_appcontext lasts for the lifetime of the click context
This commit is contained in:
parent
ae547270e9
commit
c9e000b9ce
6 changed files with 65 additions and 26 deletions
|
|
@ -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
|
||||
|
|
|
|||
20
docs/cli.rst
20
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
|
||||
-------
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue