Skip to content

Commit

Permalink
Further refactored script system to allow more customization for spec…
Browse files Browse the repository at this point in the history
…ial apps.
  • Loading branch information
mitsuhiko committed May 7, 2014
1 parent ed7b4cc commit 81576c2
Showing 1 changed file with 115 additions and 73 deletions.
188 changes: 115 additions & 73 deletions flask/cli.py
Expand Up @@ -144,57 +144,62 @@ class ScriptInfo(object):
to click.
"""

def __init__(self, app_import_path=None, debug=None, load_callback=None):
def __init__(self, app_import_path=None, debug=None, create_app=None):
self.app_import_path = app_import_path
self.debug = debug
self.load_callback = load_callback
self.create_app = create_app
self._loaded_app = None

def get_app_import_path(self):
"""Return the actual application import path or fails if it is
not yet set.
"""
if self.app_import_path is not None:
return self.app_import_path
_no_such_app()

def load_app(self):
"""Loads the app (if not yet loaded) and returns it."""
"""Loads the Flask app (if not yet loaded) and returns it."""
if self._loaded_app is not None:
return self._loaded_app
if self.load_callback is not None:
rv = self.load_callback()
if self.create_app is not None:
rv = self.create_app(self)
else:
rv = locate_app(self.get_app_import_path(), self.debug)
if self.app_import_path is None:
_no_such_app()
rv = locate_app(self.app_import_path, 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.
"""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.load_callback is None:
if self.create_app is None:
_no_such_app()
def loader():
return self.load_callback()
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 parameter but always works as context manager.
"""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:
with self.load_app(self).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)


Expand All @@ -208,10 +213,56 @@ def without_appcontext(f):
return f


class ContextGroupMixin(object):
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.
:param add_default_options: if this is True the app and debug option
is automatically added.
"""

def __init__(self, add_default_options=True,
add_default_commands=True,
create_app=None, **extra):
click.Group.__init__(self, **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.find_object(ScriptInfo)
info = ctx.ensure_object(ScriptInfo)
# Find the command in the application first, if we can find it.
# If the app is not available, we just ignore this silently.
try:
Expand All @@ -220,13 +271,13 @@ def get_command(self, ctx, name):
return rv
except NoAppException:
pass
return super(ContextGroupMixin, self).get_command(ctx, name)
return click.Group.get_command(self, ctx, name)

def list_commands(self, ctx):
# The commands available is the list of both the application (if
# available) plus the builtin commands.
rv = set(super(ContextGroupMixin, self).list_commands(ctx))
info = ctx.find_object(ScriptInfo)
rv = set(click.Group.list_commands(self, ctx))
info = ctx.ensure_object(ScriptInfo)
try:
rv.update(info.load_app().cli.list_commands(ctx))
except NoAppException:
Expand All @@ -238,52 +289,19 @@ def invoke_subcommand(self, ctx, cmd, cmd_name, args):
not getattr(cmd.callback, '__flask_without_appcontext__', False)

with ctx.find_object(ScriptInfo).conditional_context(with_context):
return super(ContextGroupMixin, self).invoke_subcommand(
ctx, cmd, cmd_name, args)

return click.Group.invoke_subcommand(
self, ctx, cmd, cmd_name, args)

class FlaskGroup(ContextGroupMixin, click.Group):
"""Special subclass of the a regular click group that supports
loading more commands from the configured Flask app.
"""

def __init__(self, help=None):
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
def set_debug(ctx, value):
ctx.ensure_object(ScriptInfo).debug = value
def main(self, *args, **kwargs):
obj = kwargs.get('obj')
if obj is None:
obj = ScriptInfo(create_app=self.create_app)
kwargs['obj'] = obj
kwargs.setdefault('auto_envvar_prefix', 'FLASK')
return click.Group.main(self, *args, **kwargs)

click.Group.__init__(self, help=help, params=[
click.Option(['-a', '--app'],
help='The application to run',
callback=set_app_id, is_eager=True),
click.Option(['--debug/--no-debug'],
help='Enable or disable debug mode.',
default=None, callback=set_debug)
])


cli = FlaskGroup(help='''\
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.
The most useful commands are the "run" and "shell" command.
Example usage:
flask --app=hello --debug run
''')


@cli.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',
help='The interface to bind to.')
@click.option('--port', '-p', default=5000,
Expand All @@ -303,7 +321,17 @@ def set_debug(ctx, value):
@pass_script_info
def run_command(info, host, port, reload, debugger, eager_loading,
with_threads):
"""Runs a local development server for the Flask application."""
"""Runs a local development server for the Flask application.
This local server is recommended for development purposes only but it
can also be used for simple intranet deployments. By default it will
not support any sort of concurrency at all to simplify debugging. This
can be changed with the --with-threads option which will enable basic
multithreading.
The reloader and debugger are by default enabled if the debug flag of
Flask is enabled and disabled otherwise.
"""
from werkzeug.serving import run_simple
if reload is None:
reload = info.debug
Expand All @@ -330,7 +358,7 @@ def run_command(info, host, port, reload, debugger, eager_loading,
use_debugger=debugger, threaded=with_threads)


@cli.command('shell', short_help='Runs a shell in the app context.')
@click.command('shell', short_help='Runs a shell in the app context.')
def shell_command():
"""Runs an interactive Python shell in the context of a given
Flask application. The application will populate the default
Expand Down Expand Up @@ -360,6 +388,21 @@ def make_default_cli(app):
return click.Group()


cli = FlaskGroup(help='''\
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.
The most useful commands are the "run" and "shell" command.
Example usage:
flask --app=hello --debug run
''')


def main(as_module=False):
this_module = __package__ + '.cli'
args = sys.argv[1:]
Expand All @@ -377,8 +420,7 @@ def main(as_module=False):
else:
name = None

cli.main(args=args, prog_name=name, obj=ScriptInfo(),
auto_envvar_prefix='FLASK')
cli.main(args=args, prog_name=name)


if __name__ == '__main__':
Expand Down

0 comments on commit 81576c2

Please sign in to comment.