Browse Source

Implemented simplified CLI interface

pull/1820/head
Armin Ronacher 9 years ago
parent
commit
523e271183
  1. 2
      Makefile
  2. 5
      flask/app.py
  3. 122
      flask/cli.py
  4. 8
      flask/helpers.py
  5. 3
      tests/test_cli.py

2
Makefile

@ -3,7 +3,7 @@
all: clean-pyc test all: clean-pyc test
test: test:
py.test tests examples FLASK_DEBUG= py.test tests examples
tox-test: tox-test:
tox tox

5
flask/app.py

@ -22,7 +22,8 @@ from werkzeug.exceptions import HTTPException, InternalServerError, \
MethodNotAllowed, BadRequest, default_exceptions MethodNotAllowed, BadRequest, default_exceptions
from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
locked_cached_property, _endpoint_from_view_func, find_package locked_cached_property, _endpoint_from_view_func, find_package, \
get_debug_flag
from . import json, cli from . import json, cli
from .wrappers import Request, Response from .wrappers import Request, Response
from .config import ConfigAttribute, Config from .config import ConfigAttribute, Config
@ -289,7 +290,7 @@ class Flask(_PackageBoundObject):
#: Default configuration parameters. #: Default configuration parameters.
default_config = ImmutableDict({ default_config = ImmutableDict({
'DEBUG': False, 'DEBUG': get_debug_flag(default=False),
'TESTING': False, 'TESTING': False,
'PROPAGATE_EXCEPTIONS': None, 'PROPAGATE_EXCEPTIONS': None,
'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None,

122
flask/cli.py

@ -17,6 +17,7 @@ from functools import update_wrapper
import click import click
from ._compat import iteritems, reraise from ._compat import iteritems, reraise
from .helpers import get_debug_flag
class NoAppException(click.UsageError): class NoAppException(click.UsageError):
@ -98,6 +99,15 @@ def locate_app(app_id):
return app return app
def find_default_import_path():
app = os.environ.get('FLASK_APP')
if app is None:
return
if os.path.isfile(app):
return prepare_exec_for_file(app)
return app
class DispatchingApp(object): class DispatchingApp(object):
"""Special application that dispatches to a flask application which """Special application that dispatches to a flask application which
is imported by name in a background thread. If an error happens is imported by name in a background thread. If an error happens
@ -158,12 +168,13 @@ class ScriptInfo(object):
to click. to click.
""" """
def __init__(self, app_import_path=None, debug=None, create_app=None): def __init__(self, app_import_path=None, create_app=None):
#: The application import path if create_app is None:
if app_import_path is None:
app_import_path = find_default_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 else:
#: automatically have it's debug flag overridden with this value. self.app_import_path = None
self.debug = debug
#: Optionally a function that is passed the script info to create #: Optionally a function that is passed the script info to create
#: the instance of the application. #: the instance of the application.
self.create_app = create_app self.create_app = create_app
@ -185,11 +196,12 @@ class ScriptInfo(object):
else: else:
if self.app_import_path is None: if self.app_import_path is None:
raise NoAppException('Could not locate Flask application. ' raise NoAppException('Could not locate Flask application. '
'You did not provide FLASK_APP or the ' 'You did not provide the FLASK_APP '
'--app parameter.') 'environment variable.')
rv = locate_app(self.app_import_path) rv = locate_app(self.app_import_path)
if self.debug is not None: debug = get_debug_flag()
rv.debug = self.debug if debug is not None:
rv.debug = debug
self._loaded_app = rv self._loaded_app = rv
return rv return rv
@ -210,29 +222,6 @@ def with_appcontext(f):
return update_wrapper(decorator, f) return update_wrapper(decorator, f)
def set_debug_value(ctx, param, value):
ctx.ensure_object(ScriptInfo).debug = value
def set_app_value(ctx, param, 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 AppGroup(click.Group): class AppGroup(click.Group):
"""This works similar to a regular click :class:`~click.Group` but it """This works similar to a regular click :class:`~click.Group` but it
changes the behavior of the :meth:`command` decorator so that it changes the behavior of the :meth:`command` decorator so that it
@ -273,25 +262,12 @@ class FlaskGroup(AppGroup):
:param add_default_commands: if this is True then the default run and :param add_default_commands: if this is True then the default run and
shell commands wil be added. shell commands wil be added.
:param add_app_option: adds the default :option:`--app` option. This gets
automatically disabled if a `create_app`
callback is defined.
:param add_debug_option: adds the default :option:`--debug` option.
:param create_app: an optional callback that is passed the script info :param create_app: an optional callback that is passed the script info
and returns the loaded app. and returns the loaded app.
""" """
def __init__(self, add_default_commands=True, add_app_option=None, def __init__(self, add_default_commands=True, create_app=None, **extra):
add_debug_option=True, create_app=None, **extra): AppGroup.__init__(self, **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)
AppGroup.__init__(self, params=params, **extra)
self.create_app = create_app self.create_app = create_app
if add_default_commands: if add_default_commands:
@ -342,33 +318,6 @@ class FlaskGroup(AppGroup):
return AppGroup.main(self, *args, **kwargs) return AppGroup.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, param, 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.')
@ -400,10 +349,12 @@ def run_command(info, host, port, reload, debugger, eager_loading,
Flask is enabled and disabled otherwise. Flask is enabled and disabled otherwise.
""" """
from werkzeug.serving import run_simple from werkzeug.serving import run_simple
debug = get_debug_flag()
if reload is None: if reload is None:
reload = info.debug reload = bool(debug)
if debugger is None: if debugger is None:
debugger = info.debug debugger = bool(debug)
if eager_loading is None: if eager_loading is None:
eager_loading = not reload eager_loading = not reload
@ -418,12 +369,9 @@ def run_command(info, host, port, reload, debugger, eager_loading,
# we won't print anything. # we won't print anything.
if info.app_import_path is not None: if info.app_import_path is not None:
print(' * Serving Flask app "%s"' % info.app_import_path) print(' * Serving Flask app "%s"' % info.app_import_path)
if info.debug is not None:
print(' * Forcing debug %s' % (info.debug and 'on' or 'off'))
run_simple(host, port, app, use_reloader=reload, run_simple(host, port, app, use_reloader=reload,
use_debugger=debugger, threaded=with_threads, use_debugger=debugger, threaded=with_threads)
passthrough_errors=True)
@click.command('shell', short_help='Runs a shell in the app context.') @click.command('shell', short_help='Runs a shell in the app context.')
@ -464,15 +412,21 @@ cli = FlaskGroup(help="""\
This shell command acts as general utility script for Flask applications. 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) and then provides commands either provided by the application or
by the application or Flask itself. 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 \b
""") %(prefix)s%(cmd)s FLASK_APP=hello
%(prefix)s%(cmd)s FLASK_DEBUG=1
%(prefix)sflask run
""" % {
'cmd': os.name == 'posix' and 'export' or 'set',
'prefix': os.name == 'posix' and '$ ' or '',
})
def main(as_module=False): def main(as_module=False):

8
flask/helpers.py

@ -14,7 +14,6 @@ import sys
import pkgutil import pkgutil
import posixpath import posixpath
import mimetypes import mimetypes
from datetime import timedelta
from time import time from time import time
from zlib import adler32 from zlib import adler32
from threading import RLock from threading import RLock
@ -54,6 +53,13 @@ _os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep]
if sep not in (None, '/')) if sep not in (None, '/'))
def get_debug_flag(default=None):
val = os.environ.get('FLASK_DEBUG')
if not val:
return default
return val not in ('0', 'false', 'no')
def _endpoint_from_view_func(view_func): def _endpoint_from_view_func(view_func):
"""Internal helper that returns the default endpoint for a given """Internal helper that returns the default endpoint for a given
function. This always is the function name. function. This always is the function name.

3
tests/test_cli.py

@ -19,7 +19,7 @@ from click.testing import CliRunner
from flask import Flask, current_app from flask import Flask, current_app
from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \
find_best_app, locate_app, script_info_option, with_appcontext find_best_app, locate_app, with_appcontext
def test_cli_name(test_apps): def test_cli_name(test_apps):
@ -123,7 +123,6 @@ def test_flaskgroup():
return Flask("flaskgroup") return Flask("flaskgroup")
@click.group(cls=FlaskGroup, create_app=create_app) @click.group(cls=FlaskGroup, create_app=create_app)
@script_info_option('--config', script_info_key='config')
def cli(**params): def cli(**params):
pass pass

Loading…
Cancel
Save