Refactored the scripting interface greatly.

This commit is contained in:
Armin Ronacher 2014-08-25 16:50:22 +02:00
parent 66ef55ce0a
commit 932f7d7cbb
4 changed files with 65 additions and 36 deletions

View file

@ -762,9 +762,14 @@ Command Line Interface
.. autoclass:: FlaskGroup .. autoclass:: FlaskGroup
:members: :members:
.. autoclass:: AppGroup
:members:
.. autoclass:: ScriptInfo .. autoclass:: ScriptInfo
:members: :members:
.. autofunction:: with_appcontext
.. autofunction:: pass_script_info .. autofunction:: pass_script_info
.. autofunction:: without_appcontext .. autofunction:: without_appcontext
@ -774,8 +779,7 @@ Command Line Interface
A special decorator that informs a click callback to be passed the A special decorator that informs a click callback to be passed the
script info object as first argument. This is normally not useful script info object as first argument. This is normally not useful
unless you implement very special commands like the run command which unless you implement very special commands like the run command which
does not want the application to be loaded yet. This can be combined does not want the application to be loaded yet.
with the :func:`without_appcontext` decorator.
.. autodata:: run_command .. autodata:: run_command

View file

@ -100,6 +100,24 @@ The command will then show up on the command line::
$ flask -a hello.py initdb $ flask -a hello.py initdb
Init the db 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 Factory Functions
----------------- -----------------

View file

@ -24,7 +24,7 @@ from werkzeug.exceptions import HTTPException, InternalServerError, \
from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
locked_cached_property, _endpoint_from_view_func, find_package locked_cached_property, _endpoint_from_view_func, find_package
from . import json from . import json, cli
from .wrappers import Request, Response from .wrappers import Request, Response
from .config import ConfigAttribute, Config from .config import ConfigAttribute, Config
from .ctx import RequestContext, AppContext, _AppCtxGlobals from .ctx import RequestContext, AppContext, _AppCtxGlobals
@ -544,7 +544,7 @@ class Flask(_PackageBoundObject):
#: provided by Flask itself and can be overridden. #: provided by Flask itself and can be overridden.
#: #:
#: This is an instance of a :class:`click.Group` object. #: 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): def _get_error_handlers(self):
from warnings import warn from warnings import warn

View file

@ -12,7 +12,7 @@
import os import os
import sys import sys
from threading import Lock from threading import Lock
from contextlib import contextmanager from functools import update_wrapper
import click import click
@ -166,30 +166,21 @@ class ScriptInfo(object):
self._loaded_app = rv self._loaded_app = rv
return rv return rv
@contextmanager
def conditional_context(self, with_context=True): pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=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) 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
def without_appcontext(f): to the ``app.cli`` object then they are wrapped with this function
"""Marks a click callback so that it does not get a app context by default unless it's disabled.
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.
""" """
f.__flask_without_appcontext__ = True @click.pass_context
return f 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): 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 more commands from the configured Flask app. Normally a developer
does not have to interface with this class but there are some very 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 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`. For information as of why this is useful see :ref:`custom-scripts`.
@ -286,14 +277,6 @@ class FlaskGroup(click.Group):
pass pass
return sorted(rv) 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): def main(self, *args, **kwargs):
obj = kwargs.get('obj') obj = kwargs.get('obj')
if obj is None: if obj is None:
@ -303,6 +286,30 @@ class FlaskGroup(click.Group):
return click.Group.main(self, *args, **kwargs) 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): def script_info_option(*args, **kwargs):
"""This decorator works exactly like :func:`click.option` but is eager """This decorator works exactly like :func:`click.option` but is eager
by default and stores the value in the :attr:`ScriptInfo.data`. This 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.') 'loading is enabled if the reloader is disabled.')
@click.option('--with-threads/--without-threads', default=False, @click.option('--with-threads/--without-threads', default=False,
help='Enable or disable multithreading.') help='Enable or disable multithreading.')
@without_appcontext
@pass_script_info @pass_script_info
def run_command(info, host, port, reload, debugger, eager_loading, def run_command(info, host, port, reload, debugger, eager_loading,
with_threads): 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.') @click.command('shell', short_help='Runs a shell in the app context.')
@with_appcontext
def shell_command(): def shell_command():
"""Runs an interactive Python shell in the context of a given """Runs an interactive Python shell in the context of a given
Flask application. The application will populate the default Flask application. The application will populate the default