Repository: pallets/flask (Python)

Issue:
deprecate `before_first_request`
While reviewing the consistent use of `setupmethod` for #4571 and #4577, I'm realizing that `before_first_request` should probably be deprecated then removed.

* There's no example use cases in the docs.
* From examples I've seen on Stack Overflow and around the web, it seems to be (mis)used as a setup method, to do something "before the application starts".
* Configuration should be done before _running_ the application, during the actual setup phase. Preferably, it should be done in a factory function.
* It incurs a function call before _every_ request, for something that only happens once. This can be inlined to only incur an `if` check, but it's not ideal.
* It runs before the first request for every _instance_, which is every or almost every time for many process worker implementations, and only once for the dev server (the only thread worker I'm aware of). The `threading.Lock` is not useful for an eventual ASGI implementation.


Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 3ce9699e0c..d5fa9fe9ad 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,6 +20,8 @@ Unreleased
     registering the blueprint will show a warning. In the next version,
     this will become an error just like the application setup methods.
     :issue:`4571`
+-   ``before_first_request`` is deprecated. Run setup code when creating
+    the application instead. :issue:`4605`
 
 
 Version 2.1.3
@@ -1032,8 +1034,7 @@ Released 2011-09-29, codename Rakija
     earlier feedback when users forget to import view code ahead of
     time.
 -   Added the ability to register callbacks that are only triggered once
-    at the beginning of the first request.
-    (:meth:`Flask.before_first_request`)
+    at the beginning of the first request. (``before_first_request``)
 -   Malformed JSON data will now trigger a bad request HTTP exception
     instead of a value error which usually would result in a 500
     internal server error if not handled. This is a backwards
diff --git a/src/flask/app.py b/src/flask/app.py
index 9333502968..65e95623d6 100644
--- a/src/flask/app.py
+++ b/src/flask/app.py
@@ -451,6 +451,10 @@ def __init__(
         #: first request to this instance. To register a function, use the
         #: :meth:`before_first_request` decorator.
         #:
+        #: .. deprecated:: 2.2
+        #:     Will be removed in Flask 2.3. Run setup code when
+        #:     creating the application instead.
+        #:
         #: .. versionadded:: 0.8
         self.before_first_request_funcs: t.List[ft.BeforeFirstRequestCallable] = []
 
@@ -1255,8 +1259,21 @@ def before_first_request(
         The function will be called without any arguments and its return
         value is ignored.
 
+        .. deprecated:: 2.2
+            Will be removed in Flask 2.3. Run setup code when creating
+            the application instead.
+
         .. versionadded:: 0.8
         """
+        import warnings
+
+        warnings.warn(
+            "'before_first_request' is deprecated and will be removed"
+            " in Flask 2.3. Run setup code while creating the"
+            " application instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         self.before_first_request_funcs.append(f)
         return f
 
@@ -1552,7 +1569,17 @@ def full_dispatch_request(self) -> Response:
 
         .. versionadded:: 0.7
         """
-        self.try_trigger_before_first_request_functions()
+        # Run before_first_request functions if this is the thread's first request.
+        # Inlined to avoid a method call on subsequent requests.
+        # This is deprecated, will be removed in Flask 2.3.
+        if not self._got_first_request:
+            with self._before_request_lock:
+                if not self._got_first_request:
+                    for func in self.before_first_request_funcs:
+                        self.ensure_sync(func)()
+
+                    self._got_first_request = True
+
         try:
             request_started.send(self)
             rv = self.preprocess_request()
@@ -1591,22 +1618,6 @@ def finalize_request(
             )
         return response
 
-    def try_trigger_before_first_request_functions(self) -> None:
-        """Called before each request and will ensure that it triggers
-        the :attr:`before_first_request_funcs` and only exactly once per
-        application instance (which means process usually).
-
-        :internal:
-        """
-        if self._got_first_request:
-            return
-        with self._before_request_lock:
-            if self._got_first_request:
-                return
-            for func in self.before_first_request_funcs:
-                self.ensure_sync(func)()
-            self._got_first_request = True
-
     def make_default_options_response(self) -> Response:
         """This method is called to create the default ``OPTIONS`` response.
         This can be changed through subclassing to change the default
diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py
index 7ed599a043..76b3606746 100644
--- a/src/flask/blueprints.py
+++ b/src/flask/blueprints.py
@@ -543,7 +543,20 @@ def before_app_first_request(
     ) -> ft.BeforeFirstRequestCallable:
         """Like :meth:`Flask.before_first_request`.  Such a function is
         executed before the first request to the application.
+
+        .. deprecated:: 2.2
+            Will be removed in Flask 2.3. Run setup code when creating
+            the application instead.
         """
+        import warnings
+
+        warnings.warn(
+            "'before_app_first_request' is deprecated and will be"
+            " removed in Flask 2.3. Use 'record_once' instead to run"
+            " setup code when registering the blueprint.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
         return f
 


Test patch:
diff --git a/tests/test_async.py b/tests/test_async.py
index c8bb9bf038..38be27c9a7 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -107,10 +107,12 @@ def test_async_before_after_request():
     def index():
         return ""
 
-    @app.before_first_request
-    async def before_first():
-        nonlocal app_first_called
-        app_first_called = True
+    with pytest.deprecated_call():
+
+        @app.before_first_request
+        async def before_first():
+            nonlocal app_first_called
+            app_first_called = True
 
     @app.before_request
     async def before():
diff --git a/tests/test_basic.py b/tests/test_basic.py
index fa9f902df2..7c1f4197ed 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -1684,9 +1684,11 @@ def index():
 def test_before_first_request_functions(app, client):
     got = []
 
-    @app.before_first_request
-    def foo():
-        got.append(42)
+    with pytest.deprecated_call():
+
+        @app.before_first_request
+        def foo():
+            got.append(42)
 
     client.get("/")
     assert got == [42]
@@ -1698,10 +1700,12 @@ def foo():
 def test_before_first_request_functions_concurrent(app, client):
     got = []
 
-    @app.before_first_request
-    def foo():
-        time.sleep(0.2)
-        got.append(42)
+    with pytest.deprecated_call():
+
+        @app.before_first_request
+        def foo():
+            time.sleep(0.2)
+            got.append(42)
 
     def get_and_assert():
         client.get("/")
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
index fbe9eeee53..1bf130f52b 100644
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -722,9 +722,11 @@ def test_app_request_processing(app, client):
     bp = flask.Blueprint("bp", __name__)
     evts = []
 
-    @bp.before_app_first_request
-    def before_first_request():
-        evts.append("first")
+    with pytest.deprecated_call():
+
+        @bp.before_app_first_request
+        def before_first_request():
+            evts.append("first")
 
     @bp.before_app_request
     def before_app():