forked from orbit-oss/flask
contexts no longer use LocalStack
This commit is contained in:
parent
0b2f809f9b
commit
d597db67de
5 changed files with 146 additions and 71 deletions
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue