Merge pull request #3711 from pallets/cli-loading-error
cleaner message when CLI can't load app
This commit is contained in:
commit
6638432457
4 changed files with 59 additions and 38 deletions
|
|
@ -12,6 +12,8 @@ Unreleased
|
||||||
- Passing ``script_info`` to app factory functions is deprecated. This
|
- Passing ``script_info`` to app factory functions is deprecated. This
|
||||||
was not portable outside the ``flask`` command. Use
|
was not portable outside the ``flask`` command. Use
|
||||||
``click.get_current_context().obj`` if it's needed. :issue:`3552`
|
``click.get_current_context().obj`` if it's needed. :issue:`3552`
|
||||||
|
- The CLI shows better error messages when the app failed to load
|
||||||
|
when looking up commands. :issue:`2741`
|
||||||
- Add :meth:`sessions.SessionInterface.get_cookie_name` to allow
|
- Add :meth:`sessions.SessionInterface.get_cookie_name` to allow
|
||||||
setting the session cookie name dynamically. :pr:`3369`
|
setting the session cookie name dynamically. :pr:`3369`
|
||||||
- Add :meth:`Config.from_file` to load config using arbitrary file
|
- Add :meth:`Config.from_file` to load config using arbitrary file
|
||||||
|
|
|
||||||
|
|
@ -536,43 +536,41 @@ class FlaskGroup(AppGroup):
|
||||||
|
|
||||||
def get_command(self, ctx, name):
|
def get_command(self, ctx, name):
|
||||||
self._load_plugin_commands()
|
self._load_plugin_commands()
|
||||||
|
# Look up built-in and plugin commands, which should be
|
||||||
|
# available even if the app fails to load.
|
||||||
|
rv = super().get_command(ctx, name)
|
||||||
|
|
||||||
# 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:
|
if rv is not None:
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
info = ctx.ensure_object(ScriptInfo)
|
info = ctx.ensure_object(ScriptInfo)
|
||||||
|
|
||||||
|
# Look up commands provided by the app, showing an error and
|
||||||
|
# continuing if the app couldn't be loaded.
|
||||||
try:
|
try:
|
||||||
rv = info.load_app().cli.get_command(ctx, name)
|
return info.load_app().cli.get_command(ctx, name)
|
||||||
if rv is not None:
|
except NoAppException as e:
|
||||||
return rv
|
click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
|
||||||
except NoAppException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def list_commands(self, ctx):
|
def list_commands(self, ctx):
|
||||||
self._load_plugin_commands()
|
self._load_plugin_commands()
|
||||||
|
# Start with the built-in and plugin commands.
|
||||||
# The commands available is the list of both the application (if
|
rv = set(super().list_commands(ctx))
|
||||||
# available) plus the builtin commands.
|
|
||||||
rv = set(click.Group.list_commands(self, ctx))
|
|
||||||
info = ctx.ensure_object(ScriptInfo)
|
info = ctx.ensure_object(ScriptInfo)
|
||||||
|
|
||||||
|
# Add commands provided by the app, showing an error and
|
||||||
|
# continuing if the app couldn't be loaded.
|
||||||
try:
|
try:
|
||||||
rv.update(info.load_app().cli.list_commands(ctx))
|
rv.update(info.load_app().cli.list_commands(ctx))
|
||||||
|
except NoAppException as e:
|
||||||
|
# When an app couldn't be loaded, show the error message
|
||||||
|
# without the traceback.
|
||||||
|
click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
|
||||||
except Exception:
|
except Exception:
|
||||||
# Here we intentionally swallow all exceptions as we don't
|
# When any other errors occurred during loading, show the
|
||||||
# want the help page to break if the app does not exist.
|
# full traceback.
|
||||||
# If someone attempts to use the command we try to create
|
click.secho(f"{traceback.format_exc()}\n", err=True, fg="red")
|
||||||
# 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)
|
return sorted(rv)
|
||||||
|
|
||||||
def main(self, *args, **kwargs):
|
def main(self, *args, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,15 @@ def client(app):
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_apps(monkeypatch):
|
def test_apps(monkeypatch):
|
||||||
monkeypatch.syspath_prepend(
|
monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "test_apps"))
|
||||||
os.path.abspath(os.path.join(os.path.dirname(__file__), "test_apps"))
|
original_modules = set(sys.modules.keys())
|
||||||
)
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Remove any imports cached during the test. Otherwise "import app"
|
||||||
|
# will work in the next test even though it's no longer on the path.
|
||||||
|
for key in sys.modules.keys() - original_modules:
|
||||||
|
sys.modules.pop(key)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ def test_locate_app_raises(test_apps, iname, aname):
|
||||||
locate_app(info, iname, aname)
|
locate_app(info, iname, aname)
|
||||||
|
|
||||||
|
|
||||||
def test_locate_app_suppress_raise():
|
def test_locate_app_suppress_raise(test_apps):
|
||||||
info = ScriptInfo()
|
info = ScriptInfo()
|
||||||
app = locate_app(info, "notanapp.py", None, raise_if_not_found=False)
|
app = locate_app(info, "notanapp.py", None, raise_if_not_found=False)
|
||||||
assert app is None
|
assert app is None
|
||||||
|
|
@ -396,21 +396,36 @@ def test_flaskgroup_debug(runner, set_debug_flag):
|
||||||
assert result.output == f"{not set_debug_flag}\n"
|
assert result.output == f"{not set_debug_flag}\n"
|
||||||
|
|
||||||
|
|
||||||
def test_print_exceptions(runner):
|
def test_no_command_echo_loading_error():
|
||||||
"""Print the stacktrace if the CLI."""
|
from flask.cli import cli
|
||||||
|
|
||||||
def create_app():
|
runner = CliRunner(mix_stderr=False)
|
||||||
raise Exception("oh no")
|
result = runner.invoke(cli, ["missing"])
|
||||||
return Flask("flaskgroup")
|
assert result.exit_code == 2
|
||||||
|
assert "FLASK_APP" in result.stderr
|
||||||
|
assert "Usage:" in result.stderr
|
||||||
|
|
||||||
@click.group(cls=FlaskGroup, create_app=create_app)
|
|
||||||
def cli(**params):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
def test_help_echo_loading_error():
|
||||||
|
from flask.cli import cli
|
||||||
|
|
||||||
|
runner = CliRunner(mix_stderr=False)
|
||||||
result = runner.invoke(cli, ["--help"])
|
result = runner.invoke(cli, ["--help"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Exception: oh no" in result.output
|
assert "FLASK_APP" in result.stderr
|
||||||
assert "Traceback" in result.output
|
assert "Usage:" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_help_echo_exception():
|
||||||
|
def create_app():
|
||||||
|
raise Exception("oh no")
|
||||||
|
|
||||||
|
cli = FlaskGroup(create_app=create_app)
|
||||||
|
runner = CliRunner(mix_stderr=False)
|
||||||
|
result = runner.invoke(cli, ["--help"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Exception: oh no" in result.stderr
|
||||||
|
assert "Usage:" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
class TestRoutes:
|
class TestRoutes:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue