Browse Source

Further refactored script system to allow more customization for special apps.

pull/1028/merge
Armin Ronacher 11 years ago
parent
commit
81576c236a
  1. 188
      flask/cli.py

188
flask/cli.py

@ -144,57 +144,62 @@ class ScriptInfo(object):
to click. 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.app_import_path = app_import_path
self.debug = debug self.debug = debug
self.load_callback = load_callback self.create_app = create_app
self._loaded_app = None 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): 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: if self._loaded_app is not None:
return self._loaded_app return self._loaded_app
if self.load_callback is not None: if self.create_app is not None:
rv = self.load_callback() rv = self.create_app(self)
else: 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 self._loaded_app = rv
return rv return rv
def make_wsgi_app(self, use_eager_loading=False): def make_wsgi_app(self, use_eager_loading=False):
"""Returns a WSGI app that loads the actual application at a """Returns a WSGI app that loads the actual application at a later
later stage. 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: if self.app_import_path is not None:
def loader(): def loader():
return locate_app(self.app_import_path, self.debug) return locate_app(self.app_import_path, self.debug)
else: else:
if self.load_callback is None: if self.create_app is None:
_no_such_app() _no_such_app()
def loader(): def loader():
return self.load_callback() return self.create_app(self)
return DispatchingApp(loader, use_eager_loading=use_eager_loading) 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 """Creates an application context or not, depending on the given
given parameter but always works as context manager. parameter but always works as context manager. This is just a
shortcut for a common operation.
""" """
if with_context: if with_context:
with self.load_app().app_context() as ctx: with self.load_app(self).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)
@ -208,10 +213,56 @@ def without_appcontext(f):
return 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): 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. # Find the command in the application first, if we can find it.
# If the app is not available, we just ignore this silently. # If the app is not available, we just ignore this silently.
try: try:
@ -220,13 +271,13 @@ class ContextGroupMixin(object):
return rv return rv
except NoAppException: except NoAppException:
pass pass
return super(ContextGroupMixin, self).get_command(ctx, name) return click.Group.get_command(self, ctx, name)
def list_commands(self, ctx): def list_commands(self, ctx):
# The commands available is the list of both the application (if # The commands available is the list of both the application (if
# available) plus the builtin commands. # available) plus the builtin commands.
rv = set(super(ContextGroupMixin, self).list_commands(ctx)) rv = set(click.Group.list_commands(self, ctx))
info = ctx.find_object(ScriptInfo) info = ctx.ensure_object(ScriptInfo)
try: try:
rv.update(info.load_app().cli.list_commands(ctx)) rv.update(info.load_app().cli.list_commands(ctx))
except NoAppException: except NoAppException:
@ -238,52 +289,19 @@ class ContextGroupMixin(object):
not getattr(cmd.callback, '__flask_without_appcontext__', False) not getattr(cmd.callback, '__flask_without_appcontext__', False)
with ctx.find_object(ScriptInfo).conditional_context(with_context): with ctx.find_object(ScriptInfo).conditional_context(with_context):
return super(ContextGroupMixin, self).invoke_subcommand( return click.Group.invoke_subcommand(
ctx, cmd, cmd_name, args) self, ctx, cmd, cmd_name, args)
class FlaskGroup(ContextGroupMixin, click.Group): def main(self, *args, **kwargs):
"""Special subclass of the a regular click group that supports obj = kwargs.get('obj')
loading more commands from the configured Flask app. if obj is None:
""" obj = ScriptInfo(create_app=self.create_app)
kwargs['obj'] = obj
def __init__(self, help=None): kwargs.setdefault('auto_envvar_prefix', 'FLASK')
def set_app_id(ctx, value): return click.Group.main(self, *args, **kwargs)
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
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)
])
@click.command('run', short_help='Runs a development server.')
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.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.')
@click.option('--port', '-p', default=5000, @click.option('--port', '-p', default=5000,
@ -303,7 +321,17 @@ Example usage:
@pass_script_info @pass_script_info
def run_command(info, host, port, reload, debugger, eager_loading, def run_command(info, host, port, reload, debugger, eager_loading,
with_threads): 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 from werkzeug.serving import run_simple
if reload is None: if reload is None:
reload = info.debug reload = info.debug
@ -330,7 +358,7 @@ def run_command(info, host, port, reload, debugger, eager_loading,
use_debugger=debugger, threaded=with_threads) 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(): def shell_command():
"""Runs an interactive Python shell in the context of a given """Runs an interactive Python shell in the context of a given
Flask application. The application will populate the default Flask application. The application will populate the default
@ -360,6 +388,21 @@ def make_default_cli(app):
return click.Group() 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): def main(as_module=False):
this_module = __package__ + '.cli' this_module = __package__ + '.cli'
args = sys.argv[1:] args = sys.argv[1:]
@ -377,8 +420,7 @@ def main(as_module=False):
else: else:
name = None name = None
cli.main(args=args, prog_name=name, obj=ScriptInfo(), cli.main(args=args, prog_name=name)
auto_envvar_prefix='FLASK')
if __name__ == '__main__': if __name__ == '__main__':

Loading…
Cancel
Save