diff --git a/src/flask/app.py b/src/flask/app.py index 736061b9..084429aa 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -38,10 +38,11 @@ from .config import ConfigAttribute from .ctx import _AppCtxGlobals from .ctx import AppContext from .ctx import RequestContext -from .globals import _app_ctx_stack -from .globals import _request_ctx_stack +from .globals import _cv_app +from .globals import _cv_req from .globals import g from .globals import request +from .globals import request_ctx from .globals import session from .helpers import _split_blueprint_path from .helpers import get_debug_flag @@ -1554,10 +1555,10 @@ class Flask(Scaffold): This no longer does the exception handling, this code was moved to the new :meth:`full_dispatch_request`. """ - req = _request_ctx_stack.top.request + req = request_ctx.request if req.routing_exception is not None: self.raise_routing_exception(req) - rule = req.url_rule + rule: Rule = req.url_rule # type: ignore[assignment] # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically if ( @@ -1566,7 +1567,8 @@ class Flask(Scaffold): ): return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint - return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args) + view_args: t.Dict[str, t.Any] = req.view_args # type: ignore[assignment] + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) def full_dispatch_request(self) -> Response: """Dispatches the request and on top of that performs request @@ -1631,8 +1633,8 @@ class Flask(Scaffold): .. versionadded:: 0.7 """ - adapter = _request_ctx_stack.top.url_adapter - methods = adapter.allowed_methods() + adapter = request_ctx.url_adapter + methods = adapter.allowed_methods() # type: ignore[union-attr] rv = self.response_class() rv.allow.update(methods) return rv @@ -1740,7 +1742,7 @@ class Flask(Scaffold): .. versionadded:: 2.2 Moved from ``flask.url_for``, which calls this method. """ - req_ctx = _request_ctx_stack.top + req_ctx = _cv_req.get(None) if req_ctx is not None: url_adapter = req_ctx.url_adapter @@ -1759,7 +1761,7 @@ class Flask(Scaffold): if _external is None: _external = _scheme is not None else: - app_ctx = _app_ctx_stack.top + app_ctx = _cv_app.get(None) # If called by helpers.url_for, an app context is active, # use its url_adapter. Otherwise, app.url_for was called @@ -1790,7 +1792,7 @@ class Flask(Scaffold): self.inject_url_defaults(endpoint, values) try: - rv = url_adapter.build( + rv = url_adapter.build( # type: ignore[union-attr] endpoint, values, method=_method, @@ -2099,7 +2101,7 @@ class Flask(Scaffold): :return: a new response object or the same, has to be an instance of :attr:`response_class`. """ - ctx = _request_ctx_stack.top + ctx = request_ctx._get_current_object() # type: ignore[attr-defined] for func in ctx._after_request_functions: response = self.ensure_sync(func)(response) @@ -2305,8 +2307,8 @@ class Flask(Scaffold): return response(environ, start_response) finally: if "werkzeug.debug.preserve_context" in environ: - environ["werkzeug.debug.preserve_context"](_app_ctx_stack.top) - environ["werkzeug.debug.preserve_context"](_request_ctx_stack.top) + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_req.get()) if error is not None and self.should_ignore_error(error): error = None diff --git a/src/flask/cli.py b/src/flask/cli.py index af29b2c1..ba0db467 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -1051,13 +1051,11 @@ def shell_command() -> None: without having to manually configure the application. """ import code - from .globals import _app_ctx_stack - app = _app_ctx_stack.top.app banner = ( f"Python {sys.version} on {sys.platform}\n" - f"App: {app.import_name} [{app.env}]\n" - f"Instance: {app.instance_path}" + f"App: {current_app.import_name} [{current_app.env}]\n" + f"Instance: {current_app.instance_path}" ) ctx: dict = {} @@ -1068,7 +1066,7 @@ def shell_command() -> None: with open(startup) as f: eval(compile(f.read(), startup, "exec"), ctx) - ctx.update(app.make_shell_context()) + ctx.update(current_app.make_shell_context()) # Site, customize, or startup script can set a hook to call when # entering interactive mode. The default one sets up readline with diff --git a/src/flask/ctx.py b/src/flask/ctx.py index 758127ea..62242e80 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -7,10 +7,8 @@ from types import TracebackType from werkzeug.exceptions import HTTPException from . import typing as ft -from .globals import _app_ctx_stack from .globals import _cv_app from .globals import _cv_req -from .globals import _request_ctx_stack from .signals import appcontext_popped from .signals import appcontext_pushed @@ -106,9 +104,9 @@ class _AppCtxGlobals: return iter(self.__dict__) def __repr__(self) -> str: - top = _app_ctx_stack.top - if top is not None: - return f"" + ctx = _cv_app.get(None) + if ctx is not None: + return f"" return object.__repr__(self) @@ -133,15 +131,15 @@ def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: .. versionadded:: 0.9 """ - top = _request_ctx_stack.top + ctx = _cv_req.get(None) - if top is None: + if ctx is None: raise RuntimeError( - "This decorator can only be used when a request context is" - " active, such as within a view function." + "'after_this_request' can only be used when a request" + " context is active, such as in a view function." ) - top._after_request_functions.append(f) + ctx._after_request_functions.append(f) return f @@ -169,19 +167,19 @@ def copy_current_request_context(f: t.Callable) -> t.Callable: .. versionadded:: 0.10 """ - top = _request_ctx_stack.top + ctx = _cv_req.get(None) - if top is None: + if ctx is None: raise RuntimeError( - "This decorator can only be used when a request context is" - " active, such as within a view function." + "'copy_current_request_context' can only be used when a" + " request context is active, such as in a view function." ) - reqctx = top.copy() + ctx = ctx.copy() def wrapper(*args, **kwargs): - with reqctx: - return reqctx.app.ensure_sync(f)(*args, **kwargs) + with ctx: + return ctx.app.ensure_sync(f)(*args, **kwargs) return update_wrapper(wrapper, f) @@ -240,7 +238,7 @@ class AppContext: def __init__(self, app: "Flask") -> None: self.app = app self.url_adapter = app.create_url_adapter(None) - self.g = app.app_ctx_globals_class() + self.g: _AppCtxGlobals = app.app_ctx_globals_class() self._cv_tokens: t.List[contextvars.Token] = [] def push(self) -> None: @@ -311,14 +309,14 @@ class RequestContext: self.app = app if request is None: request = app.request_class(environ) - self.request = request + self.request: Request = request self.url_adapter = None try: self.url_adapter = app.create_url_adapter(self.request) except HTTPException as e: self.request.routing_exception = e - self.flashes = None - self.session = session + self.flashes: t.Optional[t.List[t.Tuple[str, str]]] = None + self.session: t.Optional["SessionMixin"] = session # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index b1e3ce1b..b0639892 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -2,7 +2,7 @@ import typing as t from .app import Flask from .blueprints import Blueprint -from .globals import _request_ctx_stack +from .globals import request_ctx class UnexpectedUnicodeError(AssertionError, UnicodeError): @@ -116,9 +116,8 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None: info = [f"Locating template {template!r}:"] total_found = 0 blueprint = None - reqctx = _request_ctx_stack.top - if reqctx is not None and reqctx.request.blueprint is not None: - blueprint = reqctx.request.blueprint + if request_ctx and request_ctx.request.blueprint is not None: + blueprint = request_ctx.request.blueprint for idx, (loader, srcobj, triple) in enumerate(attempts): if isinstance(srcobj, Flask): diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 79c562c0..bc074b6a 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -12,9 +12,10 @@ import werkzeug.utils from werkzeug.exceptions import abort as _wz_abort from werkzeug.utils import redirect as _wz_redirect -from .globals import _request_ctx_stack +from .globals import _cv_req from .globals import current_app from .globals import request +from .globals import request_ctx from .globals import session from .signals import message_flashed @@ -110,11 +111,11 @@ def stream_with_context( return update_wrapper(decorator, generator_or_function) # type: ignore def generator() -> t.Generator: - ctx = _request_ctx_stack.top + ctx = _cv_req.get(None) if ctx is None: raise RuntimeError( - "Attempted to stream with context but " - "there was no context in the first place to keep around." + "'stream_with_context' can only be used when a request" + " context is active, such as in a view function." ) with ctx: # Dummy sentinel. Has to be inside the context block or we're @@ -377,11 +378,10 @@ def get_flashed_messages( :param category_filter: filter of categories to limit return values. Only categories in the list will be returned. """ - flashes = _request_ctx_stack.top.flashes + flashes = request_ctx.flashes if flashes is None: - _request_ctx_stack.top.flashes = flashes = ( - session.pop("_flashes") if "_flashes" in session else [] - ) + flashes = session.pop("_flashes") if "_flashes" in session else [] + request_ctx.flashes = flashes if category_filter: flashes = list(filter(lambda f: f[0] in category_filter, flashes)) if not with_categories: diff --git a/src/flask/templating.py b/src/flask/templating.py index 7d92cf1e..24a672b7 100644 --- a/src/flask/templating.py +++ b/src/flask/templating.py @@ -5,8 +5,8 @@ from jinja2 import Environment as BaseEnvironment from jinja2 import Template from jinja2 import TemplateNotFound -from .globals import _app_ctx_stack -from .globals import _request_ctx_stack +from .globals import _cv_app +from .globals import _cv_req from .globals import current_app from .globals import request from .helpers import stream_with_context @@ -22,9 +22,9 @@ def _default_template_ctx_processor() -> t.Dict[str, t.Any]: """Default template context processor. Injects `request`, `session` and `g`. """ - reqctx = _request_ctx_stack.top - appctx = _app_ctx_stack.top - rv = {} + appctx = _cv_app.get(None) + reqctx = _cv_req.get(None) + rv: t.Dict[str, t.Any] = {} if appctx is not None: rv["g"] = appctx.g if reqctx is not None: @@ -124,7 +124,8 @@ class DispatchingJinjaLoader(BaseLoader): return list(result) -def _render(template: Template, context: dict, app: "Flask") -> str: +def _render(app: "Flask", template: Template, context: t.Dict[str, t.Any]) -> str: + app.update_template_context(context) before_render_template.send(app, template=template, context=context) rv = template.render(context) template_rendered.send(app, template=template, context=context) @@ -135,36 +136,27 @@ def render_template( template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]], **context: t.Any ) -> str: - """Renders a template from the template folder with the given - context. + """Render a template by name with the given context. - :param template_name_or_list: the name of the template to be - rendered, or an iterable with template names - the first one existing will be rendered - :param context: the variables that should be available in the - context of the template. + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. """ - ctx = _app_ctx_stack.top - ctx.app.update_template_context(context) - return _render( - ctx.app.jinja_env.get_or_select_template(template_name_or_list), - context, - ctx.app, - ) + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _render(app, template, context) def render_template_string(source: str, **context: t.Any) -> str: - """Renders a template from the given template source string - with the given context. Template variables will be autoescaped. + """Render a template from the given source string with the given + context. - :param source: the source code of the template to be - rendered - :param context: the variables that should be available in the - context of the template. + :param source: The source code of the template to render. + :param context: The variables to make available in the template. """ - ctx = _app_ctx_stack.top - ctx.app.update_template_context(context) - return _render(ctx.app.jinja_env.from_string(source), context, ctx.app) + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _render(app, template, context) def _stream( diff --git a/src/flask/testing.py b/src/flask/testing.py index 0e1189c9..e188439b 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -163,7 +163,7 @@ class FlaskClient(Client): # behavior. It's important to not use the push and pop # methods of the actual request context object since that would # mean that cleanup handlers are called - token = _cv_req.set(outer_reqctx) + token = _cv_req.set(outer_reqctx) # type: ignore[arg-type] try: yield sess finally: diff --git a/tests/conftest.py b/tests/conftest.py index 1e1ba0d4..670acc88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,8 +6,8 @@ import textwrap import pytest from _pytest import monkeypatch -import flask -from flask import Flask as _Flask +from flask import Flask +from flask.globals import request_ctx @pytest.fixture(scope="session", autouse=True) @@ -44,14 +44,13 @@ def _reset_os_environ(monkeypatch, _standard_os_environ): monkeypatch._setitem.extend(_standard_os_environ) -class Flask(_Flask): - testing = True - secret_key = "test key" - - @pytest.fixture def app(): app = Flask("flask_test", root_path=os.path.dirname(__file__)) + app.config.update( + TESTING=True, + SECRET_KEY="test key", + ) return app @@ -92,8 +91,10 @@ def leak_detector(): # make sure we're not leaking a request context since we are # testing flask internally in debug mode in a few cases leaks = [] - while flask._request_ctx_stack.top is not None: - leaks.append(flask._request_ctx_stack.pop()) + while request_ctx: + leaks.append(request_ctx._get_current_object()) + request_ctx.pop() + assert leaks == [] diff --git a/tests/test_appctx.py b/tests/test_appctx.py index aeb75a55..f5ca0bde 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -1,6 +1,8 @@ import pytest import flask +from flask.globals import app_ctx +from flask.globals import request_ctx def test_basic_url_generation(app): @@ -29,14 +31,14 @@ def test_url_generation_without_context_fails(): def test_request_context_means_app_context(app): with app.test_request_context(): - assert flask.current_app._get_current_object() == app - assert flask._app_ctx_stack.top is None + assert flask.current_app._get_current_object() is app + assert not flask.current_app def test_app_context_provides_current_app(app): with app.app_context(): - assert flask.current_app._get_current_object() == app - assert flask._app_ctx_stack.top is None + assert flask.current_app._get_current_object() is app + assert not flask.current_app def test_app_tearing_down(app): @@ -175,11 +177,11 @@ def test_context_refcounts(app, client): @app.route("/") def index(): - with flask._app_ctx_stack.top: - with flask._request_ctx_stack.top: + with app_ctx: + with request_ctx: pass - env = flask._request_ctx_stack.top.request.environ - assert env["werkzeug.request"] is not None + + assert flask.request.environ["werkzeug.request"] is not None return "" res = client.get("/") diff --git a/tests/test_basic.py b/tests/test_basic.py index dca48e2d..3e98fdac 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1110,14 +1110,10 @@ def test_enctype_debug_helper(app, client): def index(): return flask.request.files["foo"].filename - # with statement is important because we leave an exception on the - # stack otherwise and we want to ensure that this is not the case - # to not negatively affect other tests. - with client: - with pytest.raises(DebugFilesKeyError) as e: - client.post("/fail", data={"foo": "index.txt"}) - assert "no file contents were transmitted" in str(e.value) - assert "This was submitted: 'index.txt'" in str(e.value) + with pytest.raises(DebugFilesKeyError) as e: + client.post("/fail", data={"foo": "index.txt"}) + assert "no file contents were transmitted" in str(e.value) + assert "This was submitted: 'index.txt'" in str(e.value) def test_response_types(app, client): @@ -1548,29 +1544,21 @@ def test_server_name_subdomain(): assert rv.data == b"subdomain" -@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") -@pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") -def test_exception_propagation(app, client): - def apprunner(config_key): - @app.route("/") - def index(): - 1 // 0 +@pytest.mark.parametrize("key", ["TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None]) +def test_exception_propagation(app, client, key): + app.testing = False - if config_key is not None: - app.config[config_key] = True - with pytest.raises(Exception): - client.get("/") - else: - assert client.get("/").status_code == 500 + @app.route("/") + def index(): + 1 // 0 - # we have to run this test in an isolated thread because if the - # debug flag is set to true and an exception happens the context is - # not torn down. This causes other tests that run after this fail - # when they expect no exception on the stack. - for config_key in "TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None: - t = Thread(target=apprunner, args=(config_key,)) - t.start() - t.join() + if key is not None: + app.config[key] = True + + with pytest.raises(ZeroDivisionError): + client.get("/") + else: + assert client.get("/").status_code == 500 @pytest.mark.parametrize("debug", [True, False]) diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py index 1a478917..abfacb98 100644 --- a/tests/test_reqctx.py +++ b/tests/test_reqctx.py @@ -3,6 +3,7 @@ import warnings import pytest import flask +from flask.globals import request_ctx from flask.sessions import SecureCookieSessionInterface from flask.sessions import SessionInterface @@ -116,7 +117,7 @@ def test_context_binding(app): assert index() == "Hello World!" with app.test_request_context("/meh"): assert meh() == "http://localhost/meh" - assert flask._request_ctx_stack.top is None + assert not flask.request def test_context_test(app): @@ -152,7 +153,7 @@ class TestGreenletContextCopying: @app.route("/") def index(): flask.session["fizz"] = "buzz" - reqctx = flask._request_ctx_stack.top.copy() + reqctx = request_ctx.copy() def g(): assert not flask.request diff --git a/tests/test_session_interface.py b/tests/test_session_interface.py index 39562f5a..613da37f 100644 --- a/tests/test_session_interface.py +++ b/tests/test_session_interface.py @@ -1,4 +1,5 @@ import flask +from flask.globals import request_ctx from flask.sessions import SessionInterface @@ -13,7 +14,7 @@ def test_open_session_with_endpoint(): pass def open_session(self, app, request): - flask._request_ctx_stack.top.match_request() + request_ctx.match_request() assert request.endpoint is not None app = flask.Flask(__name__) diff --git a/tests/test_testing.py b/tests/test_testing.py index dd6347e5..0b37a2e5 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -5,6 +5,7 @@ import werkzeug import flask from flask import appcontext_popped from flask.cli import ScriptInfo +from flask.globals import _cv_req from flask.json import jsonify from flask.testing import EnvironBuilder from flask.testing import FlaskCliRunner @@ -399,4 +400,4 @@ def test_client_pop_all_preserved(app, req_ctx, client): # close the response, releasing the context held by stream_with_context rv.close() # only req_ctx fixture should still be pushed - assert flask._request_ctx_stack.top is req_ctx + assert _cv_req.get(None) is req_ctx