Repository: pytest-dev/pytest-flask (language: python)

Issue:
Flask v3.0.0 breaks `pytest-flask` import of `_request_ctx_stack`
**Describe the bug**

[Flask v3.0.0, released Sep 30th](https://flask.palletsprojects.com/en/3.0.x/changes/#version-3-0-0), removed `_request_ctx_stack`, used [in `fixtures.py`](https://github.com/pytest-dev/pytest-flask/blob/04c12559451d6c9da0633d604f094e0c9a0e6bb2/pytest_flask/fixtures.py#L6)

**To Reproduce**

1. Install `pytest-flask`
2. Run your tests
3. See import error

<details><summary>:warning: Import error stacktrace :warning:</summary>
<p>

```
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pytest/__main__.py", line 5, in <module>
    raise SystemExit(pytest.console_main())
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/config/__init__.py", line 187, in console_main
    code = main()
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/config/__init__.py", line 145, in main
    config = _prepareconfig(args, plugins)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/config/__init__.py", line 324, in _prepareconfig
    config = pluginmanager.hook.pytest_cmdline_parse(
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pluggy/_hooks.py", line 493, in __call__
    return self._hookexec(self.name, self._hookimpls, kwargs, firstresult)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pluggy/_manager.py", line [11](https://github.com/zenodo/zenodo-rdm/actions/runs/6382674986/job/17321782664#step:8:12)5, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pluggy/_callers.py", line 130, in _multicall
    teardown[0].send(outcome)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/helpconfig.py", line 102, in pytest_cmdline_parse
    config: Config = outcome.get_result()
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pluggy/_result.py", line 114, in get_result
    raise exc.with_traceback(exc.__traceback__)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pluggy/_callers.py", line 77, in _multicall
    res = hook_impl.function(*args)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/config/__init__.py", line 1017, in pytest_cmdline_parse
    self.parse(args)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/config/__init__.py", line 1305, in parse
    self._preparse(args, addopts=addopts)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/config/__init__.py", line 1188, in _preparse
    self.pluginmanager.load_setuptools_entrypoints("pytest11")
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pluggy/_manager.py", line 398, in load_setuptools_entrypoints
    plugin = ep.load()
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/importlib/metadata.py", line 86, in load
    module = import_module(match.group('module'))
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/importlib/__init__.py", line [12](https://github.com/zenodo/zenodo-rdm/actions/runs/6382674986/job/17321782664#step:8:13)7, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
  File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/assertion/rewrite.py", line 168, in exec_module
    exec(co, module.__dict__)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pytest_invenio/plugin.py", line 25, in <module>
    from .fixtures import (
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/assertion/rewrite.py", line 168, in exec_module
    exec(co, module.__dict__)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pytest_invenio/fixtures.py", line 22, in <module>
    from pytest_flask.plugin import _make_test_response_class
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/assertion/rewrite.py", line 168, in exec_module
    exec(co, module.__dict__)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/pytest_flask/plugin.py", line [14](https://github.com/zenodo/zenodo-rdm/actions/runs/6382674986/job/17321782664#step:8:15), in <module>
    from .fixtures import accept_any
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/_pytest/assertion/rewrite.py", line [16](https://github.com/zenodo/zenodo-rdm/actions/runs/6382674986/job/17321782664#step:8:17)8, in exec_module
    exec(co, module.__dict__)
  File "/opt/hostedtoolcache/Python/3.9.[18](https://github.com/zenodo/zenodo-rdm/actions/runs/6382674986/job/17321782664#step:8:19)/x64/lib/python3.9/site-packages/pytest_flask/fixtures.py", line 6, in <module>
    from flask import _request_ctx_stack
ImportError: cannot import name '_request_ctx_stack' from 'flask' (/opt/hostedtoolcache/Python/3.9.18/x[64](https://github.com/zenodo/zenodo-rdm/actions/runs/6382674986/job/17321782664#step:8:65)/lib/python3.9/site-packages/flask/__init__.py)
```

</p>
</details> 


**Environment (please complete the following information):**
 - OS: any
 - Python Version 3.9
 - pytest-flask version 1.2.0

**Additional context**

It looks like there's a [deprecation warning already for using the fixture](https://github.com/pytest-dev/pytest-flask/blob/04c12559451d6c9da0633d604f094e0c9a0e6bb2/pytest_flask/fixtures.py#L96-L109), so a possible path for a fix would be to:

1. (Optional) "inline" the import inside the fixture function (to avoid affecting people still using the fixture with Flask <3.0.0, and give them a chance to respond to the deprecation warning).
2. Remove the fixture completely and release `pytest-flask` v2.0.0



Discussion / hints:
The request_ctx text fixture has had a deprecation warning on it for about two years.
Should that just be removed at which point `request_ctx_stack` is no longer needed?


Fix patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 99154ce..ebd5f6b 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -3,6 +3,11 @@
 Changelog
 =========
 
+UNRELEASED
+----------
+
+- Fix compatibility with ``Flask 3.0`` -- the consequence is that the deprecated and incompatible ``request_ctx`` has been removed.
+
 1.2.1
 ------------------
 - Fix bug in ``:meth:pytest_flask.fixtures.live_server``
diff --git a/docs/features.rst b/docs/features.rst
index c19de7d..5fba992 100644
--- a/docs/features.rst
+++ b/docs/features.rst
@@ -188,28 +188,6 @@ in your project's ``pytest.ini`` file)::
     addopts = --live-server-port=5000
 
 
-``request_ctx`` - request context (Deprecated)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-**This fixture is deprecated and will be removed in the future.**
-
-The request context which contains all request relevant information.
-
-.. hint::
-
-    The request context has been pushed implicitly any time the ``app``
-    fixture is applied and is kept around during test execution, so it’s easy
-    to introspect the data:
-
-    .. code:: python
-
-        from flask import request, url_for
-
-        def test_request_headers(client):
-            res = client.get(url_for('ping'), headers=[('X-Something', '42')])
-            assert request.headers['X-Something'] == '42'
-
-
 ``live_server_scope`` - set the scope of the live server
 ``````````````````````````````````````````````````````````````````
 
diff --git a/requirements/main.txt b/requirements/main.txt
index 17ab14e..c30a17e 100644
--- a/requirements/main.txt
+++ b/requirements/main.txt
@@ -1,3 +1,3 @@
 pytest>=5.2
-Flask <3.0
-Werkzeug>=0.7
+Flask
+Werkzeug


Test patch:
diff --git a/src/pytest_flask/fixtures.py b/src/pytest_flask/fixtures.py
index 45c94bf..2d8a2a8 100644
--- a/src/pytest_flask/fixtures.py
+++ b/src/pytest_flask/fixtures.py
@@ -3,7 +3,6 @@
 import warnings
 
 import pytest
-from flask import _request_ctx_stack
 
 from ._internal import _determine_scope
 from ._internal import _make_accept_header
@@ -92,23 +91,6 @@ def config(app):
     return app.config
 
 
-@pytest.fixture
-def request_ctx(app):
-    """The request context which contains all request relevant information,
-    e.g. `session`, `g`, `flashes`, etc.
-    """
-    warnings.warn(
-        "In Werzeug 2.0.0, the Client request methods "
-        "(client.get, client.post) always return an instance of TestResponse. This "
-        "class provides a reference to the request object through 'response.request' "
-        "The fixture 'request_ctx' is deprecated and will be removed in the future, using TestResponse.request "
-        "is the preferred way.",
-        DeprecationWarning,
-        stacklevel=2,
-    )
-    return _request_ctx_stack.top
-
-
 @pytest.fixture(params=["application/json", "text/html"])
 def mimetype(request):
     return request.param
diff --git a/src/pytest_flask/plugin.py b/src/pytest_flask/plugin.py
index 07b1df8..6dafacb 100644
--- a/src/pytest_flask/plugin.py
+++ b/src/pytest_flask/plugin.py
@@ -15,7 +15,6 @@
 from .fixtures import client_class
 from .fixtures import config
 from .fixtures import live_server
-from .fixtures import request_ctx
 from .pytest_compat import getfixturevalue
 
 
diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py
index 1d58217..b38af95 100755
--- a/tests/test_fixtures.py
+++ b/tests/test_fixtures.py
@@ -16,21 +16,6 @@ def test_accept_json(self, accept_json):
     def test_accept_jsonp(self, accept_jsonp):
         assert accept_jsonp == [("Accept", "application/json-p")]
 
-    def test_request_ctx(self, app, request_ctx):
-        assert request_ctx.app is app
-
-    def test_request_ctx_is_kept_around(self, client):
-        res = client.get(url_for("index"), headers=[("X-Something", "42")])
-        """In werkzeug 2.0.0 the test Client provides a new attribute 'request'
-        in the response class which holds a reference to the request object that
-        produced the respective response, making instrospection easier"""
-        try:
-            assert res.request.headers["X-Something"] == "42"
-        except AttributeError:
-            """This is the conventional (pre 2.0.0) way of reaching the
-            request object, using flask.request global."""
-            assert request.headers["X-Something"] == "42"
-
     def test_accept_mimetype(self, accept_mimetype):
         mimestrings = [[("Accept", "application/json")], [("Accept", "text/html")]]
         assert accept_mimetype in mimestrings