From 3bdb90f06b9d3167320180d4a5055dcd949bf72f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Apr 2014 13:26:23 +0200 Subject: [PATCH] Added click support to Flask --- flask/__main__.py | 2 +- flask/app.py | 47 ++++++- flask/cli.py | 347 ++++++++++++++++++++++++++++++++++++++++++++++ flask/run.py | 247 --------------------------------- 4 files changed, 392 insertions(+), 251 deletions(-) create mode 100644 flask/cli.py delete mode 100644 flask/run.py diff --git a/flask/__main__.py b/flask/__main__.py index e3c3bd34..cbcdebf1 100644 --- a/flask/__main__.py +++ b/flask/__main__.py @@ -11,5 +11,5 @@ if __name__ == '__main__': - from run import main + from cli import main main(as_module=True) diff --git a/flask/app.py b/flask/app.py index bb5cbbd7..3e421f3d 100644 --- a/flask/app.py +++ b/flask/app.py @@ -34,6 +34,7 @@ from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception, \ request_tearing_down, appcontext_tearing_down +from .cli import make_default_cli from ._compat import reraise, string_types, text_type, integer_types # a lock used for logger initialization @@ -476,6 +477,12 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } + #: A list of shell context processor functions that should be run + #: when a shell context is created. + #: + #: .. versionadded:: 1.0 + self.shell_context_processors = [] + #: all the attached blueprints in a dictionary by name. Blueprints #: can be attached multiple times so this dictionary does not tell #: you how often they got attached. @@ -531,6 +538,14 @@ class Flask(_PackageBoundObject): endpoint='static', view_func=self.send_static_file) + #: The click command line context for this application. Commands + #: registered here show up in the ``flask`` command once the + #: application has been discovered. The default commands are + #: provided by Flask itself and can be overridden. + #: + #: This is an instance of a :class:`click.Group` object. + self.cli = make_default_cli(self) + def _get_error_handlers(self): from warnings import warn warn(DeprecationWarning('error_handlers is deprecated, use the ' @@ -748,6 +763,18 @@ class Flask(_PackageBoundObject): # existing views. context.update(orig_ctx) + def make_shell_context(self): + """Returns the shell context for an interactive shell for this + application. This runs all the registered shell context + processors. + + .. versionadded:: 1.0 + """ + rv = {'app': self, 'g': g} + for processor in self.shell_context_processors: + rv.update(processor()) + return rv + def run(self, host=None, port=None, debug=None, **options): """Runs the application on a local development server. If the :attr:`debug` flag is set the server will automatically reload @@ -758,6 +785,11 @@ class Flask(_PackageBoundObject): ``use_evalex=False`` as parameter. This will keep the debugger's traceback screen active, but disable code execution. + It is not recommended to use this function for development with + automatic reloading as this is badly supported. Instead you should + be using the ``flask`` command line script's ``runserver`` + support. + .. admonition:: Keep in Mind Flask will suppress any server error with a generic error page @@ -1091,7 +1123,7 @@ class Flask(_PackageBoundObject): Use :meth:`register_error_handler` instead of modifying :attr:`error_handler_spec` directly, for application wide error handlers. - + .. versionadded:: 0.7 One can now additionally also register custom exception types that do not necessarily have to be a subclass of the @@ -1325,6 +1357,15 @@ class Flask(_PackageBoundObject): self.template_context_processors[None].append(f) return f + @setupmethod + def shell_context_processor(self, f): + """Registers a shell context processor function. + + .. versionadded:: 1.0 + """ + self.shell_context_processors.append(f) + return f + @setupmethod def url_value_preprocessor(self, f): """Registers a function as URL value preprocessor for all view @@ -1609,7 +1650,8 @@ class Flask(_PackageBoundObject): # some extra logic involved when creating these objects with # specific values (like default content type selection). if isinstance(rv, (text_type, bytes, bytearray)): - rv = self.response_class(rv, headers=headers, status=status_or_headers) + rv = self.response_class(rv, headers=headers, + status=status_or_headers) headers = status_or_headers = None else: rv = self.response_class.force_type(rv, request.environ) @@ -1624,7 +1666,6 @@ class Flask(_PackageBoundObject): return rv - def create_url_adapter(self, request): """Creates a URL adapter for the given request. The URL adapter is created at a point where the request context is not yet set up diff --git a/flask/cli.py b/flask/cli.py new file mode 100644 index 00000000..7e982878 --- /dev/null +++ b/flask/cli.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +""" + flask.run + ~~~~~~~~~ + + A simple command line application to run flask apps. + + :copyright: (c) 2014 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +import os +import sys +from threading import Lock +from contextlib import contextmanager + +import click + +from ._compat import iteritems + + +class NoAppException(click.UsageError): + """Raised if an application cannot be found or loaded.""" + + +def find_best_app(module): + """Given a module instance this tries to find the best possible + application in the module or raises an exception. + """ + from . import Flask + + # Search for the most common names first. + for attr_name in 'app', 'application': + app = getattr(module, attr_name, None) + if app is not None and isinstance(app, Flask): + return app + + # Otherwise find the only object that is a Flask instance. + matches = [v for k, v in iteritems(module.__dict__) + if isinstance(v, Flask)] + + if matches: + if len(matches) > 1: + raise NoAppException('More than one possible Flask application ' + 'found in module "%s", none of which are called ' + '"app". Be explicit!' % module.__name__) + return matches[0] + + raise NoAppException('Failed to find application in module "%s". Are ' + 'you sure it contains a Flask application? Maybe ' + 'you wrapped it in a WSGI middleware or you are ' + 'using a factory function.' % module.__name__) + + +def prepare_exec_for_file(filename): + module = [] + + # Chop off file extensions or package markers + if filename.endswith('.py'): + filename = filename[:-3] + elif os.path.split(filename)[1] == '__init__.py': + filename = os.path.dirname(filename) + filename = os.path.realpath(filename) + + dirpath = filename + while 1: + dirpath, extra = os.path.split(dirpath) + module.append(extra) + if not os.path.isfile(os.path.join(dirpath, '__init__.py')): + break + + sys.path.insert(0, dirpath) + return '.'.join(module[::-1]) + + +def locate_app(app_id, debug=None): + """Attempts to locate the application.""" + if ':' in app_id: + module, app_obj = app_id.split(':', 1) + else: + module = app_id + app_obj = None + + __import__(module) + mod = sys.modules[module] + if app_obj is None: + app = find_best_app(mod) + else: + app = getattr(mod, app_obj, 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 + + +class DispatchingApp(object): + """Special applicationt that dispatches to a flask application which + is imported by name on first request. This is safer than importing + the application upfront because it means that we can forward all + errors for import problems into the browser as error. + """ + + def __init__(self, app_id, debug=None, use_eager_loading=False): + self.app_id = app_id + self.app = None + self.debug = debug + self._lock = Lock() + if use_eager_loading: + self._load_unlocked() + + def _load_unlocked(self): + self.app = rv = locate_app(self.app_id, self.debug) + return rv + + def __call__(self, environ, start_response): + if self.app is not None: + return self.app(environ, start_response) + with self._lock: + if self.app is not None: + rv = self.app + else: + rv = self._load_unlocked() + return rv(environ, start_response) + + +class ScriptInfo(object): + """Help object to deal with Flask applications. This is usually not + necessary to interface with as it's used internally in the dispatching + to click. + """ + + def __init__(self): + self.app_import_path = None + self.debug = 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 + raise NoAppException('Could not locate application. ' + 'You did not provide FLASK_APP or the ' + '--app parameter.') + + def load_app(self): + """Loads the app (if not yet loaded) and returns it.""" + if self._loaded_app is not None: + return self._loaded_app + rv = locate_app(self.get_app_import_path(), self.debug) + self._loaded_app = rv + return rv + + @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. + """ + if with_context: + with self.load_app().app_context() as ctx: + yield ctx + else: + yield None + + +pass_script_info = click.make_pass_decorator(ScriptInfo) + + +def without_appcontext(f): + """Marks a click callback so that it does not get a app context + created. This only works for commands directly registered to + the toplevel system. This really is only useful for very + special commands like the runserver one. + """ + f.__flask_without_appcontext__ = True + return f + + +class FlaskClickGroup(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) or os.sep in value or \ + os.altsep is not None and os.altsep in value: + value = prepare_exec_for_file(value) + ctx.obj.app_import_path = value + def set_debug(ctx, value): + ctx.obj.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) + ]) + + def get_command(self, ctx, name): + info = ctx.find_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: + rv = info.load_app().cli.get_command(ctx, name) + if rv is not None: + return rv + except NoAppException: + pass + 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(click.Group.list_commands(self, ctx)) + info = ctx.find_object(ScriptInfo) + try: + rv.update(info.load_app().cli.list_commands(ctx)) + except NoAppException: + pass + return sorted(rv) + + def invoke_subcommand(self, ctx, cmd, cmd_name, args): + with_context = cmd.callback is None or \ + not getattr(cmd.callback, '__flask_without_appcontext__', False) + + with ctx.find_object(ScriptInfo).conditional_context(with_context): + return click.Group.invoke_subcommand( + self, ctx, cmd, cmd_name, args) + + +cli = FlaskClickGroup(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', + help='The interface to bind to.') +@click.option('--port', '-p', default=5000, + help='The port to bind to.') +@click.option('--reload/--no-reload', default=None, + help='Enable or disable the reloader. By default the reloader ' + 'is active is debug is enabled.') +@click.option('--debugger/--no-debugger', default=None, + help='Enable or disable the debugger. By default the debugger ' + 'is active if debug is enabled.') +@click.option('--eager-loading/--lazy-loader', default=None, + help='Enable or disable eager loading. By default eager ' + 'loading is enabled if the reloader is disabled.') +@click.option('--with-threads/--without-threads', default=False, + help='Enable or disable multithreading.') +@without_appcontext +@pass_script_info +def run_command(info, host, port, reload, debugger, eager_loading, + with_threads): + """Runs a local development server for the Flask application.""" + from werkzeug.serving import run_simple + app_id = info.get_app_import_path() + if reload is None: + reload = info.debug + if debugger is None: + debugger = info.debug + if eager_loading is None: + eager_loading = not reload + + # Extra startup messages. This depends a but on Werkzeug internals to + # not double execute when the reloader kicks in. + if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': + print(' * Serving Flask app "%s"' % app_id) + if info.debug is not None: + print(' * Forcing debug %s' % (info.debug and 'on' or 'off')) + + app = DispatchingApp(app_id, info.debug, eager_loading) + run_simple(host, port, app, use_reloader=reload, + use_debugger=debugger, threaded=with_threads) + + +@cli.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 + namespace of this shell according to it's configuration. + + This is useful for executing small snippets of management code + without having to manually configuring the application. + """ + import code + from flask.globals import _app_ctx_stack + app = _app_ctx_stack.top.app + banner = 'Python %s on %s\nApp: %s%s\nInstance: %s' % ( + sys.version, + sys.platform, + app.import_name, + app.debug and ' [debug]' or '', + app.instance_path, + ) + code.interact(banner=banner, local=app.make_shell_context()) + + +def make_default_cli(app): + """Creates the default click object for the app itself. Currently + there are no default commands registered because all builtin commands + are registered on the actual cmd object here. + """ + return click.Group() + + +def main(as_module=False): + this_module = __package__ + '.cli' + args = sys.argv[1:] + + if as_module: + if sys.version_info >= (2, 7): + name = 'python -m ' + this_module.rsplit('.', 1)[0] + else: + name = 'python -m ' + this_module + + # This module is always executed as "python -m flask.run" and as such + # we need to ensure that we restore the actual command line so that + # the reloader can properly operate. + sys.argv = ['-m', this_module] + sys.argv[1:] + else: + name = 'flask' + + cli.main(args=args, prog_name=name, obj=ScriptInfo(), + auto_envvar_prefix='FLASK') + + +if __name__ == '__main__': + main(as_module=True) diff --git a/flask/run.py b/flask/run.py deleted file mode 100644 index 9d86df02..00000000 --- a/flask/run.py +++ /dev/null @@ -1,247 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.run - ~~~~~~~~~ - - A simple command line application to run flask apps. - - :copyright: (c) 2014 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" - -import os -import sys -from threading import Lock -from optparse import OptionParser - -from werkzeug.serving import run_simple - -from ._compat import iteritems - - -def find_best_app(module): - """Given a module instance this tries to find the best possible application - in the module or raises a RuntimeError. - """ - from flask import Flask - - # The app name wins, even if it's not a flask object. - app = getattr(module, 'app', None) - if app is not None and callable(app): - return app - - # Otherwise find the first object named Flask - matches = [] - for key, value in iteritems(module.__dict__): - if isinstance(value, Flask): - matches.append(value) - - if matches: - if len(matches) > 1: - raise RuntimeError('More than one possible Flask application ' - 'found in module "%s", none of which are called ' - '"app". Be explicit!' % module) - return matches[0] - - raise RuntimeError('Failed to find application in module "%s". Are ' - 'you sure it contains a Flask application? Maybe ' - 'you wrapped it in a WSGI middleware or you are ' - 'using a factory function.' % module) - - -def prepare_exec_for_file(filename): - module = [] - - # Chop off file extensions or package markers - if filename.endswith('.py'): - filename = filename[:-3] - elif os.path.split(filename)[1] == '__init__.py': - filename = os.path.dirname(filename) - filename = os.path.realpath(filename) - - dirpath = filename - while 1: - dirpath, extra = os.path.split(dirpath) - module.append(extra) - if not os.path.isfile(os.path.join(dirpath, '__init__.py')): - break - - sys.path.insert(0, dirpath) - return '.'.join(module[::-1]) - - -def locate_app(app_id, debug=None): - """Attempts to locate the application.""" - if ':' in app_id: - module, app_obj = app_id.split(':', 1) - else: - module = app_id - app_obj = None - - __import__(module) - mod = sys.modules[module] - if app_obj is None: - app = find_best_app(mod) - else: - app = getattr(mod, app_obj, 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 - - -class DispatchingApp(object): - """Special applicationt that dispatches to a flask application which - is imported by name on first request. This is safer than importing - the application upfront because it means that we can forward all - errors for import problems into the browser as error. - """ - - def __init__(self, app_id, debug=None, use_eager_loading=False): - self.app_id = app_id - self.app = None - self.debug = debug - self._lock = Lock() - if use_eager_loading: - self._load_unlocked() - - def _load_unlocked(self): - self.app = rv = locate_app(self.app_id, self.debug) - return rv - - def __call__(self, environ, start_response): - if self.app is not None: - return self.app(environ, start_response) - with self._lock: - if self.app is not None: - rv = self.app - else: - rv = self._load_unlocked() - return rv(environ, start_response) - - -def run_application(app_id, host='127.0.0.1', port=5000, debug=None, - use_reloader=False, use_debugger=False, - use_eager_loading=None, magic_app_id=True, - **options): - """Useful function to start a Werkzeug server for an application that - is known by it's import name. By default the app ID can also be a - full file name in which case Flask attempts to reconstruct the import - name from it and do the right thing. - - :param app_id: the import name of the application module. If a colon - is provided, everything afterwards is the application - object name. In case the magic app id is enabled this - can also be a filename. - :param host: the host to bind to. - :param port: the port to bind to. - :param debug: if set to something other than None then the application's - debug flag will be set to this. - :param use_reloader: enables or disables the reloader. - :param use_debugger: enables or disables the builtin debugger. - :param use_eager_loading: enables or disables eager loading. This is - normally conditional to the reloader. - :param magic_app_id: if this is enabled then the app id can also be a - filename instead of an import module and Flask - will attempt to reconstruct the import name. - :param options: the options to be forwarded to the underlying - Werkzeug server. See - :func:`werkzeug.serving.run_simple` for more - information. - """ - if magic_app_id: - if os.path.isfile(app_id) or os.sep in app_id or \ - os.altsep is not None and os.altsep in app_id: - app_id = prepare_exec_for_file(app_id) - - if use_eager_loading is None: - use_eager_loading = not use_reloader - - # Extra startup messages. This depends a but on Werkzeug internals to - # not double execute when the reloader kicks in. - if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': - print ' * Serving Flask app "%s"' % app_id - if debug is not None: - print ' * Forcing debug %s' % (debug and 'on' or 'off') - - app = DispatchingApp(app_id, debug, use_eager_loading) - run_simple(host, port, app, use_reloader=use_reloader, - use_debugger=use_debugger, **options) - - -def main(as_module=False): - this_module = __package__ + '.run' - - if as_module: - if sys.version_info >= (2, 7): - name = 'python -m ' + this_module.rsplit('.', 1)[0] - else: - name = 'python -m ' + this_module - else: - name = 'flask-run' - - parser = OptionParser(usage='%prog [options] module', prog=name) - parser.add_option('--debug', action='store_true', - dest='debug', help='Flip debug flag on. If enabled ' - 'this also affects debugger and reloader defaults.') - parser.add_option('--no-debug', action='store_false', - dest='debug', help='Flip debug flag off.') - parser.add_option('--host', default='127.0.0.1', - help='The host to bind on. (defaults to 127.0.0.1)') - parser.add_option('--port', default=5000, - help='The port to bind on. (defaults to 5000)') - parser.add_option('--with-reloader', action='store_true', - dest='with_reloader', - help='Enable the reloader.') - parser.add_option('--without-reloader', action='store_false', - dest='with_reloader', - help='Disable the reloader.') - parser.add_option('--with-debugger', action='store_true', - dest='with_debugger', - help='Enable the debugger.') - parser.add_option('--without-debugger', action='store_false', - dest='with_debugger', - help='Disable the debugger.') - parser.add_option('--with-eager-loading', action='store_true', - dest='with_eager_loading', - help='Force enable the eager-loading. This makes the ' - 'application load immediately but makes development ' - 'flows harder. It\'s not recommended to enable eager ' - 'loading when the reloader is enabled as it can lead ' - 'to unexpected crashes.') - parser.add_option('--without-eager-loading', action='store_false', - dest='with_eager_loading', - help='Disable the eager-loading.') - parser.add_option('--with-threads', action='store_true', - dest='with_threads', - help='Enable multi-threading to handle multiple ' - 'requests concurrently.') - parser.add_option('--without-threads', action='store_false', - dest='with_threads', - help='Disables multi-threading. (default)') - opts, args = parser.parse_args() - if len(args) != 1: - parser.error('Expected exactly one argument which is the import ' - 'name of the application.') - - if opts.with_debugger is None: - opts.with_debugger = opts.debug - if opts.with_reloader is None: - opts.with_reloader = opts.debug - - # This module is always executed as "python -m flask.run" and as such - # we need to ensure that we restore the actual command line so that - # the reloader can properly operate. - sys.argv = ['-m', this_module] + sys.argv[1:] - - run_application(args[0], opts.host, opts.port, debug=opts.debug, - use_reloader=opts.with_reloader, - use_debugger=opts.with_debugger, - use_eager_loading=opts.with_eager_loading, - threaded=opts.with_threads) - - -if __name__ == '__main__': - main(as_module=True)