Skip to content

Commit 5d8e356

Browse files
committed
refactor lazy loading
Remove the `--eager-loading/--lazy-loading` options and the `DispatchingApp` middleware. The `run` command handles loading exceptions directly. The reloader always prints out tracebacks immediately and always defers raising the error.
1 parent 095651b commit 5d8e356

File tree

6 files changed

+31
-118
lines changed

6 files changed

+31
-118
lines changed

CHANGES.rst

+6
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ Unreleased
9292
JSON response like a dict is. :issue:`4672`
9393
- When type checking, allow ``TypedDict`` to be returned from view
9494
functions. :pr:`4695`
95+
- Remove the ``--eager-loading/--lazy-loading`` options from the
96+
``flask run`` command. The app is always eager loaded the first
97+
time, then lazily loaded in the reloader. The reloader always prints
98+
errors immediately but continues serving. Remove the internal
99+
``DispatchingApp`` middleware used by the previous implementation.
100+
:issue:`4715`
95101

96102

97103
Version 2.1.3

docs/quickstart.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ is with the ``-app`` option.
4646
.. code-block:: text
4747
4848
$ flask --app hello run
49-
* Serving Flask app 'hello' (lazy loading)
49+
* Serving Flask app 'hello'
5050
* Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
5151
5252
.. admonition:: Application Discovery Behavior
@@ -110,7 +110,7 @@ To enable all development features, set the ``--env`` option to
110110
.. code-block:: text
111111
112112
$ flask --app hello --env development run
113-
* Serving Flask app 'hello' (lazy loading)
113+
* Serving Flask app 'hello'
114114
* Environment: development
115115
* Debug mode: on
116116
* Running on http://127.0.0.1:5000 (Press CTRL+C to quit)

docs/server.rst

+2-7
Original file line numberDiff line numberDiff line change
@@ -127,24 +127,19 @@ macOS Monterey and later automatically starts a service that uses port
127127
disable "AirPlay Receiver".
128128

129129

130-
Lazy or Eager Loading
131-
~~~~~~~~~~~~~~~~~~~~~
130+
Deferred Errors on Reload
131+
~~~~~~~~~~~~~~~~~~~~~~~~~
132132

133133
When using the ``flask run`` command with the reloader, the server will
134134
continue to run even if you introduce syntax errors or other
135135
initialization errors into the code. Accessing the site will show the
136136
interactive debugger for the error, rather than crashing the server.
137-
This feature is called "lazy loading".
138137

139138
If a syntax error is already present when calling ``flask run``, it will
140139
fail immediately and show the traceback rather than waiting until the
141140
site is accessed. This is intended to make errors more visible initially
142141
while still allowing the server to handle errors on reload.
143142

144-
To override this behavior and always fail immediately, even on reload,
145-
pass the ``--eager-loading`` option. To always keep the server running,
146-
even on the initial call, pass ``--lazy-loading``.
147-
148143

149144
In Code
150145
-------

src/flask/app.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,7 @@ def run(
998998
options.setdefault("use_debugger", self.debug)
999999
options.setdefault("threaded", True)
10001000

1001-
cli.show_server_banner(self.env, self.debug, self.name, False)
1001+
cli.show_server_banner(self.env, self.debug, self.name)
10021002

10031003
from werkzeug.serving import run_simple
10041004

src/flask/cli.py

+20-86
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import typing as t
1111
from functools import update_wrapper
1212
from operator import attrgetter
13-
from threading import Lock
14-
from threading import Thread
1513

1614
import click
1715
from click.core import ParameterSource
@@ -267,74 +265,6 @@ def get_version(ctx, param, value):
267265
)
268266

269267

270-
class DispatchingApp:
271-
"""Special application that dispatches to a Flask application which
272-
is imported by name in a background thread. If an error happens
273-
it is recorded and shown as part of the WSGI handling which in case
274-
of the Werkzeug debugger means that it shows up in the browser.
275-
"""
276-
277-
def __init__(self, loader, use_eager_loading=None):
278-
self.loader = loader
279-
self._app = None
280-
self._lock = Lock()
281-
self._bg_loading_exc = None
282-
283-
if use_eager_loading is None:
284-
use_eager_loading = not is_running_from_reloader()
285-
286-
if use_eager_loading:
287-
self._load_unlocked()
288-
else:
289-
self._load_in_background()
290-
291-
def _load_in_background(self):
292-
# Store the Click context and push it in the loader thread so
293-
# script_info is still available.
294-
ctx = click.get_current_context(silent=True)
295-
296-
def _load_app():
297-
__traceback_hide__ = True # noqa: F841
298-
299-
with self._lock:
300-
if ctx is not None:
301-
click.globals.push_context(ctx)
302-
303-
try:
304-
self._load_unlocked()
305-
except Exception as e:
306-
self._bg_loading_exc = e
307-
308-
t = Thread(target=_load_app, args=())
309-
t.start()
310-
311-
def _flush_bg_loading_exception(self):
312-
__traceback_hide__ = True # noqa: F841
313-
exc = self._bg_loading_exc
314-
315-
if exc is not None:
316-
self._bg_loading_exc = None
317-
raise exc
318-
319-
def _load_unlocked(self):
320-
__traceback_hide__ = True # noqa: F841
321-
self._app = rv = self.loader()
322-
self._bg_loading_exc = None
323-
return rv
324-
325-
def __call__(self, environ, start_response):
326-
__traceback_hide__ = True # noqa: F841
327-
if self._app is not None:
328-
return self._app(environ, start_response)
329-
self._flush_bg_loading_exception()
330-
with self._lock:
331-
if self._app is not None:
332-
rv = self._app
333-
else:
334-
rv = self._load_unlocked()
335-
return rv(environ, start_response)
336-
337-
338268
class ScriptInfo:
339269
"""Helper object to deal with Flask applications. This is usually not
340270
necessary to interface with as it's used internally in the dispatching
@@ -811,20 +741,15 @@ def load_dotenv(path: str | os.PathLike | None = None) -> bool:
811741
return loaded # True if at least one file was located and loaded.
812742

813743

814-
def show_server_banner(env, debug, app_import_path, eager_loading):
744+
def show_server_banner(env, debug, app_import_path):
815745
"""Show extra startup messages the first time the server is run,
816746
ignoring the reloader.
817747
"""
818748
if is_running_from_reloader():
819749
return
820750

821751
if app_import_path is not None:
822-
message = f" * Serving Flask app {app_import_path!r}"
823-
824-
if not eager_loading:
825-
message += " (lazy loading)"
826-
827-
click.echo(message)
752+
click.echo(f" * Serving Flask app '{app_import_path}'")
828753

829754
click.echo(f" * Environment: {env}")
830755

@@ -963,12 +888,6 @@ def convert(self, value, param, ctx):
963888
help="Enable or disable the debugger. By default the debugger "
964889
"is active if debug is enabled.",
965890
)
966-
@click.option(
967-
"--eager-loading/--lazy-loading",
968-
default=None,
969-
help="Enable or disable eager loading. By default eager "
970-
"loading is enabled if the reloader is disabled.",
971-
)
972891
@click.option(
973892
"--with-threads/--without-threads",
974893
default=True,
@@ -1000,7 +919,6 @@ def run_command(
1000919
port,
1001920
reload,
1002921
debugger,
1003-
eager_loading,
1004922
with_threads,
1005923
cert,
1006924
extra_files,
@@ -1014,7 +932,23 @@ def run_command(
1014932
The reloader and debugger are enabled by default with the
1015933
'--env development' or '--debug' options.
1016934
"""
1017-
app = DispatchingApp(info.load_app, use_eager_loading=eager_loading)
935+
try:
936+
app = info.load_app()
937+
except Exception as e:
938+
if is_running_from_reloader():
939+
# When reloading, print out the error immediately, but raise
940+
# it later so the debugger or server can handle it.
941+
traceback.print_exc()
942+
err = e
943+
944+
def app(environ, start_response):
945+
raise err from None
946+
947+
else:
948+
# When not reloading, raise the error immediately so the
949+
# command fails.
950+
raise e from None
951+
1018952
debug = get_debug_flag()
1019953

1020954
if reload is None:
@@ -1023,7 +957,7 @@ def run_command(
1023957
if debugger is None:
1024958
debugger = debug
1025959

1026-
show_server_banner(get_env(), debug, info.app_import_path, eager_loading)
960+
show_server_banner(get_env(), debug, info.app_import_path)
1027961

1028962
from werkzeug.serving import run_simple
1029963

tests/test_cli.py

-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# This file was part of Flask-CLI and was modified under the terms of
22
# its Revised BSD License. Copyright © 2015 CERN.
33
import os
4-
import platform
54
import ssl
65
import sys
76
import types
@@ -17,7 +16,6 @@
1716
from flask import current_app
1817
from flask import Flask
1918
from flask.cli import AppGroup
20-
from flask.cli import DispatchingApp
2119
from flask.cli import find_best_app
2220
from flask.cli import FlaskGroup
2321
from flask.cli import get_version
@@ -290,26 +288,6 @@ def create_app():
290288
assert app.name == "testapp"
291289

292290

293-
@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="flaky on pypy")
294-
def test_lazy_load_error(monkeypatch):
295-
"""When using lazy loading, the correct exception should be
296-
re-raised.
297-
"""
298-
299-
class BadExc(Exception):
300-
pass
301-
302-
def bad_load():
303-
raise BadExc
304-
305-
lazy = DispatchingApp(bad_load, use_eager_loading=False)
306-
307-
# reduce flakiness by waiting for the internal loading lock
308-
with lazy._lock:
309-
with pytest.raises(BadExc):
310-
lazy._flush_bg_loading_exception()
311-
312-
313291
def test_app_cli_has_app_context(app, runner):
314292
def _param_cb(ctx, param, value):
315293
# current_app should be available in parameter callbacks

0 commit comments

Comments
 (0)