Browse Source

Greatly refactored click integration and documented it a bit more.

pull/1028/merge
Armin Ronacher 11 years ago
parent
commit
3569fc2441
  1. 27
      docs/api.rst
  2. 114
      docs/cli.rst
  3. 188
      flask/cli.py
  4. 2
      setup.py

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

@ -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

@ -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
def set_debug_value(ctx, value):
ctx.ensure_object(ScriptInfo).debug = value
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): class FlaskGroup(click.Group):
"""Special subclass of the a regular click group that supports """Special subclass of the a regular click group that supports loading
loading more commands from the configured Flask app. Normally a more commands from the configured Flask app. Normally a developer
developer does not have to interface with this class but there are does not have to interface with this class but there are some very
some very advanced usecases for which it makes sense to create an advanced usecases for which it makes sense to create an instance of
instance of this. this.
:param add_default_options: if this is True the app and debug option For information as of why this is useful see :ref:`custom-scripts`.
is automatically added.
: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

@ -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…
Cancel
Save