From d597db67ded68d4aedf42b9d1d4570573fb79cd6 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 4 Jul 2022 22:50:51 -0700 Subject: [PATCH] contexts no longer use LocalStack --- CHANGES.rst | 9 +++++ src/flask/__init__.py | 28 ++++++++++++- src/flask/ctx.py | 81 ++++++++++++++++---------------------- src/flask/globals.py | 91 +++++++++++++++++++++++++++++++++++-------- src/flask/testing.py | 8 ++-- 5 files changed, 146 insertions(+), 71 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 504e319f..5fda08e3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,15 @@ Unreleased - The ``RequestContext.g`` property returning ``AppContext.g`` is removed. +- The app and request contexts are managed using Python context vars + directly rather than Werkzeug's ``LocalStack``. This should result + in better performance and memory use. :pr:`4672` + + - Extension maintainers, be aware that ``_app_ctx_stack.top`` + and ``_request_ctx_stack.top`` are deprecated. Store data on + ``g`` instead using a unique prefix, like + ``g._extension_name_attr``. + - Add new customization points to the ``Flask`` app object for many previously global behaviors. diff --git a/src/flask/__init__.py b/src/flask/__init__.py index 8ca1dbad..7eb8c24b 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -11,8 +11,6 @@ from .ctx import after_this_request as after_this_request from .ctx import copy_current_request_context as copy_current_request_context from .ctx import has_app_context as has_app_context from .ctx import has_request_context as has_request_context -from .globals import _app_ctx_stack as _app_ctx_stack -from .globals import _request_ctx_stack as _request_ctx_stack from .globals import current_app as current_app from .globals import g as g from .globals import request as request @@ -45,3 +43,29 @@ from .templating import stream_template as stream_template from .templating import stream_template_string as stream_template_string __version__ = "2.2.0.dev0" + + +def __getattr__(name): + if name == "_app_ctx_stack": + import warnings + from .globals import __app_ctx_stack + + warnings.warn( + "'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + return __app_ctx_stack + + if name == "_request_ctx_stack": + import warnings + from .globals import __request_ctx_stack + + warnings.warn( + "'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + return __request_ctx_stack + + raise AttributeError(name) diff --git a/src/flask/ctx.py b/src/flask/ctx.py index dc1f23b3..758127ea 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -1,3 +1,4 @@ +import contextvars import sys import typing as t from functools import update_wrapper @@ -7,6 +8,8 @@ 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 @@ -212,7 +215,7 @@ def has_request_context() -> bool: .. versionadded:: 0.7 """ - return _request_ctx_stack.top is not None + return _cv_app.get(None) is not None def has_app_context() -> bool: @@ -222,7 +225,7 @@ def has_app_context() -> bool: .. versionadded:: 0.9 """ - return _app_ctx_stack.top is not None + return _cv_req.get(None) is not None class AppContext: @@ -238,28 +241,29 @@ class AppContext: self.app = app self.url_adapter = app.create_url_adapter(None) self.g = app.app_ctx_globals_class() - - # Like request context, app contexts can be pushed multiple times - # but there a basic "refcount" is enough to track them. - self._refcnt = 0 + self._cv_tokens: t.List[contextvars.Token] = [] def push(self) -> None: """Binds the app context to the current context.""" - self._refcnt += 1 - _app_ctx_stack.push(self) + self._cv_tokens.append(_cv_app.set(self)) appcontext_pushed.send(self.app) def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore """Pops the app context.""" try: - self._refcnt -= 1 - if self._refcnt <= 0: + if len(self._cv_tokens) == 1: if exc is _sentinel: exc = sys.exc_info()[1] self.app.do_teardown_appcontext(exc) finally: - rv = _app_ctx_stack.pop() - assert rv is self, f"Popped wrong app context. ({rv!r} instead of {self!r})" + ctx = _cv_app.get() + _cv_app.reset(self._cv_tokens.pop()) + + if ctx is not self: + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" + ) + appcontext_popped.send(self.app) def __enter__(self) -> "AppContext": @@ -315,18 +319,13 @@ class RequestContext: self.request.routing_exception = e self.flashes = None self.session = session - - # Request contexts can be pushed multiple times and interleaved with - # other request contexts. Now only if the last level is popped we - # get rid of them. Additionally if an application context is missing - # one is created implicitly so for each level we add this information - self._implicit_app_ctx_stack: t.List[t.Optional["AppContext"]] = [] - # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. self._after_request_functions: t.List[ft.AfterRequestCallable] = [] + self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = [] + def copy(self) -> "RequestContext": """Creates a copy of this request context with the same request object. This can be used to move a request context to a different greenlet. @@ -360,15 +359,15 @@ class RequestContext: def push(self) -> None: # Before we push the request context we have to ensure that there # is an application context. - app_ctx = _app_ctx_stack.top - if app_ctx is None or app_ctx.app != self.app: + app_ctx = _cv_app.get(None) + + if app_ctx is None or app_ctx.app is not self.app: app_ctx = self.app.app_context() app_ctx.push() - self._implicit_app_ctx_stack.append(app_ctx) else: - self._implicit_app_ctx_stack.append(None) + app_ctx = None - _request_ctx_stack.push(self) + self._cv_tokens.append((_cv_req.set(self), app_ctx)) # Open the session at the moment that the request context is available. # This allows a custom open_session method to use the request context. @@ -394,11 +393,10 @@ class RequestContext: .. versionchanged:: 0.9 Added the `exc` argument. """ - app_ctx = self._implicit_app_ctx_stack.pop() - clear_request = False + clear_request = len(self._cv_tokens) == 1 try: - if not self._implicit_app_ctx_stack: + if clear_request: if exc is _sentinel: exc = sys.exc_info()[1] self.app.do_teardown_request(exc) @@ -406,36 +404,23 @@ class RequestContext: request_close = getattr(self.request, "close", None) if request_close is not None: request_close() - clear_request = True finally: - rv = _request_ctx_stack.pop() + ctx = _cv_req.get() + token, app_ctx = self._cv_tokens.pop() + _cv_req.reset(token) # get rid of circular dependencies at the end of the request # so that we don't require the GC to be active. if clear_request: - rv.request.environ["werkzeug.request"] = None + ctx.request.environ["werkzeug.request"] = None - # Get rid of the app as well if necessary. if app_ctx is not None: app_ctx.pop(exc) - assert ( - rv is self - ), f"Popped wrong request context. ({rv!r} instead of {self!r})" - - def auto_pop(self, exc: t.Optional[BaseException]) -> None: - """ - .. deprecated:: 2.2 - Will be removed in Flask 2.3. - """ - import warnings - - warnings.warn( - "'ctx.auto_pop' is deprecated and will be removed in Flask 2.3.", - DeprecationWarning, - stacklevel=2, - ) - self.pop(exc) + if ctx is not self: + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) def __enter__(self) -> "RequestContext": self.push() diff --git a/src/flask/globals.py b/src/flask/globals.py index f4781f6c..af079877 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -1,7 +1,7 @@ import typing as t from contextvars import ContextVar -from werkzeug.local import LocalStack +from werkzeug.local import LocalProxy if t.TYPE_CHECKING: # pragma: no cover from .app import Flask @@ -11,6 +11,39 @@ if t.TYPE_CHECKING: # pragma: no cover from .sessions import SessionMixin from .wrappers import Request + +class _FakeStack: + def __init__(self, name: str, cv: ContextVar[t.Any]) -> None: + self.name = name + self.cv = cv + + def _warn(self): + import warnings + + warnings.warn( + f"'_{self.name}_ctx_stack' is deprecated and will be" + " removed in Flask 2.3. Use 'g' to store data, or" + f" '{self.name}_ctx' to access the current context.", + DeprecationWarning, + stacklevel=3, + ) + + def push(self, obj: t.Any) -> None: + self._warn() + self.cv.set(obj) + + def pop(self) -> t.Any: + self._warn() + ctx = self.cv.get(None) + self.cv.set(None) + return ctx + + @property + def top(self) -> t.Optional[t.Any]: + self._warn() + return self.cv.get(None) + + _no_app_msg = """\ Working outside of application context. @@ -18,16 +51,16 @@ This typically means that you attempted to use functionality that needed the current application. To solve this, set up an application context with app.app_context(). See the documentation for more information.\ """ -_cv_app: ContextVar[t.List["AppContext"]] = ContextVar("flask.app_ctx") -_app_ctx_stack: LocalStack["AppContext"] = LocalStack(_cv_app) -app_ctx: "AppContext" = _app_ctx_stack( # type: ignore[assignment] - unbound_message=_no_app_msg +_cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx") +__app_ctx_stack = _FakeStack("app", _cv_app) +app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment] + _cv_app, unbound_message=_no_app_msg ) -current_app: "Flask" = _app_ctx_stack( # type: ignore[assignment] - "app", unbound_message=_no_app_msg +current_app: "Flask" = LocalProxy( # type: ignore[assignment] + _cv_app, "app", unbound_message=_no_app_msg ) -g: "_AppCtxGlobals" = _app_ctx_stack( # type: ignore[assignment] - "g", unbound_message=_no_app_msg +g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment] + _cv_app, "g", unbound_message=_no_app_msg ) _no_req_msg = """\ @@ -37,14 +70,38 @@ This typically means that you attempted to use functionality that needed an active HTTP request. Consult the documentation on testing for information about how to avoid this problem.\ """ -_cv_req: ContextVar[t.List["RequestContext"]] = ContextVar("flask.request_ctx") -_request_ctx_stack: LocalStack["RequestContext"] = LocalStack(_cv_req) -request_ctx: "RequestContext" = _request_ctx_stack( # type: ignore[assignment] - unbound_message=_no_req_msg +_cv_req: ContextVar["RequestContext"] = ContextVar("flask.request_ctx") +__request_ctx_stack = _FakeStack("request", _cv_req) +request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment] + _cv_req, unbound_message=_no_req_msg ) -request: "Request" = _request_ctx_stack( # type: ignore[assignment] - "request", unbound_message=_no_req_msg +request: "Request" = LocalProxy( # type: ignore[assignment] + _cv_req, "request", unbound_message=_no_req_msg ) -session: "SessionMixin" = _request_ctx_stack( # type: ignore[assignment] - "session", unbound_message=_no_req_msg +session: "SessionMixin" = LocalProxy( # type: ignore[assignment] + _cv_req, "session", unbound_message=_no_req_msg ) + + +def __getattr__(name: str) -> t.Any: + if name == "_app_ctx_stack": + import warnings + + warnings.warn( + "'_app_ctx_stack' is deprecated and will be remoevd in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + return __app_ctx_stack + + if name == "_request_ctx_stack": + import warnings + + warnings.warn( + "'_request_ctx_stack' is deprecated and will be remoevd in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + return __request_ctx_stack + + raise AttributeError(name) diff --git a/src/flask/testing.py b/src/flask/testing.py index f652c23a..0e1189c9 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -11,7 +11,7 @@ from werkzeug.urls import url_parse from werkzeug.wrappers import Request as BaseRequest from .cli import ScriptInfo -from .globals import _request_ctx_stack +from .globals import _cv_req from .json import dumps as json_dumps from .sessions import SessionMixin @@ -147,7 +147,7 @@ class FlaskClient(Client): app = self.application environ_overrides = kwargs.setdefault("environ_overrides", {}) self.cookie_jar.inject_wsgi(environ_overrides) - outer_reqctx = _request_ctx_stack.top + outer_reqctx = _cv_req.get(None) with app.test_request_context(*args, **kwargs) as c: session_interface = app.session_interface sess = session_interface.open_session(app, c.request) @@ -163,11 +163,11 @@ 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 - _request_ctx_stack.push(outer_reqctx) + token = _cv_req.set(outer_reqctx) try: yield sess finally: - _request_ctx_stack.pop() + _cv_req.reset(token) resp = app.response_class() if not session_interface.is_null_session(sess):