Further refactored script system to allow more customization for special apps.
This commit is contained in:
parent
ed7b4ccac1
commit
81576c236a
1 changed files with 116 additions and 74 deletions
190
flask/cli.py
190
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class FlaskGroup(ContextGroupMixin, click.Group):
|
@click.command('run', short_help='Runs a development server.')
|
||||||
"""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
|
|
||||||
|
|
||||||
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.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…
Add table
Add a link
Reference in a new issue