Greatly refactored click integration and documented it a bit more.
This commit is contained in:
parent
81576c236a
commit
3569fc2441
4 changed files with 249 additions and 82 deletions
27
docs/api.rst
27
docs/api.rst
|
|
@ -753,3 +753,30 @@ Full example::
|
||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
The `provide_automatic_options` functionality was added.
|
The `provide_automatic_options` functionality was added.
|
||||||
|
|
||||||
|
Command Line Interface
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. currentmodule:: flask.cli
|
||||||
|
|
||||||
|
.. autoclass:: FlaskGroup
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: ScriptInfo
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autofunction:: pass_script_info
|
||||||
|
|
||||||
|
.. autofunction:: without_appcontext
|
||||||
|
|
||||||
|
.. autofunction:: script_info_option
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. autodata:: run_command
|
||||||
|
|
||||||
|
.. autodata:: shell_command
|
||||||
|
|
|
||||||
114
docs/cli.rst
114
docs/cli.rst
|
|
@ -108,7 +108,8 @@ In case you are using factory functions to create your application (see
|
||||||
work with them directly. Flask won't be able to figure out how to
|
work with them directly. Flask won't be able to figure out how to
|
||||||
instanciate your application properly by itself. Because of this reason
|
instanciate your application properly by itself. Because of this reason
|
||||||
the recommendation is to create a separate file that instanciates
|
the recommendation is to create a separate file that instanciates
|
||||||
applications.
|
applications. This is by far not the only way to make this work. Another
|
||||||
|
is the :ref:`custom-scripts` support.
|
||||||
|
|
||||||
For instance if you have a factory function that creates an application
|
For instance if you have a factory function that creates an application
|
||||||
from a filename you could make a separate file that creates such an
|
from a filename you could make a separate file that creates such an
|
||||||
|
|
@ -128,3 +129,114 @@ it up::
|
||||||
export FLASK_APP=/path/to/autoapp.py
|
export FLASK_APP=/path/to/autoapp.py
|
||||||
|
|
||||||
From this point onwards ``flask`` will find your application.
|
From this point onwards ``flask`` will find your application.
|
||||||
|
|
||||||
|
.. _custom-scripts:
|
||||||
|
|
||||||
|
Custom Scripts
|
||||||
|
--------------
|
||||||
|
|
||||||
|
While the most common way is to use the ``flask`` command, you can also
|
||||||
|
make your own "driver scripts". Since Flask uses click for the scripts
|
||||||
|
there is no reason you cannot hook these scripts into any click
|
||||||
|
application. There is one big caveat and that is, that commands
|
||||||
|
registered to :attr:`Flask.cli` will expect to be (indirectly at least)
|
||||||
|
launched from a :class:`flask.cli.FlaskGroup` click group. This is
|
||||||
|
necessary so that the commands know which Flask application they have to
|
||||||
|
work with.
|
||||||
|
|
||||||
|
To understand why you might want custom scripts you need to understand how
|
||||||
|
click finds and executes the Flask application. If you use the ``flask``
|
||||||
|
script you specify the application to work with on the command line or
|
||||||
|
environment variable as an import name. This is simple but it has some
|
||||||
|
limitations. Primarily it does not work with application factory
|
||||||
|
functions (see :ref:`app-factories`).
|
||||||
|
|
||||||
|
With a custom script you don't have this problem as you can fully
|
||||||
|
customize how the application will be created. This is very useful if you
|
||||||
|
write reusable applications that you want to ship to users and they should
|
||||||
|
be presented with a custom management script.
|
||||||
|
|
||||||
|
If you are used to writing click applications this will look familiar but
|
||||||
|
at the same time, slightly different because of how commands are loaded.
|
||||||
|
We won't go into detail now about the differences but if you are curious
|
||||||
|
you can have a look at the :ref:`script-info-object` section to learn all
|
||||||
|
about it.
|
||||||
|
|
||||||
|
To explain all of this here an example ``manage.py`` script that manages a
|
||||||
|
hypothetical wiki application. We will go through the details
|
||||||
|
afterwards::
|
||||||
|
|
||||||
|
import click
|
||||||
|
from flask.cli import FlaskGroup, script_info_option
|
||||||
|
|
||||||
|
def create_wiki_app(info):
|
||||||
|
from yourwiki import create_app
|
||||||
|
config = info.data.get('config') or 'wikiconfig.py'
|
||||||
|
return create_app(config=config)
|
||||||
|
|
||||||
|
@click.group(cls=FlaskGroup, create_app=create_wiki_app)
|
||||||
|
@script_info_option('--config', script_info_key='config')
|
||||||
|
def cli(**params):
|
||||||
|
"""This is a management script for the wiki application."""
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cli()
|
||||||
|
|
||||||
|
That's a lot of code for not much, so let's go through all parts step by
|
||||||
|
step.
|
||||||
|
|
||||||
|
1. At first we import regular ``click`` as well as the click extensions
|
||||||
|
from the ``flask.cli`` package. Primarily we are here interested
|
||||||
|
in the :class:`~flask.cli.FlaskGroup` click group and the
|
||||||
|
:func:`~flask.cli.script_info_option` decorator.
|
||||||
|
2. The next thing we do is defining a function that is invoked with the
|
||||||
|
script info object (:ref:`script-info-object`) from flask and it's
|
||||||
|
purpose is to fully import and create the application. This can
|
||||||
|
either directly import an application object or create it (see
|
||||||
|
:ref:`app-factories`).
|
||||||
|
|
||||||
|
What is ``data.info``? It's a dictionary of arbitrary data on the
|
||||||
|
script info that can be filled by options or through other means. We
|
||||||
|
will come back to this later.
|
||||||
|
3. Next step is to create a :class:`FlaskGroup`. In this case we just
|
||||||
|
make an empty function with a help doc string that just does nothing
|
||||||
|
and then pass the ``create_wiki_app`` function as factory function.
|
||||||
|
|
||||||
|
Whenever click now needs to operate on a flask application it will
|
||||||
|
call that function with the script info and ask for it to be created.
|
||||||
|
4. In step 2 you could see that the config is passed to the actual
|
||||||
|
creation function. This config comes from the :func:`script_info_option`
|
||||||
|
decorator for the main script. It accepts a ``--config`` option and
|
||||||
|
then stores it in the script info so we can use it to create the
|
||||||
|
application.
|
||||||
|
5. All is rounded up by invoking the script.
|
||||||
|
|
||||||
|
.. _script-info-object:
|
||||||
|
|
||||||
|
The Script Info
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The Flask script integration might be confusing at first, but it has good
|
||||||
|
rasons it's done this way. The reason for this is that Flask wants to
|
||||||
|
both provide custom commands to click as well as not loading your
|
||||||
|
application unless it has to. The reason for this is added flexibility.
|
||||||
|
|
||||||
|
This way an application can provide custom commands, but even in the
|
||||||
|
absence of an application the ``flask`` script is still operational on a
|
||||||
|
basic level. In addition to that does it mean that the individual
|
||||||
|
commands have the option to not create an instance of the Flask
|
||||||
|
application unless required. This is very useful as it allows the server
|
||||||
|
command for instance, the load the application on first request instead of
|
||||||
|
immediately to give a better debug experience.
|
||||||
|
|
||||||
|
All of this is provided through the :class:`flask.cli.ScriptInfo` object
|
||||||
|
and some helper utilities around. The basic way it operates is that when
|
||||||
|
the :class:`flask.cli.FlaskGroup` executes as a script it creates a script
|
||||||
|
info and keeps it around. From that point onwards modifications on the
|
||||||
|
script info can be done through click options. To simplify this pattern
|
||||||
|
the :func:`flask.cli.script_info_option` decorator was added.
|
||||||
|
|
||||||
|
One Flask actually needs the individual Flask application it will invoke
|
||||||
|
the :meth:`flask.cli.ScriptInfo.load_app` method. This happens when the
|
||||||
|
server starts, when the shell is launched or when the script looks for an
|
||||||
|
application provided click command.
|
||||||
|
|
|
||||||
188
flask/cli.py
188
flask/cli.py
|
|
@ -81,7 +81,7 @@ def prepare_exec_for_file(filename):
|
||||||
return '.'.join(module[::-1])
|
return '.'.join(module[::-1])
|
||||||
|
|
||||||
|
|
||||||
def locate_app(app_id, debug=None):
|
def locate_app(app_id):
|
||||||
"""Attempts to locate the application."""
|
"""Attempts to locate the application."""
|
||||||
if ':' in app_id:
|
if ':' in app_id:
|
||||||
module, app_obj = app_id.split(':', 1)
|
module, app_obj = app_id.split(':', 1)
|
||||||
|
|
@ -98,8 +98,7 @@ def locate_app(app_id, debug=None):
|
||||||
if app is None:
|
if app is None:
|
||||||
raise RuntimeError('Failed to find application in module "%s"'
|
raise RuntimeError('Failed to find application in module "%s"'
|
||||||
% module)
|
% module)
|
||||||
if debug is not None:
|
|
||||||
app.debug = debug
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -145,13 +144,24 @@ class ScriptInfo(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, app_import_path=None, debug=None, create_app=None):
|
def __init__(self, app_import_path=None, debug=None, create_app=None):
|
||||||
|
#: The application import path
|
||||||
self.app_import_path = app_import_path
|
self.app_import_path = app_import_path
|
||||||
|
#: The debug flag. If this is not None, the application will
|
||||||
|
#: automatically have it's debug flag overridden with this value.
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
#: Optionally a function that is passed the script info to create
|
||||||
|
#: the instance of the application.
|
||||||
self.create_app = create_app
|
self.create_app = create_app
|
||||||
|
#: A dictionary with arbitrary data that can be associated with
|
||||||
|
#: this script info.
|
||||||
|
self.data = {}
|
||||||
self._loaded_app = None
|
self._loaded_app = None
|
||||||
|
|
||||||
def load_app(self):
|
def load_app(self):
|
||||||
"""Loads the Flask app (if not yet loaded) and returns it."""
|
"""Loads the Flask app (if not yet loaded) and returns it. Calling
|
||||||
|
this multiple times will just result in the already loaded app to
|
||||||
|
be returned.
|
||||||
|
"""
|
||||||
if self._loaded_app is not None:
|
if self._loaded_app is not None:
|
||||||
return self._loaded_app
|
return self._loaded_app
|
||||||
if self.create_app is not None:
|
if self.create_app is not None:
|
||||||
|
|
@ -159,29 +169,12 @@ class ScriptInfo(object):
|
||||||
else:
|
else:
|
||||||
if self.app_import_path is None:
|
if self.app_import_path is None:
|
||||||
_no_such_app()
|
_no_such_app()
|
||||||
rv = locate_app(self.app_import_path, self.debug)
|
rv = locate_app(self.app_import_path)
|
||||||
|
if self.debug is not None:
|
||||||
|
rv.debug = self.debug
|
||||||
self._loaded_app = rv
|
self._loaded_app = rv
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def make_wsgi_app(self, use_eager_loading=False):
|
|
||||||
"""Returns a WSGI app that loads the actual application at a later
|
|
||||||
stage (on first request). This has the advantage over
|
|
||||||
:meth:`load_app` that if used with a WSGI server, it will allow
|
|
||||||
the server to intercept errors later during request handling
|
|
||||||
instead of dying a horrible death.
|
|
||||||
|
|
||||||
If eager loading is disabled the loading will happen immediately.
|
|
||||||
"""
|
|
||||||
if self.app_import_path is not None:
|
|
||||||
def loader():
|
|
||||||
return locate_app(self.app_import_path, self.debug)
|
|
||||||
else:
|
|
||||||
if self.create_app is None:
|
|
||||||
_no_such_app()
|
|
||||||
def loader():
|
|
||||||
return self.create_app(self)
|
|
||||||
return DispatchingApp(loader, use_eager_loading=use_eager_loading)
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def conditional_context(self, with_context=True):
|
def conditional_context(self, with_context=True):
|
||||||
"""Creates an application context or not, depending on the given
|
"""Creates an application context or not, depending on the given
|
||||||
|
|
@ -189,17 +182,12 @@ class ScriptInfo(object):
|
||||||
shortcut for a common operation.
|
shortcut for a common operation.
|
||||||
"""
|
"""
|
||||||
if with_context:
|
if with_context:
|
||||||
with self.load_app(self).app_context() as ctx:
|
with self.load_app().app_context() as ctx:
|
||||||
yield ctx
|
yield ctx
|
||||||
else:
|
else:
|
||||||
yield None
|
yield None
|
||||||
|
|
||||||
|
|
||||||
#: 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.
|
|
||||||
pass_script_info = click.make_pass_decorator(ScriptInfo)
|
pass_script_info = click.make_pass_decorator(ScriptInfo)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -213,54 +201,65 @@ def without_appcontext(f):
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
class FlaskGroup(click.Group):
|
def set_debug_value(ctx, value):
|
||||||
"""Special subclass of the a regular click group that supports
|
ctx.ensure_object(ScriptInfo).debug = value
|
||||||
loading 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.
|
|
||||||
|
|
||||||
:param add_default_options: if this is True the app and debug option
|
|
||||||
is automatically added.
|
def set_app_value(ctx, value):
|
||||||
|
if value is not None:
|
||||||
|
if os.path.isfile(value):
|
||||||
|
value = prepare_exec_for_file(value)
|
||||||
|
elif '.' not in sys.path:
|
||||||
|
sys.path.insert(0, '.')
|
||||||
|
ctx.ensure_object(ScriptInfo).app_import_path = value
|
||||||
|
|
||||||
|
|
||||||
|
debug_option = click.Option(['--debug/--no-debug'],
|
||||||
|
help='Enable or disable debug mode.',
|
||||||
|
default=None, callback=set_debug_value)
|
||||||
|
|
||||||
|
|
||||||
|
app_option = click.Option(['-a', '--app'],
|
||||||
|
help='The application to run',
|
||||||
|
callback=set_app_value, is_eager=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FlaskGroup(click.Group):
|
||||||
|
"""Special subclass of the a regular click group that supports loading
|
||||||
|
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.
|
||||||
|
|
||||||
|
For information as of why this is useful see :ref:`custom-scripts`.
|
||||||
|
|
||||||
|
:param add_default_commands: if this is True then the default run and
|
||||||
|
shell commands wil be added.
|
||||||
|
:param add_app_option: adds the default ``--app`` option. This gets
|
||||||
|
automatically disabled if a `create_app`
|
||||||
|
callback is defined.
|
||||||
|
:param add_debug_option: adds the default ``--debug`` option.
|
||||||
|
:param create_app: an optional callback that is passed the script info
|
||||||
|
and returns the loaded app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, add_default_options=True,
|
def __init__(self, add_default_commands=True, add_app_option=None,
|
||||||
add_default_commands=True,
|
add_debug_option=True, create_app=None, **extra):
|
||||||
create_app=None, **extra):
|
params = list(extra.pop('params', None) or ())
|
||||||
click.Group.__init__(self, **extra)
|
if add_app_option is None:
|
||||||
|
add_app_option = create_app is None
|
||||||
|
if add_app_option:
|
||||||
|
params.append(app_option)
|
||||||
|
if add_debug_option:
|
||||||
|
params.append(debug_option)
|
||||||
|
|
||||||
|
click.Group.__init__(self, params=params, **extra)
|
||||||
self.create_app = create_app
|
self.create_app = create_app
|
||||||
if add_default_options:
|
|
||||||
self.add_app_option()
|
|
||||||
self.add_debug_option()
|
|
||||||
if add_default_commands:
|
if add_default_commands:
|
||||||
self.add_command(run_command)
|
self.add_command(run_command)
|
||||||
self.add_command(shell_command)
|
self.add_command(shell_command)
|
||||||
|
|
||||||
def add_app_option(self):
|
|
||||||
"""Adds an option to the default command that defines an import
|
|
||||||
path that points to an application.
|
|
||||||
"""
|
|
||||||
def set_app_id(ctx, value):
|
|
||||||
if value is not None:
|
|
||||||
if os.path.isfile(value):
|
|
||||||
value = prepare_exec_for_file(value)
|
|
||||||
elif '.' not in sys.path:
|
|
||||||
sys.path.insert(0, '.')
|
|
||||||
ctx.ensure_object(ScriptInfo).app_import_path = value
|
|
||||||
|
|
||||||
self.params.append(click.Option(['-a', '--app'],
|
|
||||||
help='The application to run',
|
|
||||||
callback=set_app_id, is_eager=True))
|
|
||||||
|
|
||||||
def add_debug_option(self):
|
|
||||||
"""Adds an option that controls the debug flag."""
|
|
||||||
def set_debug(ctx, value):
|
|
||||||
ctx.ensure_object(ScriptInfo).debug = value
|
|
||||||
|
|
||||||
self.params.append(click.Option(['--debug/--no-debug'],
|
|
||||||
help='Enable or disable debug mode.',
|
|
||||||
default=None, callback=set_debug))
|
|
||||||
|
|
||||||
def get_command(self, ctx, name):
|
def get_command(self, ctx, name):
|
||||||
info = ctx.ensure_object(ScriptInfo)
|
info = ctx.ensure_object(ScriptInfo)
|
||||||
# Find the command in the application first, if we can find it.
|
# Find the command in the application first, if we can find it.
|
||||||
|
|
@ -301,6 +300,33 @@ class FlaskGroup(click.Group):
|
||||||
return click.Group.main(self, *args, **kwargs)
|
return click.Group.main(self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
is useful to further customize an application factory in very complex
|
||||||
|
situations.
|
||||||
|
|
||||||
|
:param script_info_key: this is a mandatory keyword argument which
|
||||||
|
defines under which data key the value should
|
||||||
|
be stored.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
key = kwargs.pop('script_info_key')
|
||||||
|
except LookupError:
|
||||||
|
raise TypeError('script_info_key not provided.')
|
||||||
|
|
||||||
|
real_callback = kwargs.get('callback')
|
||||||
|
def callback(ctx, value):
|
||||||
|
if real_callback is not None:
|
||||||
|
value = real_callback(ctx, value)
|
||||||
|
ctx.ensure_object(ScriptInfo).data[key] = value
|
||||||
|
return value
|
||||||
|
|
||||||
|
kwargs['callback'] = callback
|
||||||
|
kwargs.setdefault('is_eager', True)
|
||||||
|
return click.option(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@click.command('run', short_help='Runs a development server.')
|
@click.command('run', short_help='Runs a development server.')
|
||||||
@click.option('--host', '-h', default='127.0.0.1',
|
@click.option('--host', '-h', default='127.0.0.1',
|
||||||
help='The interface to bind to.')
|
help='The interface to bind to.')
|
||||||
|
|
@ -340,7 +366,7 @@ def run_command(info, host, port, reload, debugger, eager_loading,
|
||||||
if eager_loading is None:
|
if eager_loading is None:
|
||||||
eager_loading = not reload
|
eager_loading = not reload
|
||||||
|
|
||||||
app = info.make_wsgi_app(use_eager_loading=eager_loading)
|
app = DispatchingApp(info.load_app, use_eager_loading=eager_loading)
|
||||||
|
|
||||||
# Extra startup messages. This depends a but on Werkzeug internals to
|
# Extra startup messages. This depends a but on Werkzeug internals to
|
||||||
# not double execute when the reloader kicks in.
|
# not double execute when the reloader kicks in.
|
||||||
|
|
@ -388,19 +414,21 @@ def make_default_cli(app):
|
||||||
return click.Group()
|
return click.Group()
|
||||||
|
|
||||||
|
|
||||||
cli = FlaskGroup(help='''\
|
@click.group(cls=FlaskGroup)
|
||||||
This shell command acts as general utility script for Flask applications.
|
def cli(**params):
|
||||||
|
"""
|
||||||
|
This shell command acts as general utility script for Flask applications.
|
||||||
|
|
||||||
It loads the application configured (either through the FLASK_APP environment
|
It loads the application configured (either through the FLASK_APP environment
|
||||||
variable or the --app parameter) and then provides commands either provided
|
variable or the --app parameter) and then provides commands either provided
|
||||||
by the application or Flask itself.
|
by the application or Flask itself.
|
||||||
|
|
||||||
The most useful commands are the "run" and "shell" command.
|
The most useful commands are the "run" and "shell" command.
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
flask --app=hello --debug run
|
flask --app=hello --debug run
|
||||||
''')
|
"""
|
||||||
|
|
||||||
|
|
||||||
def main(as_module=False):
|
def main(as_module=False):
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -96,7 +96,7 @@ setup(
|
||||||
'Werkzeug>=0.7',
|
'Werkzeug>=0.7',
|
||||||
'Jinja2>=2.4',
|
'Jinja2>=2.4',
|
||||||
'itsdangerous>=0.21',
|
'itsdangerous>=0.21',
|
||||||
'click',
|
'click>=0.6',
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue