diff --git a/docs/api.rst b/docs/api.rst index 6c9f7414..945082bc 100644 --- a/docs/api.rst +++ b/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 diff --git a/docs/cli.rst b/docs/cli.rst index 0ac578e9..171352eb 100644 --- a/docs/cli.rst +++ b/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. diff --git a/flask/cli.py b/flask/cli.py index aa385011..a1308e85 100644 --- a/flask/cli.py +++ b/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): diff --git a/setup.py b/setup.py index 196eca4c..1df413b2 100644 --- a/setup.py +++ b/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',