forked from s434596/CatOrNot
899 lines
29 KiB
Python
899 lines
29 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
"""
|
||
|
flask.cli
|
||
|
~~~~~~~~~
|
||
|
|
||
|
A simple command line application to run flask apps.
|
||
|
|
||
|
:copyright: © 2010 by the Pallets team.
|
||
|
:license: BSD, see LICENSE for more details.
|
||
|
"""
|
||
|
|
||
|
from __future__ import print_function
|
||
|
|
||
|
import ast
|
||
|
import inspect
|
||
|
import os
|
||
|
import re
|
||
|
import ssl
|
||
|
import sys
|
||
|
import traceback
|
||
|
from functools import update_wrapper
|
||
|
from operator import attrgetter
|
||
|
from threading import Lock, Thread
|
||
|
|
||
|
import click
|
||
|
from werkzeug.utils import import_string
|
||
|
|
||
|
from . import __version__
|
||
|
from ._compat import getargspec, iteritems, reraise, text_type
|
||
|
from .globals import current_app
|
||
|
from .helpers import get_debug_flag, get_env, get_load_dotenv
|
||
|
|
||
|
try:
|
||
|
import dotenv
|
||
|
except ImportError:
|
||
|
dotenv = None
|
||
|
|
||
|
|
||
|
class NoAppException(click.UsageError):
|
||
|
"""Raised if an application cannot be found or loaded."""
|
||
|
|
||
|
|
||
|
def find_best_app(script_info, 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 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 len(matches) == 1:
|
||
|
return matches[0]
|
||
|
elif len(matches) > 1:
|
||
|
raise NoAppException(
|
||
|
'Detected multiple Flask applications in module "{module}". Use '
|
||
|
'"FLASK_APP={module}:name" to specify the correct '
|
||
|
'one.'.format(module=module.__name__)
|
||
|
)
|
||
|
|
||
|
# Search for app factory functions.
|
||
|
for attr_name in ('create_app', 'make_app'):
|
||
|
app_factory = getattr(module, attr_name, None)
|
||
|
|
||
|
if inspect.isfunction(app_factory):
|
||
|
try:
|
||
|
app = call_factory(script_info, app_factory)
|
||
|
|
||
|
if isinstance(app, Flask):
|
||
|
return app
|
||
|
except TypeError:
|
||
|
if not _called_with_wrong_args(app_factory):
|
||
|
raise
|
||
|
raise NoAppException(
|
||
|
'Detected factory "{factory}" in module "{module}", but '
|
||
|
'could not call it without arguments. Use '
|
||
|
'"FLASK_APP=\'{module}:{factory}(args)\'" to specify '
|
||
|
'arguments.'.format(
|
||
|
factory=attr_name, module=module.__name__
|
||
|
)
|
||
|
)
|
||
|
|
||
|
raise NoAppException(
|
||
|
'Failed to find Flask application or factory in module "{module}". '
|
||
|
'Use "FLASK_APP={module}:name to specify one.'.format(
|
||
|
module=module.__name__
|
||
|
)
|
||
|
)
|
||
|
|
||
|
|
||
|
def call_factory(script_info, app_factory, arguments=()):
|
||
|
"""Takes an app factory, a ``script_info` object and optionally a tuple
|
||
|
of arguments. Checks for the existence of a script_info argument and calls
|
||
|
the app_factory depending on that and the arguments provided.
|
||
|
"""
|
||
|
args_spec = getargspec(app_factory)
|
||
|
arg_names = args_spec.args
|
||
|
arg_defaults = args_spec.defaults
|
||
|
|
||
|
if 'script_info' in arg_names:
|
||
|
return app_factory(*arguments, script_info=script_info)
|
||
|
elif arguments:
|
||
|
return app_factory(*arguments)
|
||
|
elif not arguments and len(arg_names) == 1 and arg_defaults is None:
|
||
|
return app_factory(script_info)
|
||
|
|
||
|
return app_factory()
|
||
|
|
||
|
|
||
|
def _called_with_wrong_args(factory):
|
||
|
"""Check whether calling a function raised a ``TypeError`` because
|
||
|
the call failed or because something in the factory raised the
|
||
|
error.
|
||
|
|
||
|
:param factory: the factory function that was called
|
||
|
:return: true if the call failed
|
||
|
"""
|
||
|
tb = sys.exc_info()[2]
|
||
|
|
||
|
try:
|
||
|
while tb is not None:
|
||
|
if tb.tb_frame.f_code is factory.__code__:
|
||
|
# in the factory, it was called successfully
|
||
|
return False
|
||
|
|
||
|
tb = tb.tb_next
|
||
|
|
||
|
# didn't reach the factory
|
||
|
return True
|
||
|
finally:
|
||
|
del tb
|
||
|
|
||
|
|
||
|
def find_app_by_string(script_info, module, app_name):
|
||
|
"""Checks if the given string is a variable name or a function. If it is a
|
||
|
function, it checks for specified arguments and whether it takes a
|
||
|
``script_info`` argument and calls the function with the appropriate
|
||
|
arguments.
|
||
|
"""
|
||
|
from flask import Flask
|
||
|
match = re.match(r'^ *([^ ()]+) *(?:\((.*?) *,? *\))? *$', app_name)
|
||
|
|
||
|
if not match:
|
||
|
raise NoAppException(
|
||
|
'"{name}" is not a valid variable name or function '
|
||
|
'expression.'.format(name=app_name)
|
||
|
)
|
||
|
|
||
|
name, args = match.groups()
|
||
|
|
||
|
try:
|
||
|
attr = getattr(module, name)
|
||
|
except AttributeError as e:
|
||
|
raise NoAppException(e.args[0])
|
||
|
|
||
|
if inspect.isfunction(attr):
|
||
|
if args:
|
||
|
try:
|
||
|
args = ast.literal_eval('({args},)'.format(args=args))
|
||
|
except (ValueError, SyntaxError)as e:
|
||
|
raise NoAppException(
|
||
|
'Could not parse the arguments in '
|
||
|
'"{app_name}".'.format(e=e, app_name=app_name)
|
||
|
)
|
||
|
else:
|
||
|
args = ()
|
||
|
|
||
|
try:
|
||
|
app = call_factory(script_info, attr, args)
|
||
|
except TypeError as e:
|
||
|
if not _called_with_wrong_args(attr):
|
||
|
raise
|
||
|
|
||
|
raise NoAppException(
|
||
|
'{e}\nThe factory "{app_name}" in module "{module}" could not '
|
||
|
'be called with the specified arguments.'.format(
|
||
|
e=e, app_name=app_name, module=module.__name__
|
||
|
)
|
||
|
)
|
||
|
else:
|
||
|
app = attr
|
||
|
|
||
|
if isinstance(app, Flask):
|
||
|
return app
|
||
|
|
||
|
raise NoAppException(
|
||
|
'A valid Flask application was not obtained from '
|
||
|
'"{module}:{app_name}".'.format(
|
||
|
module=module.__name__, app_name=app_name
|
||
|
)
|
||
|
)
|
||
|
|
||
|
|
||
|
def prepare_import(path):
|
||
|
"""Given a filename this will try to calculate the python path, add it
|
||
|
to the search path and return the actual module name that is expected.
|
||
|
"""
|
||
|
path = os.path.realpath(path)
|
||
|
|
||
|
if os.path.splitext(path)[1] == '.py':
|
||
|
path = os.path.splitext(path)[0]
|
||
|
|
||
|
if os.path.basename(path) == '__init__':
|
||
|
path = os.path.dirname(path)
|
||
|
|
||
|
module_name = []
|
||
|
|
||
|
# move up until outside package structure (no __init__.py)
|
||
|
while True:
|
||
|
path, name = os.path.split(path)
|
||
|
module_name.append(name)
|
||
|
|
||
|
if not os.path.exists(os.path.join(path, '__init__.py')):
|
||
|
break
|
||
|
|
||
|
if sys.path[0] != path:
|
||
|
sys.path.insert(0, path)
|
||
|
|
||
|
return '.'.join(module_name[::-1])
|
||
|
|
||
|
|
||
|
def locate_app(script_info, module_name, app_name, raise_if_not_found=True):
|
||
|
__traceback_hide__ = True
|
||
|
|
||
|
try:
|
||
|
__import__(module_name)
|
||
|
except ImportError:
|
||
|
# Reraise the ImportError if it occurred within the imported module.
|
||
|
# Determine this by checking whether the trace has a depth > 1.
|
||
|
if sys.exc_info()[-1].tb_next:
|
||
|
raise NoAppException(
|
||
|
'While importing "{name}", an ImportError was raised:'
|
||
|
'\n\n{tb}'.format(name=module_name, tb=traceback.format_exc())
|
||
|
)
|
||
|
elif raise_if_not_found:
|
||
|
raise NoAppException(
|
||
|
'Could not import "{name}".'.format(name=module_name)
|
||
|
)
|
||
|
else:
|
||
|
return
|
||
|
|
||
|
module = sys.modules[module_name]
|
||
|
|
||
|
if app_name is None:
|
||
|
return find_best_app(script_info, module)
|
||
|
else:
|
||
|
return find_app_by_string(script_info, module, app_name)
|
||
|
|
||
|
|
||
|
def get_version(ctx, param, value):
|
||
|
if not value or ctx.resilient_parsing:
|
||
|
return
|
||
|
message = 'Flask %(version)s\nPython %(python_version)s'
|
||
|
click.echo(message % {
|
||
|
'version': __version__,
|
||
|
'python_version': sys.version,
|
||
|
}, color=ctx.color)
|
||
|
ctx.exit()
|
||
|
|
||
|
|
||
|
version_option = click.Option(
|
||
|
['--version'],
|
||
|
help='Show the flask version',
|
||
|
expose_value=False,
|
||
|
callback=get_version,
|
||
|
is_flag=True,
|
||
|
is_eager=True
|
||
|
)
|
||
|
|
||
|
|
||
|
class DispatchingApp(object):
|
||
|
"""Special application that dispatches to a Flask application which
|
||
|
is imported by name in a background thread. If an error happens
|
||
|
it is recorded and shown as part of the WSGI handling which in case
|
||
|
of the Werkzeug debugger means that it shows up in the browser.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, loader, use_eager_loading=False):
|
||
|
self.loader = loader
|
||
|
self._app = None
|
||
|
self._lock = Lock()
|
||
|
self._bg_loading_exc_info = None
|
||
|
if use_eager_loading:
|
||
|
self._load_unlocked()
|
||
|
else:
|
||
|
self._load_in_background()
|
||
|
|
||
|
def _load_in_background(self):
|
||
|
def _load_app():
|
||
|
__traceback_hide__ = True
|
||
|
with self._lock:
|
||
|
try:
|
||
|
self._load_unlocked()
|
||
|
except Exception:
|
||
|
self._bg_loading_exc_info = sys.exc_info()
|
||
|
t = Thread(target=_load_app, args=())
|
||
|
t.start()
|
||
|
|
||
|
def _flush_bg_loading_exception(self):
|
||
|
__traceback_hide__ = True
|
||
|
exc_info = self._bg_loading_exc_info
|
||
|
if exc_info is not None:
|
||
|
self._bg_loading_exc_info = None
|
||
|
reraise(*exc_info)
|
||
|
|
||
|
def _load_unlocked(self):
|
||
|
__traceback_hide__ = True
|
||
|
self._app = rv = self.loader()
|
||
|
self._bg_loading_exc_info = None
|
||
|
return rv
|
||
|
|
||
|
def __call__(self, environ, start_response):
|
||
|
__traceback_hide__ = True
|
||
|
if self._app is not None:
|
||
|
return self._app(environ, start_response)
|
||
|
self._flush_bg_loading_exception()
|
||
|
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. In future versions of Flask this object will most likely play
|
||
|
a bigger role. Typically it's created automatically by the
|
||
|
:class:`FlaskGroup` but you can also manually create it and pass it
|
||
|
onwards as click object.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, app_import_path=None, create_app=None):
|
||
|
#: Optionally the import path for the Flask application.
|
||
|
self.app_import_path = app_import_path or os.environ.get('FLASK_APP')
|
||
|
#: Optionally a function that is passed the script info to create
|
||
|
#: the instance of the application.
|
||
|
self.create_app = create_app
|
||
|
#: A dictionary with arbitrary data that can be associated with
|
||
|
#: this script info.
|
||
|
self.data = {}
|
||
|
self._loaded_app = None
|
||
|
|
||
|
def load_app(self):
|
||
|
"""Loads the Flask app (if not yet loaded) and returns it. Calling
|
||
|
this multiple times will just result in the already loaded app to
|
||
|
be returned.
|
||
|
"""
|
||
|
__traceback_hide__ = True
|
||
|
|
||
|
if self._loaded_app is not None:
|
||
|
return self._loaded_app
|
||
|
|
||
|
app = None
|
||
|
|
||
|
if self.create_app is not None:
|
||
|
app = call_factory(self, self.create_app)
|
||
|
else:
|
||
|
if self.app_import_path:
|
||
|
path, name = (self.app_import_path.split(':', 1) + [None])[:2]
|
||
|
import_name = prepare_import(path)
|
||
|
app = locate_app(self, import_name, name)
|
||
|
else:
|
||
|
for path in ('wsgi.py', 'app.py'):
|
||
|
import_name = prepare_import(path)
|
||
|
app = locate_app(self, import_name, None,
|
||
|
raise_if_not_found=False)
|
||
|
|
||
|
if app:
|
||
|
break
|
||
|
|
||
|
if not app:
|
||
|
raise NoAppException(
|
||
|
'Could not locate a Flask application. You did not provide '
|
||
|
'the "FLASK_APP" environment variable, and a "wsgi.py" or '
|
||
|
'"app.py" module was not found in the current directory.'
|
||
|
)
|
||
|
|
||
|
debug = get_debug_flag()
|
||
|
|
||
|
# Update the app's debug flag through the descriptor so that other
|
||
|
# values repopulate as well.
|
||
|
if debug is not None:
|
||
|
app.debug = debug
|
||
|
|
||
|
self._loaded_app = app
|
||
|
return app
|
||
|
|
||
|
|
||
|
pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True)
|
||
|
|
||
|
|
||
|
def with_appcontext(f):
|
||
|
"""Wraps a callback so that it's guaranteed to be executed with the
|
||
|
script's application context. If callbacks are registered directly
|
||
|
to the ``app.cli`` object then they are wrapped with this function
|
||
|
by default unless it's disabled.
|
||
|
"""
|
||
|
@click.pass_context
|
||
|
def decorator(__ctx, *args, **kwargs):
|
||
|
with __ctx.ensure_object(ScriptInfo).load_app().app_context():
|
||
|
return __ctx.invoke(f, *args, **kwargs)
|
||
|
return update_wrapper(decorator, f)
|
||
|
|
||
|
|
||
|
class AppGroup(click.Group):
|
||
|
"""This works similar to a regular click :class:`~click.Group` but it
|
||
|
changes the behavior of the :meth:`command` decorator so that it
|
||
|
automatically wraps the functions in :func:`with_appcontext`.
|
||
|
|
||
|
Not to be confused with :class:`FlaskGroup`.
|
||
|
"""
|
||
|
|
||
|
def command(self, *args, **kwargs):
|
||
|
"""This works exactly like the method of the same name on a regular
|
||
|
:class:`click.Group` but it wraps callbacks in :func:`with_appcontext`
|
||
|
unless it's disabled by passing ``with_appcontext=False``.
|
||
|
"""
|
||
|
wrap_for_ctx = kwargs.pop('with_appcontext', True)
|
||
|
def decorator(f):
|
||
|
if wrap_for_ctx:
|
||
|
f = with_appcontext(f)
|
||
|
return click.Group.command(self, *args, **kwargs)(f)
|
||
|
return decorator
|
||
|
|
||
|
def group(self, *args, **kwargs):
|
||
|
"""This works exactly like the method of the same name on a regular
|
||
|
:class:`click.Group` but it defaults the group class to
|
||
|
:class:`AppGroup`.
|
||
|
"""
|
||
|
kwargs.setdefault('cls', AppGroup)
|
||
|
return click.Group.group(self, *args, **kwargs)
|
||
|
|
||
|
|
||
|
class FlaskGroup(AppGroup):
|
||
|
"""Special subclass of the :class:`AppGroup` 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 use cases for which it makes sense to create an
|
||
|
instance of this.
|
||
|
|
||
|
For information as of why this is useful see :ref:`custom-scripts`.
|
||
|
|
||
|
:param add_default_commands: if this is True then the default run and
|
||
|
shell commands wil be added.
|
||
|
:param add_version_option: adds the ``--version`` option.
|
||
|
:param create_app: an optional callback that is passed the script info and
|
||
|
returns the loaded app.
|
||
|
:param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
|
||
|
files to set environment variables. Will also change the working
|
||
|
directory to the directory containing the first file found.
|
||
|
|
||
|
.. versionchanged:: 1.0
|
||
|
If installed, python-dotenv will be used to load environment variables
|
||
|
from :file:`.env` and :file:`.flaskenv` files.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, add_default_commands=True, create_app=None,
|
||
|
add_version_option=True, load_dotenv=True, **extra):
|
||
|
params = list(extra.pop('params', None) or ())
|
||
|
|
||
|
if add_version_option:
|
||
|
params.append(version_option)
|
||
|
|
||
|
AppGroup.__init__(self, params=params, **extra)
|
||
|
self.create_app = create_app
|
||
|
self.load_dotenv = load_dotenv
|
||
|
|
||
|
if add_default_commands:
|
||
|
self.add_command(run_command)
|
||
|
self.add_command(shell_command)
|
||
|
self.add_command(routes_command)
|
||
|
|
||
|
self._loaded_plugin_commands = False
|
||
|
|
||
|
def _load_plugin_commands(self):
|
||
|
if self._loaded_plugin_commands:
|
||
|
return
|
||
|
try:
|
||
|
import pkg_resources
|
||
|
except ImportError:
|
||
|
self._loaded_plugin_commands = True
|
||
|
return
|
||
|
|
||
|
for ep in pkg_resources.iter_entry_points('flask.commands'):
|
||
|
self.add_command(ep.load(), ep.name)
|
||
|
self._loaded_plugin_commands = True
|
||
|
|
||
|
def get_command(self, ctx, name):
|
||
|
self._load_plugin_commands()
|
||
|
|
||
|
# We load built-in commands first as these should always be the
|
||
|
# same no matter what the app does. If the app does want to
|
||
|
# override this it needs to make a custom instance of this group
|
||
|
# and not attach the default commands.
|
||
|
#
|
||
|
# This also means that the script stays functional in case the
|
||
|
# application completely fails.
|
||
|
rv = AppGroup.get_command(self, ctx, name)
|
||
|
if rv is not None:
|
||
|
return rv
|
||
|
|
||
|
info = ctx.ensure_object(ScriptInfo)
|
||
|
try:
|
||
|
rv = info.load_app().cli.get_command(ctx, name)
|
||
|
if rv is not None:
|
||
|
return rv
|
||
|
except NoAppException:
|
||
|
pass
|
||
|
|
||
|
def list_commands(self, ctx):
|
||
|
self._load_plugin_commands()
|
||
|
|
||
|
# 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.ensure_object(ScriptInfo)
|
||
|
try:
|
||
|
rv.update(info.load_app().cli.list_commands(ctx))
|
||
|
except Exception:
|
||
|
# Here we intentionally swallow all exceptions as we don't
|
||
|
# want the help page to break if the app does not exist.
|
||
|
# If someone attempts to use the command we try to create
|
||
|
# the app again and this will give us the error.
|
||
|
# However, we will not do so silently because that would confuse
|
||
|
# users.
|
||
|
traceback.print_exc()
|
||
|
return sorted(rv)
|
||
|
|
||
|
def main(self, *args, **kwargs):
|
||
|
# Set a global flag that indicates that we were invoked from the
|
||
|
# command line interface. This is detected by Flask.run to make the
|
||
|
# call into a no-op. This is necessary to avoid ugly errors when the
|
||
|
# script that is loaded here also attempts to start a server.
|
||
|
os.environ['FLASK_RUN_FROM_CLI'] = 'true'
|
||
|
|
||
|
if get_load_dotenv(self.load_dotenv):
|
||
|
load_dotenv()
|
||
|
|
||
|
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 super(FlaskGroup, self).main(*args, **kwargs)
|
||
|
|
||
|
|
||
|
def _path_is_ancestor(path, other):
|
||
|
"""Take ``other`` and remove the length of ``path`` from it. Then join it
|
||
|
to ``path``. If it is the original value, ``path`` is an ancestor of
|
||
|
``other``."""
|
||
|
return os.path.join(path, other[len(path):].lstrip(os.sep)) == other
|
||
|
|
||
|
|
||
|
def load_dotenv(path=None):
|
||
|
"""Load "dotenv" files in order of precedence to set environment variables.
|
||
|
|
||
|
If an env var is already set it is not overwritten, so earlier files in the
|
||
|
list are preferred over later files.
|
||
|
|
||
|
Changes the current working directory to the location of the first file
|
||
|
found, with the assumption that it is in the top level project directory
|
||
|
and will be where the Python path should import local packages from.
|
||
|
|
||
|
This is a no-op if `python-dotenv`_ is not installed.
|
||
|
|
||
|
.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
|
||
|
|
||
|
:param path: Load the file at this location instead of searching.
|
||
|
:return: ``True`` if a file was loaded.
|
||
|
|
||
|
.. versionadded:: 1.0
|
||
|
"""
|
||
|
if dotenv is None:
|
||
|
if path or os.path.exists('.env') or os.path.exists('.flaskenv'):
|
||
|
click.secho(
|
||
|
' * Tip: There are .env files present.'
|
||
|
' Do "pip install python-dotenv" to use them.',
|
||
|
fg='yellow')
|
||
|
return
|
||
|
|
||
|
if path is not None:
|
||
|
return dotenv.load_dotenv(path)
|
||
|
|
||
|
new_dir = None
|
||
|
|
||
|
for name in ('.env', '.flaskenv'):
|
||
|
path = dotenv.find_dotenv(name, usecwd=True)
|
||
|
|
||
|
if not path:
|
||
|
continue
|
||
|
|
||
|
if new_dir is None:
|
||
|
new_dir = os.path.dirname(path)
|
||
|
|
||
|
dotenv.load_dotenv(path)
|
||
|
|
||
|
if new_dir and os.getcwd() != new_dir:
|
||
|
os.chdir(new_dir)
|
||
|
|
||
|
return new_dir is not None # at least one file was located and loaded
|
||
|
|
||
|
|
||
|
def show_server_banner(env, debug, app_import_path, eager_loading):
|
||
|
"""Show extra startup messages the first time the server is run,
|
||
|
ignoring the reloader.
|
||
|
"""
|
||
|
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||
|
return
|
||
|
|
||
|
if app_import_path is not None:
|
||
|
message = ' * Serving Flask app "{0}"'.format(app_import_path)
|
||
|
|
||
|
if not eager_loading:
|
||
|
message += ' (lazy loading)'
|
||
|
|
||
|
click.echo(message)
|
||
|
|
||
|
click.echo(' * Environment: {0}'.format(env))
|
||
|
|
||
|
if env == 'production':
|
||
|
click.secho(
|
||
|
' WARNING: Do not use the development server in a production'
|
||
|
' environment.', fg='red')
|
||
|
click.secho(' Use a production WSGI server instead.', dim=True)
|
||
|
|
||
|
if debug is not None:
|
||
|
click.echo(' * Debug mode: {0}'.format('on' if debug else 'off'))
|
||
|
|
||
|
|
||
|
class CertParamType(click.ParamType):
|
||
|
"""Click option type for the ``--cert`` option. Allows either an
|
||
|
existing file, the string ``'adhoc'``, or an import for a
|
||
|
:class:`~ssl.SSLContext` object.
|
||
|
"""
|
||
|
|
||
|
name = 'path'
|
||
|
|
||
|
def __init__(self):
|
||
|
self.path_type = click.Path(
|
||
|
exists=True, dir_okay=False, resolve_path=True)
|
||
|
|
||
|
def convert(self, value, param, ctx):
|
||
|
try:
|
||
|
return self.path_type(value, param, ctx)
|
||
|
except click.BadParameter:
|
||
|
value = click.STRING(value, param, ctx).lower()
|
||
|
|
||
|
if value == 'adhoc':
|
||
|
try:
|
||
|
import OpenSSL
|
||
|
except ImportError:
|
||
|
raise click.BadParameter(
|
||
|
'Using ad-hoc certificates requires pyOpenSSL.',
|
||
|
ctx, param)
|
||
|
|
||
|
return value
|
||
|
|
||
|
obj = import_string(value, silent=True)
|
||
|
|
||
|
if sys.version_info < (2, 7):
|
||
|
if obj:
|
||
|
return obj
|
||
|
else:
|
||
|
if isinstance(obj, ssl.SSLContext):
|
||
|
return obj
|
||
|
|
||
|
raise
|
||
|
|
||
|
|
||
|
def _validate_key(ctx, param, value):
|
||
|
"""The ``--key`` option must be specified when ``--cert`` is a file.
|
||
|
Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed.
|
||
|
"""
|
||
|
cert = ctx.params.get('cert')
|
||
|
is_adhoc = cert == 'adhoc'
|
||
|
|
||
|
if sys.version_info < (2, 7):
|
||
|
is_context = cert and not isinstance(cert, (text_type, bytes))
|
||
|
else:
|
||
|
is_context = isinstance(cert, ssl.SSLContext)
|
||
|
|
||
|
if value is not None:
|
||
|
if is_adhoc:
|
||
|
raise click.BadParameter(
|
||
|
'When "--cert" is "adhoc", "--key" is not used.',
|
||
|
ctx, param)
|
||
|
|
||
|
if is_context:
|
||
|
raise click.BadParameter(
|
||
|
'When "--cert" is an SSLContext object, "--key is not used.',
|
||
|
ctx, param)
|
||
|
|
||
|
if not cert:
|
||
|
raise click.BadParameter(
|
||
|
'"--cert" must also be specified.',
|
||
|
ctx, param)
|
||
|
|
||
|
ctx.params['cert'] = cert, value
|
||
|
|
||
|
else:
|
||
|
if cert and not (is_adhoc or is_context):
|
||
|
raise click.BadParameter(
|
||
|
'Required when using "--cert".',
|
||
|
ctx, param)
|
||
|
|
||
|
return value
|
||
|
|
||
|
|
||
|
@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,
|
||
|
help='The port to bind to.')
|
||
|
@click.option('--cert', type=CertParamType(),
|
||
|
help='Specify a certificate file to use HTTPS.')
|
||
|
@click.option('--key',
|
||
|
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
||
|
callback=_validate_key, expose_value=False,
|
||
|
help='The key file to use when specifying a certificate.')
|
||
|
@click.option('--reload/--no-reload', default=None,
|
||
|
help='Enable or disable the reloader. By default the reloader '
|
||
|
'is active if 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=True,
|
||
|
help='Enable or disable multithreading.')
|
||
|
@pass_script_info
|
||
|
def run_command(info, host, port, reload, debugger, eager_loading,
|
||
|
with_threads, cert):
|
||
|
"""Run a local development server.
|
||
|
|
||
|
This server is for development purposes only. It does not provide
|
||
|
the stability, security, or performance of production WSGI servers.
|
||
|
|
||
|
The reloader and debugger are enabled by default if
|
||
|
FLASK_ENV=development or FLASK_DEBUG=1.
|
||
|
"""
|
||
|
debug = get_debug_flag()
|
||
|
|
||
|
if reload is None:
|
||
|
reload = debug
|
||
|
|
||
|
if debugger is None:
|
||
|
debugger = debug
|
||
|
|
||
|
if eager_loading is None:
|
||
|
eager_loading = not reload
|
||
|
|
||
|
show_server_banner(get_env(), debug, info.app_import_path, eager_loading)
|
||
|
app = DispatchingApp(info.load_app, use_eager_loading=eager_loading)
|
||
|
|
||
|
from werkzeug.serving import run_simple
|
||
|
run_simple(host, port, app, use_reloader=reload, use_debugger=debugger,
|
||
|
threaded=with_threads, ssl_context=cert)
|
||
|
|
||
|
|
||
|
@click.command('shell', short_help='Runs a shell in the app context.')
|
||
|
@with_appcontext
|
||
|
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 configure 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.env,
|
||
|
app.instance_path,
|
||
|
)
|
||
|
ctx = {}
|
||
|
|
||
|
# Support the regular Python interpreter startup script if someone
|
||
|
# is using it.
|
||
|
startup = os.environ.get('PYTHONSTARTUP')
|
||
|
if startup and os.path.isfile(startup):
|
||
|
with open(startup, 'r') as f:
|
||
|
eval(compile(f.read(), startup, 'exec'), ctx)
|
||
|
|
||
|
ctx.update(app.make_shell_context())
|
||
|
|
||
|
code.interact(banner=banner, local=ctx)
|
||
|
|
||
|
|
||
|
@click.command('routes', short_help='Show the routes for the app.')
|
||
|
@click.option(
|
||
|
'--sort', '-s',
|
||
|
type=click.Choice(('endpoint', 'methods', 'rule', 'match')),
|
||
|
default='endpoint',
|
||
|
help=(
|
||
|
'Method to sort routes by. "match" is the order that Flask will match '
|
||
|
'routes when dispatching a request.'
|
||
|
)
|
||
|
)
|
||
|
@click.option(
|
||
|
'--all-methods',
|
||
|
is_flag=True,
|
||
|
help="Show HEAD and OPTIONS methods."
|
||
|
)
|
||
|
@with_appcontext
|
||
|
def routes_command(sort, all_methods):
|
||
|
"""Show all registered routes with endpoints and methods."""
|
||
|
|
||
|
rules = list(current_app.url_map.iter_rules())
|
||
|
if not rules:
|
||
|
click.echo('No routes were registered.')
|
||
|
return
|
||
|
|
||
|
ignored_methods = set(() if all_methods else ('HEAD', 'OPTIONS'))
|
||
|
|
||
|
if sort in ('endpoint', 'rule'):
|
||
|
rules = sorted(rules, key=attrgetter(sort))
|
||
|
elif sort == 'methods':
|
||
|
rules = sorted(rules, key=lambda rule: sorted(rule.methods))
|
||
|
|
||
|
rule_methods = [
|
||
|
', '.join(sorted(rule.methods - ignored_methods)) for rule in rules
|
||
|
]
|
||
|
|
||
|
headers = ('Endpoint', 'Methods', 'Rule')
|
||
|
widths = (
|
||
|
max(len(rule.endpoint) for rule in rules),
|
||
|
max(len(methods) for methods in rule_methods),
|
||
|
max(len(rule.rule) for rule in rules),
|
||
|
)
|
||
|
widths = [max(len(h), w) for h, w in zip(headers, widths)]
|
||
|
row = '{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}'.format(*widths)
|
||
|
|
||
|
click.echo(row.format(*headers).strip())
|
||
|
click.echo(row.format(*('-' * width for width in widths)))
|
||
|
|
||
|
for rule, methods in zip(rules, rule_methods):
|
||
|
click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip())
|
||
|
|
||
|
|
||
|
cli = FlaskGroup(help="""\
|
||
|
A general utility script for Flask applications.
|
||
|
|
||
|
Provides commands from Flask, extensions, and the application. Loads the
|
||
|
application defined in the FLASK_APP environment variable, or from a wsgi.py
|
||
|
file. Setting the FLASK_ENV environment variable to 'development' will enable
|
||
|
debug mode.
|
||
|
|
||
|
\b
|
||
|
{prefix}{cmd} FLASK_APP=hello.py
|
||
|
{prefix}{cmd} FLASK_ENV=development
|
||
|
{prefix}flask run
|
||
|
""".format(
|
||
|
cmd='export' if os.name == 'posix' else 'set',
|
||
|
prefix='$ ' if os.name == 'posix' else '> '
|
||
|
))
|
||
|
|
||
|
|
||
|
def main(as_module=False):
|
||
|
args = sys.argv[1:]
|
||
|
|
||
|
if as_module:
|
||
|
this_module = 'flask'
|
||
|
|
||
|
if sys.version_info < (2, 7):
|
||
|
this_module += '.cli'
|
||
|
|
||
|
name = 'python -m ' + this_module
|
||
|
|
||
|
# Python rewrites "python -m flask" to the path to the file in argv.
|
||
|
# Restore the original command so that the reloader works.
|
||
|
sys.argv = ['-m', this_module] + args
|
||
|
else:
|
||
|
name = None
|
||
|
|
||
|
cli.main(args=args, prog_name=name)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
main(as_module=True)
|