forked from orbit-oss/flask
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
|
||||
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
|
||||
instanciate your application properly by itself. Because of this reason
|
||||
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
|
||||
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
|
||||
|
||||
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])
|
||||
|
||||
|
||||
def locate_app(app_id, debug=None):
|
||||
def locate_app(app_id):
|
||||
"""Attempts to locate the application."""
|
||||
if ':' in app_id:
|
||||
module, app_obj = app_id.split(':', 1)
|
||||
|
|
@ -98,8 +98,7 @@ def locate_app(app_id, debug=None):
|
|||
if app is None:
|
||||
raise RuntimeError('Failed to find application in module "%s"'
|
||||
% module)
|
||||
if debug is not None:
|
||||
app.debug = debug
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
|
@ -145,13 +144,24 @@ class ScriptInfo(object):
|
|||
"""
|
||||
|
||||
def __init__(self, app_import_path=None, debug=None, create_app=None):
|
||||
#: The application 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
|
||||
#: Optionally a function that is passed the script info to create
|
||||
#: the instance of the application.
|
||||
self.create_app = create_app
|
||||
#: A dictionary with arbitrary data that can be associated with
|
||||
#: this script info.
|
||||
self.data = {}
|
||||
self._loaded_app = None
|
||||
|
||||
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:
|
||||
return self._loaded_app
|
||||
if self.create_app is not None:
|
||||
|
|
@ -159,29 +169,12 @@ class ScriptInfo(object):
|
|||
else:
|
||||
if self.app_import_path is None:
|
||||
_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
|
||||
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
|
||||
def conditional_context(self, with_context=True):
|
||||
"""Creates an application context or not, depending on the given
|
||||
|
|
@ -189,17 +182,12 @@ class ScriptInfo(object):
|
|||
shortcut for a common operation.
|
||||
"""
|
||||
if with_context:
|
||||
with self.load_app(self).app_context() as ctx:
|
||||
with self.load_app().app_context() as ctx:
|
||||
yield ctx
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
|
|
@ -213,54 +201,65 @@ def without_appcontext(f):
|
|||
return f
|
||||
|
||||
|
||||
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.
|
||||
def set_debug_value(ctx, value):
|
||||
ctx.ensure_object(ScriptInfo).debug = value
|
||||
|
||||
: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,
|
||||
add_default_commands=True,
|
||||
create_app=None, **extra):
|
||||
click.Group.__init__(self, **extra)
|
||||
def __init__(self, add_default_commands=True, add_app_option=None,
|
||||
add_debug_option=True, create_app=None, **extra):
|
||||
params = list(extra.pop('params', None) or ())
|
||||
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
|
||||
if add_default_options:
|
||||
self.add_app_option()
|
||||
self.add_debug_option()
|
||||
|
||||
if add_default_commands:
|
||||
self.add_command(run_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):
|
||||
info = ctx.ensure_object(ScriptInfo)
|
||||
# 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)
|
||||
|
||||
|
||||
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.option('--host', '-h', default='127.0.0.1',
|
||||
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:
|
||||
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
|
||||
# not double execute when the reloader kicks in.
|
||||
|
|
@ -388,19 +414,21 @@ def make_default_cli(app):
|
|||
return click.Group()
|
||||
|
||||
|
||||
cli = FlaskGroup(help='''\
|
||||
This shell command acts as general utility script for Flask applications.
|
||||
@click.group(cls=FlaskGroup)
|
||||
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
|
||||
variable or the --app parameter) and then provides commands either provided
|
||||
by the application or Flask itself.
|
||||
It loads the application configured (either through the FLASK_APP environment
|
||||
variable or the --app parameter) and then provides commands either provided
|
||||
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):
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -96,7 +96,7 @@ setup(
|
|||
'Werkzeug>=0.7',
|
||||
'Jinja2>=2.4',
|
||||
'itsdangerous>=0.21',
|
||||
'click',
|
||||
'click>=0.6',
|
||||
],
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue