contexts no longer use LocalStack

This commit is contained in:
David Lord 2022-07-04 22:50:51 -07:00
parent 0b2f809f9b
commit d597db67de
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
5 changed files with 146 additions and 71 deletions

View file

@ -15,6 +15,15 @@ Unreleased
- The ``RequestContext.g`` property returning ``AppContext.g`` is - The ``RequestContext.g`` property returning ``AppContext.g`` is
removed. 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 - Add new customization points to the ``Flask`` app object for many
previously global behaviors. previously global behaviors.

View file

@ -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 copy_current_request_context as copy_current_request_context
from .ctx import has_app_context as has_app_context from .ctx import has_app_context as has_app_context
from .ctx import has_request_context as has_request_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 current_app as current_app
from .globals import g as g from .globals import g as g
from .globals import request as request 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 from .templating import stream_template_string as stream_template_string
__version__ = "2.2.0.dev0" __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)

View file

@ -1,3 +1,4 @@
import contextvars
import sys import sys
import typing as t import typing as t
from functools import update_wrapper from functools import update_wrapper
@ -7,6 +8,8 @@ from werkzeug.exceptions import HTTPException
from . import typing as ft from . import typing as ft
from .globals import _app_ctx_stack from .globals import _app_ctx_stack
from .globals import _cv_app
from .globals import _cv_req
from .globals import _request_ctx_stack from .globals import _request_ctx_stack
from .signals import appcontext_popped from .signals import appcontext_popped
from .signals import appcontext_pushed from .signals import appcontext_pushed
@ -212,7 +215,7 @@ def has_request_context() -> bool:
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
return _request_ctx_stack.top is not None return _cv_app.get(None) is not None
def has_app_context() -> bool: def has_app_context() -> bool:
@ -222,7 +225,7 @@ def has_app_context() -> bool:
.. versionadded:: 0.9 .. versionadded:: 0.9
""" """
return _app_ctx_stack.top is not None return _cv_req.get(None) is not None
class AppContext: class AppContext:
@ -238,28 +241,29 @@ class AppContext:
self.app = app self.app = app
self.url_adapter = app.create_url_adapter(None) self.url_adapter = app.create_url_adapter(None)
self.g = app.app_ctx_globals_class() self.g = app.app_ctx_globals_class()
self._cv_tokens: t.List[contextvars.Token] = []
# Like request context, app contexts can be pushed multiple times
# but there a basic "refcount" is enough to track them.
self._refcnt = 0
def push(self) -> None: def push(self) -> None:
"""Binds the app context to the current context.""" """Binds the app context to the current context."""
self._refcnt += 1 self._cv_tokens.append(_cv_app.set(self))
_app_ctx_stack.push(self)
appcontext_pushed.send(self.app) appcontext_pushed.send(self.app)
def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore
"""Pops the app context.""" """Pops the app context."""
try: try:
self._refcnt -= 1 if len(self._cv_tokens) == 1:
if self._refcnt <= 0:
if exc is _sentinel: if exc is _sentinel:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
self.app.do_teardown_appcontext(exc) self.app.do_teardown_appcontext(exc)
finally: finally:
rv = _app_ctx_stack.pop() ctx = _cv_app.get()
assert rv is self, f"Popped wrong app context. ({rv!r} instead of {self!r})" _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) appcontext_popped.send(self.app)
def __enter__(self) -> "AppContext": def __enter__(self) -> "AppContext":
@ -315,18 +319,13 @@ class RequestContext:
self.request.routing_exception = e self.request.routing_exception = e
self.flashes = None self.flashes = None
self.session = session 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 # Functions that should be executed after the request on the response
# object. These will be called before the regular "after_request" # object. These will be called before the regular "after_request"
# functions. # functions.
self._after_request_functions: t.List[ft.AfterRequestCallable] = [] self._after_request_functions: t.List[ft.AfterRequestCallable] = []
self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = []
def copy(self) -> "RequestContext": def copy(self) -> "RequestContext":
"""Creates a copy of this request context with the same request object. """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. This can be used to move a request context to a different greenlet.
@ -360,15 +359,15 @@ class RequestContext:
def push(self) -> None: def push(self) -> None:
# Before we push the request context we have to ensure that there # Before we push the request context we have to ensure that there
# is an application context. # is an application context.
app_ctx = _app_ctx_stack.top app_ctx = _cv_app.get(None)
if app_ctx is None or app_ctx.app != self.app:
if app_ctx is None or app_ctx.app is not self.app:
app_ctx = self.app.app_context() app_ctx = self.app.app_context()
app_ctx.push() app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else: 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. # Open the session at the moment that the request context is available.
# This allows a custom open_session method to use the request context. # This allows a custom open_session method to use the request context.
@ -394,11 +393,10 @@ class RequestContext:
.. versionchanged:: 0.9 .. versionchanged:: 0.9
Added the `exc` argument. Added the `exc` argument.
""" """
app_ctx = self._implicit_app_ctx_stack.pop() clear_request = len(self._cv_tokens) == 1
clear_request = False
try: try:
if not self._implicit_app_ctx_stack: if clear_request:
if exc is _sentinel: if exc is _sentinel:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
self.app.do_teardown_request(exc) self.app.do_teardown_request(exc)
@ -406,36 +404,23 @@ class RequestContext:
request_close = getattr(self.request, "close", None) request_close = getattr(self.request, "close", None)
if request_close is not None: if request_close is not None:
request_close() request_close()
clear_request = True
finally: 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 # get rid of circular dependencies at the end of the request
# so that we don't require the GC to be active. # so that we don't require the GC to be active.
if clear_request: 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: if app_ctx is not None:
app_ctx.pop(exc) app_ctx.pop(exc)
assert ( if ctx is not self:
rv is self raise AssertionError(
), f"Popped wrong request context. ({rv!r} instead of {self!r})" f"Popped wrong request context. ({ctx!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)
def __enter__(self) -> "RequestContext": def __enter__(self) -> "RequestContext":
self.push() self.push()

View file

@ -1,7 +1,7 @@
import typing as t import typing as t
from contextvars import ContextVar from contextvars import ContextVar
from werkzeug.local import LocalStack from werkzeug.local import LocalProxy
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING: # pragma: no cover
from .app import Flask from .app import Flask
@ -11,6 +11,39 @@ if t.TYPE_CHECKING: # pragma: no cover
from .sessions import SessionMixin from .sessions import SessionMixin
from .wrappers import Request 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 = """\ _no_app_msg = """\
Working outside of application context. 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 the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.\ with app.app_context(). See the documentation for more information.\
""" """
_cv_app: ContextVar[t.List["AppContext"]] = ContextVar("flask.app_ctx") _cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx")
_app_ctx_stack: LocalStack["AppContext"] = LocalStack(_cv_app) __app_ctx_stack = _FakeStack("app", _cv_app)
app_ctx: "AppContext" = _app_ctx_stack( # type: ignore[assignment] app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment]
unbound_message=_no_app_msg _cv_app, unbound_message=_no_app_msg
) )
current_app: "Flask" = _app_ctx_stack( # type: ignore[assignment] current_app: "Flask" = LocalProxy( # type: ignore[assignment]
"app", unbound_message=_no_app_msg _cv_app, "app", unbound_message=_no_app_msg
) )
g: "_AppCtxGlobals" = _app_ctx_stack( # type: ignore[assignment] g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment]
"g", unbound_message=_no_app_msg _cv_app, "g", unbound_message=_no_app_msg
) )
_no_req_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 an active HTTP request. Consult the documentation on testing for
information about how to avoid this problem.\ information about how to avoid this problem.\
""" """
_cv_req: ContextVar[t.List["RequestContext"]] = ContextVar("flask.request_ctx") _cv_req: ContextVar["RequestContext"] = ContextVar("flask.request_ctx")
_request_ctx_stack: LocalStack["RequestContext"] = LocalStack(_cv_req) __request_ctx_stack = _FakeStack("request", _cv_req)
request_ctx: "RequestContext" = _request_ctx_stack( # type: ignore[assignment] request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment]
unbound_message=_no_req_msg _cv_req, unbound_message=_no_req_msg
) )
request: "Request" = _request_ctx_stack( # type: ignore[assignment] request: "Request" = LocalProxy( # type: ignore[assignment]
"request", unbound_message=_no_req_msg _cv_req, "request", unbound_message=_no_req_msg
) )
session: "SessionMixin" = _request_ctx_stack( # type: ignore[assignment] session: "SessionMixin" = LocalProxy( # type: ignore[assignment]
"session", unbound_message=_no_req_msg _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)

View file

@ -11,7 +11,7 @@ from werkzeug.urls import url_parse
from werkzeug.wrappers import Request as BaseRequest from werkzeug.wrappers import Request as BaseRequest
from .cli import ScriptInfo from .cli import ScriptInfo
from .globals import _request_ctx_stack from .globals import _cv_req
from .json import dumps as json_dumps from .json import dumps as json_dumps
from .sessions import SessionMixin from .sessions import SessionMixin
@ -147,7 +147,7 @@ class FlaskClient(Client):
app = self.application app = self.application
environ_overrides = kwargs.setdefault("environ_overrides", {}) environ_overrides = kwargs.setdefault("environ_overrides", {})
self.cookie_jar.inject_wsgi(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: with app.test_request_context(*args, **kwargs) as c:
session_interface = app.session_interface session_interface = app.session_interface
sess = session_interface.open_session(app, c.request) 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 # behavior. It's important to not use the push and pop
# methods of the actual request context object since that would # methods of the actual request context object since that would
# mean that cleanup handlers are called # mean that cleanup handlers are called
_request_ctx_stack.push(outer_reqctx) token = _cv_req.set(outer_reqctx)
try: try:
yield sess yield sess
finally: finally:
_request_ctx_stack.pop() _cv_req.reset(token)
resp = app.response_class() resp = app.response_class()
if not session_interface.is_null_session(sess): if not session_interface.is_null_session(sess):