From 932f7d7cbb5a4e04fd0922c3272a6e32d408abe8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 25 Aug 2014 16:50:22 +0200 Subject: [PATCH] Refactored the scripting interface greatly. --- docs/api.rst | 8 ++++-- docs/cli.rst | 18 +++++++++++++ flask/app.py | 4 +-- flask/cli.py | 71 +++++++++++++++++++++++++++++----------------------- 4 files changed, 65 insertions(+), 36 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 945082bc..6b254c8e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -762,9 +762,14 @@ Command Line Interface .. autoclass:: FlaskGroup :members: +.. autoclass:: AppGroup + :members: + .. autoclass:: ScriptInfo :members: +.. autofunction:: with_appcontext + .. autofunction:: pass_script_info .. autofunction:: without_appcontext @@ -774,8 +779,7 @@ Command Line Interface A special decorator that informs a click callback to be passed the script info object as first argument. This is normally not useful unless you implement very special commands like the run command which - does not want the application to be loaded yet. This can be combined - with the :func:`without_appcontext` decorator. + does not want the application to be loaded yet. .. autodata:: run_command diff --git a/docs/cli.rst b/docs/cli.rst index 6eb6cb9c..a36c33a3 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -100,6 +100,24 @@ The command will then show up on the command line:: $ flask -a hello.py initdb Init the db +Application Context +------------------- + +Most commands operate on the application so it makes a lot of sense if +they have the application context setup. Because of this, if you register +a callback on ``app.cli`` with the :meth:`~flask.cli.AppGroup.command` the +callback will automatically be wrapped through :func:`cli.with_appcontext` +which informs the cli system to ensure that an application context is set +up. This behavior is not available if a command is lated later with +:func:`~click.Group.add_command` or through other means. + +It can also be disabled by passing ``with_appcontext=False`` to the +decorator:: + + @app.cli.command(with_appcontext=False) + def example(): + pass + Factory Functions ----------------- diff --git a/flask/app.py b/flask/app.py index d6371c43..b931c4e2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -24,7 +24,7 @@ from werkzeug.exceptions import HTTPException, InternalServerError, \ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ locked_cached_property, _endpoint_from_view_func, find_package -from . import json +from . import json, cli from .wrappers import Request, Response from .config import ConfigAttribute, Config from .ctx import RequestContext, AppContext, _AppCtxGlobals @@ -544,7 +544,7 @@ class Flask(_PackageBoundObject): #: provided by Flask itself and can be overridden. #: #: This is an instance of a :class:`click.Group` object. - self.cli = click.Group(self) + self.cli = cli.AppGroup(self) def _get_error_handlers(self): from warnings import warn diff --git a/flask/cli.py b/flask/cli.py index 95a35808..e4d25549 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -12,7 +12,7 @@ import os import sys from threading import Lock -from contextlib import contextmanager +from functools import update_wrapper import click @@ -166,30 +166,21 @@ class ScriptInfo(object): self._loaded_app = rv return rv - @contextmanager - def conditional_context(self, with_context=True): - """Creates an application context or not, depending on the given - parameter but always works as context manager. This is just a - shortcut for a common operation. - """ - if with_context: - with self.load_app().app_context() as ctx: - yield ctx - else: - yield None + +pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) -pass_script_info = click.make_pass_decorator(ScriptInfo) - - -def without_appcontext(f): - """Marks a click callback so that it does not get a app context - created. This only works for commands directly registered to - the toplevel system. This really is only useful for very - special commands like the runserver one. +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. """ - f.__flask_without_appcontext__ = True - return f + @click.pass_context + def decorator(__ctx, *args, **kwargs): + with __ctx.ensure_object(ScriptInfo).load_app().app_context(): + return __ctx.invoke(f, *args, **kwargs) + return update_wrapper(decorator, f) def set_debug_value(ctx, param, value): @@ -220,7 +211,7 @@ class FlaskGroup(click.Group): more commands from the configured Flask app. Normally a developer does not have to interface with this class but there are some very advanced usecases for which it makes sense to create an instance of - this. + this. Not to be confused with :class:`AppGroup`. For information as of why this is useful see :ref:`custom-scripts`. @@ -286,14 +277,6 @@ class FlaskGroup(click.Group): pass return sorted(rv) - def invoke_subcommand(self, ctx, cmd, cmd_name, args): - with_context = cmd.callback is None or \ - not getattr(cmd.callback, '__flask_without_appcontext__', False) - - with ctx.find_object(ScriptInfo).conditional_context(with_context): - return click.Group.invoke_subcommand( - self, ctx, cmd, cmd_name, args) - def main(self, *args, **kwargs): obj = kwargs.get('obj') if obj is None: @@ -303,6 +286,30 @@ class FlaskGroup(click.Group): return click.Group.main(self, *args, **kwargs) +class AppGroup(click.Group): + """This works similar to a regular click :class:`~click.Group` but it + changes the behavior of the :meth:`command` decorator so that it + automatically wraps the functions in :func:`with_appcontext`. + + Not to be confused with :class:`FlaskGroup`. + """ + + def __init__(self, app): + click.Group.__init__(self) + + def command(self, *args, **kwargs): + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` + unless it's disabled by passing ``with_appcontext=False``. + """ + wrap_for_ctx = kwargs.pop('with_appcontext', True) + def decorator(f): + if wrap_for_ctx: + f = with_appcontext(f) + return click.Group.command(*args, **kwargs)(f) + return decorator + + def script_info_option(*args, **kwargs): """This decorator works exactly like :func:`click.option` but is eager by default and stores the value in the :attr:`ScriptInfo.data`. This @@ -346,7 +353,6 @@ def script_info_option(*args, **kwargs): 'loading is enabled if the reloader is disabled.') @click.option('--with-threads/--without-threads', default=False, help='Enable or disable multithreading.') -@without_appcontext @pass_script_info def run_command(info, host, port, reload, debugger, eager_loading, with_threads): @@ -388,6 +394,7 @@ def run_command(info, host, port, reload, debugger, eager_loading, @click.command('shell', short_help='Runs a shell in the app context.') +@with_appcontext def shell_command(): """Runs an interactive Python shell in the context of a given Flask application. The application will populate the default