Merge app and request contexts into a single context (#5639)

This commit is contained in:
ascender1729 2025-03-30 12:59:46 +05:30
parent f61172b8dd
commit aa37c0a145
3 changed files with 130 additions and 315 deletions

View file

@ -28,10 +28,8 @@ from werkzeug.wsgi import get_host
from . import cli from . import cli
from . import typing as ft from . import typing as ft
from .ctx import AppContext from .ctx import ExecutionContext
from .ctx import RequestContext from .globals import _cv_execution
from .globals import _cv_app
from .globals import _cv_request
from .globals import current_app from .globals import current_app
from .globals import g from .globals import g
from .globals import request from .globals import request
@ -1057,7 +1055,7 @@ class Flask(App):
.. versionadded:: 2.2 .. versionadded:: 2.2
Moved from ``flask.url_for``, which calls this method. Moved from ``flask.url_for``, which calls this method.
""" """
req_ctx = _cv_request.get(None) req_ctx = _cv_execution.get(None)
if req_ctx is not None: if req_ctx is not None:
url_adapter = req_ctx.url_adapter url_adapter = req_ctx.url_adapter
@ -1076,7 +1074,7 @@ class Flask(App):
if _external is None: if _external is None:
_external = _scheme is not None _external = _scheme is not None
else: else:
app_ctx = _cv_app.get(None) app_ctx = _cv_execution.get(None)
# If called by helpers.url_for, an app context is active, # If called by helpers.url_for, an app context is active,
# use its url_adapter. Otherwise, app.url_for was called # use its url_adapter. Otherwise, app.url_for was called
@ -1371,7 +1369,7 @@ class Flask(App):
:data:`appcontext_tearing_down` signal is sent. :data:`appcontext_tearing_down` signal is sent.
This is called by This is called by
:meth:`AppContext.pop() <flask.ctx.AppContext.pop>`. :meth:`ExecutionContext.pop() <flask.ctx.ExecutionContext.pop>`.
.. versionadded:: 0.9 .. versionadded:: 0.9
""" """
@ -1383,98 +1381,50 @@ class Flask(App):
appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
def app_context(self) -> AppContext: def app_context(self) -> ExecutionContext:
"""Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` """Create an :class:`~flask.ctx.ExecutionContext`. This is typically
block to push the context, which will make :data:`current_app` used with the :keyword:`with` statement to set up a context, but
point at this application. the functions that do this can also be used directly.
An application context is automatically pushed by .. versionchanged:: 3.0
:meth:`RequestContext.push() <flask.ctx.RequestContext.push>` The context is now created using the new unified execution context.
when handling a request, and when running a CLI command. Use
this to manually create a context outside of these situations.
::
with app.app_context():
init_db()
See :doc:`/appcontext`.
.. versionadded:: 0.9
""" """
return AppContext(self) return ExecutionContext(self)
def request_context(self, environ: WSGIEnvironment) -> RequestContext: def request_context(self, environ: WSGIEnvironment) -> ExecutionContext:
"""Create a :class:`~flask.ctx.RequestContext` representing a """Create a :class:`~flask.ctx.ExecutionContext` for a WSGI
WSGI environment. Use a ``with`` block to push the context, environment. Use a :class:`~werkzeug.test.EnvironBuilder` to create
which will make :data:`request` point at this request. such an environment. See the :doc:`/testing` guide for more
information.
See :doc:`/reqcontext`. The request and app contexts are now combined into a single execution context.
Typically you should not call this from your own code. A request
context is automatically pushed by the :meth:`wsgi_app` when
handling a request. Use :meth:`test_request_context` to create
an environment and context instead of this method.
:param environ: a WSGI environment
""" """
return RequestContext(self, environ) return ExecutionContext(self, environ=environ)
def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> ExecutionContext:
"""Create a :class:`~flask.ctx.RequestContext` for a WSGI """Create a :class:`~flask.ctx.ExecutionContext` for a test request.
environment created from the given values. This is mostly useful Use a :class:`~werkzeug.test.EnvironBuilder` to create such an
during testing, where you may want to run a function that uses environment. See the :doc:`/testing` guide for more information.
request data without dispatching a full request.
See :doc:`/reqcontext`. The request and app contexts are now combined into a single execution context.
Use a ``with`` block to push the context, which will make
:data:`request` point at the request for the created
environment. ::
with app.test_request_context(...):
generate_report()
When using the shell, it may be easier to push and pop the
context manually to avoid indentation. ::
ctx = app.test_request_context(...)
ctx.push()
...
ctx.pop()
Takes the same arguments as Werkzeug's
:class:`~werkzeug.test.EnvironBuilder`, with some defaults from
the application. See the linked Werkzeug docs for most of the
available arguments. Flask-specific behavior is listed here.
:param path: URL path being requested.
:param base_url: Base URL where the app is being served, which
``path`` is relative to. If not given, built from
:data:`PREFERRED_URL_SCHEME`, ``subdomain``,
:data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`.
:param subdomain: Subdomain name to append to
:data:`SERVER_NAME`.
:param url_scheme: Scheme to use instead of
:data:`PREFERRED_URL_SCHEME`.
:param data: The request body, either as a string or a dict of
form keys and values.
:param json: If given, this is serialized as JSON and passed as
``data``. Also defaults ``content_type`` to
``application/json``.
:param args: other positional arguments passed to
:class:`~werkzeug.test.EnvironBuilder`.
:param kwargs: other keyword arguments passed to
:class:`~werkzeug.test.EnvironBuilder`.
""" """
from .testing import EnvironBuilder environ = {
"HTTP_HOST": "localhost",
builder = EnvironBuilder(self, *args, **kwargs) "PATH_INFO": "/",
"REQUEST_METHOD": "GET",
try: "SERVER_NAME": "localhost",
return self.request_context(builder.get_environ()) "SERVER_PORT": "80",
finally: "SERVER_PROTOCOL": "HTTP/1.1",
builder.close() "wsgi.errors": sys.stderr,
"wsgi.input": sys.stdin,
"wsgi.multiprocess": False,
"wsgi.multithread": False,
"wsgi.run_once": False,
"wsgi.url_scheme": "http",
"wsgi.version": (1, 0),
}
environ.update(*args, **kwargs)
return self.request_context(environ)
def wsgi_app( def wsgi_app(
self, environ: WSGIEnvironment, start_response: StartResponse self, environ: WSGIEnvironment, start_response: StartResponse
@ -1518,8 +1468,7 @@ class Flask(App):
return response(environ, start_response) return response(environ, start_response)
finally: finally:
if "werkzeug.debug.preserve_context" in environ: if "werkzeug.debug.preserve_context" in environ:
environ["werkzeug.debug.preserve_context"](_cv_app.get()) environ["werkzeug.debug.preserve_context"](_cv_execution.get())
environ["werkzeug.debug.preserve_context"](_cv_request.get())
if error is not None and self.should_ignore_error(error): if error is not None and self.should_ignore_error(error):
error = None error = None

View file

@ -5,6 +5,8 @@ import sys
import typing as t import typing as t
from functools import update_wrapper from functools import update_wrapper
from types import TracebackType from types import TracebackType
import warnings
from typing import Optional, Union, cast
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
@ -13,6 +15,7 @@ from .globals import _cv_app
from .globals import _cv_request from .globals import _cv_request
from .signals import appcontext_popped from .signals import appcontext_popped
from .signals import appcontext_pushed from .signals import appcontext_pushed
from .signals import request_tearing_down
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING: # pragma: no cover
from _typeshed.wsgi import WSGIEnvironment from _typeshed.wsgi import WSGIEnvironment
@ -25,6 +28,9 @@ if t.TYPE_CHECKING: # pragma: no cover
# a singleton sentinel value for parameter defaults # a singleton sentinel value for parameter defaults
_sentinel = object() _sentinel = object()
# Context variable for the new unified context
_cv_execution = contextvars.ContextVar[Optional["ExecutionContext"]]("flask.execution_ctx")
class _AppCtxGlobals: class _AppCtxGlobals:
"""A plain object. Used as a namespace for storing data during an """A plain object. Used as a namespace for storing data during an
@ -194,243 +200,108 @@ def copy_current_request_context(f: F) -> F:
def has_request_context() -> bool: def has_request_context() -> bool:
"""If you have code that wants to test if a request context is there or """Deprecated. Use has_execution_context() instead."""
not this function can be used. For instance, you may want to take advantage warnings.warn(
of request information if the request object is available, but fail "'has_request_context' is deprecated and will be removed in Flask 4.0. "
silently if it is unavailable. "Use 'has_execution_context' instead.",
DeprecationWarning,
:: stacklevel=2,
)
class User(db.Model): return has_execution_context()
def __init__(self, username, remote_addr=None):
self.username = username
if remote_addr is None and has_request_context():
remote_addr = request.remote_addr
self.remote_addr = remote_addr
Alternatively you can also just test any of the context bound objects
(such as :class:`request` or :class:`g`) for truthness::
class User(db.Model):
def __init__(self, username, remote_addr=None):
self.username = username
if remote_addr is None and request:
remote_addr = request.remote_addr
self.remote_addr = remote_addr
.. versionadded:: 0.7
"""
return _cv_request.get(None) is not None
def has_app_context() -> bool: def has_app_context() -> bool:
"""Works like :func:`has_request_context` but for the application """Deprecated. Use has_execution_context() instead."""
context. You can also just do a boolean check on the warnings.warn(
:data:`current_app` object instead. "'has_app_context' is deprecated and will be removed in Flask 4.0. "
"Use 'has_execution_context' instead.",
.. versionadded:: 0.9 DeprecationWarning,
""" stacklevel=2,
return _cv_app.get(None) is not None )
return has_execution_context()
class AppContext: def has_execution_context() -> bool:
"""The app context contains application-specific information. An app """Check if an execution context is active."""
context is created and pushed at the beginning of each request if return _cv_execution.get(None) is not None
one is not already active. An app context is also pushed when
running CLI commands.
"""
def __init__(self, app: Flask) -> None:
self.app = app
self.url_adapter = app.create_url_adapter(None)
self.g: _AppCtxGlobals = app.app_ctx_globals_class()
self._cv_tokens: list[contextvars.Token[AppContext]] = []
def push(self) -> None:
"""Binds the app context to the current context."""
self._cv_tokens.append(_cv_app.set(self))
appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync)
def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
"""Pops the app context."""
try:
if len(self._cv_tokens) == 1:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_appcontext(exc)
finally:
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, _async_wrapper=self.app.ensure_sync)
def __enter__(self) -> AppContext:
self.push()
return self
def __exit__(
self,
exc_type: type | None,
exc_value: BaseException | None,
tb: TracebackType | None,
) -> None:
self.pop(exc_value)
class RequestContext: class ExecutionContext:
"""The request context contains per-request information. The Flask """A unified context that combines application and request contexts.
app creates and pushes it at the beginning of the request, then pops This is the new way to handle context in Flask, replacing the separate
it at the end of the request. It will create the URL adapter and app and request contexts.
request object for the WSGI environment provided.
Do not attempt to use this class directly, instead use
:meth:`~flask.Flask.test_request_context` and
:meth:`~flask.Flask.request_context` to create this object.
When the request context is popped, it will evaluate all the
functions registered on the application for teardown execution
(:meth:`~flask.Flask.teardown_request`).
The request context is automatically popped at the end of the
request. When using the interactive debugger, the context will be
restored so ``request`` is still accessible. Similarly, the test
client can preserve the context after the request ends. However,
teardown functions may already have closed some resources such as
database connections.
""" """
def __init__( def __init__(
self, self,
app: Flask, app: Flask,
environ: WSGIEnvironment, environ: WSGIEnvironment | None = None,
request: Request | None = None, request: Request | None = None,
session: SessionMixin | None = None, session: SessionMixin | None = None,
) -> None: ) -> None:
self.app = app self.app = app
if request is None: self.url_adapter = app.create_url_adapter(None)
request = app.request_class(environ) self.g: _AppCtxGlobals = app.app_ctx_globals_class()
request.json_module = app.json self._cv_token: Optional[contextvars.Token[Optional[ExecutionContext]]] = None
self.request: Request = request
self.url_adapter = None # Request-specific attributes
try: self.environ = environ
self.url_adapter = app.create_url_adapter(self.request) self.request = request
except HTTPException as e: self.session = session
self.request.routing_exception = e
self.flashes: list[tuple[str, str]] | None = None
self.session: SessionMixin | None = session
# 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: list[ft.AfterRequestCallable[t.Any]] = [] self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = []
self._cv_tokens: list[ # Track if this is a request context
tuple[contextvars.Token[RequestContext], AppContext | None] self._is_request_context = request is not None
] = []
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.
Because the actual request object is the same this cannot be used to
move a request context to a different thread unless access to the
request object is locked.
.. versionadded:: 0.10
.. versionchanged:: 1.1
The current session object is used instead of reloading the original
data. This prevents `flask.session` pointing to an out-of-date object.
"""
return self.__class__(
self.app,
environ=self.request.environ,
request=self.request,
session=self.session,
)
def match_request(self) -> None:
"""Can be overridden by a subclass to hook into the matching
of the request.
"""
try:
result = self.url_adapter.match(return_rule=True) # type: ignore
self.request.url_rule, self.request.view_args = result # type: ignore
except HTTPException as e:
self.request.routing_exception = e
def push(self) -> None: def push(self) -> None:
# Before we push the request context we have to ensure that there """Push this context to the stack."""
# is an application context. if self._cv_token is not None:
app_ctx = _cv_app.get(None) raise RuntimeError("Context is already pushed")
if app_ctx is None or app_ctx.app is not self.app: self._cv_token = _cv_execution.set(self)
app_ctx = self.app.app_context()
app_ctx.push() if self._is_request_context:
# For backward compatibility, also set the old context vars
_cv_app.set(self)
_cv_request.set(self)
appcontext_pushed.send(self.app)
else: else:
app_ctx = None _cv_app.set(self)
self._cv_tokens.append((_cv_request.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.
# Only open a new session if this is the first time the request was
# pushed, otherwise stream_with_context loses the session.
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
# Match the request URL after loading the session, so that the
# session is available in custom URL converters.
if self.url_adapter is not None:
self.match_request()
def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
"""Pops the request context and unbinds it by doing that. This will """Pop this context from the stack."""
also trigger the execution of functions registered by the if self._cv_token is None:
:meth:`~flask.Flask.teardown_request` decorator. raise RuntimeError("Context is not pushed")
.. versionchanged:: 0.9 if exc is _sentinel:
Added the `exc` argument. exc = sys.exc_info()[1]
"""
clear_request = len(self._cv_tokens) == 1
try: if self._is_request_context and self.request is not None:
if clear_request: # Run request teardown functions
if exc is _sentinel: for func in reversed(self._after_request_functions):
exc = sys.exc_info()[1] if hasattr(self.request, 'response'):
self.app.do_teardown_request(exc) self.app.ensure_sync(func)(self.request.response)
request_tearing_down.send(self.app, exc=exc)
request_close = getattr(self.request, "close", None) # Run app teardown functions
if request_close is not None: for func in reversed(self.app.teardown_appcontext_funcs):
request_close() self.app.ensure_sync(func)(exc)
finally:
ctx = _cv_request.get()
token, app_ctx = self._cv_tokens.pop()
_cv_request.reset(token)
# get rid of circular dependencies at the end of the request appcontext_popped.send(self.app)
# so that we don't require the GC to be active.
if clear_request:
ctx.request.environ["werkzeug.request"] = None
if app_ctx is not None: # Reset context vars
app_ctx.pop(exc) if self._cv_token is not None:
token = cast(contextvars.Token[Optional[ExecutionContext]], self._cv_token)
_cv_execution.reset(token)
self._cv_token = None
if ctx is not self: if self._is_request_context:
raise AssertionError( _cv_app.reset(token)
f"Popped wrong request context. ({ctx!r} instead of {self!r})" _cv_request.reset(token)
) else:
_cv_app.reset(token)
def __enter__(self) -> RequestContext: def __enter__(self) -> ExecutionContext:
self.push() self.push()
return self return self
@ -443,7 +314,4 @@ class RequestContext:
self.pop(exc_value) self.pop(exc_value)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return f"<ExecutionContext {self.app.name!r}>"
f"<{type(self).__name__} {self.request.url!r}"
f" [{self.request.method}] of {self.app.name}>"
)

View file

@ -8,8 +8,7 @@ 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
from .ctx import _AppCtxGlobals from .ctx import _AppCtxGlobals
from .ctx import AppContext from .ctx import ExecutionContext
from .ctx import RequestContext
from .sessions import SessionMixin from .sessions import SessionMixin
from .wrappers import Request from .wrappers import Request
@ -21,15 +20,15 @@ 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[AppContext] = ContextVar("flask.app_ctx") _cv_execution: ContextVar[ExecutionContext] = ContextVar("flask.execution_ctx")
app_ctx: AppContext = LocalProxy( # type: ignore[assignment] execution_ctx: ExecutionContext = LocalProxy( # type: ignore[assignment]
_cv_app, unbound_message=_no_app_msg _cv_execution, unbound_message=_no_app_msg
) )
current_app: Flask = LocalProxy( # type: ignore[assignment] current_app: Flask = LocalProxy( # type: ignore[assignment]
_cv_app, "app", unbound_message=_no_app_msg _cv_execution, "app", unbound_message=_no_app_msg
) )
g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment]
_cv_app, "g", unbound_message=_no_app_msg _cv_execution, "g", unbound_message=_no_app_msg
) )
_no_req_msg = """\ _no_req_msg = """\
@ -39,13 +38,12 @@ 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_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") request_ctx: ExecutionContext = LocalProxy( # type: ignore[assignment]
request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] _cv_execution, unbound_message=_no_req_msg
_cv_request, unbound_message=_no_req_msg
) )
request: Request = LocalProxy( # type: ignore[assignment] request: Request = LocalProxy( # type: ignore[assignment]
_cv_request, "request", unbound_message=_no_req_msg _cv_execution, "request", unbound_message=_no_req_msg
) )
session: SessionMixin = LocalProxy( # type: ignore[assignment] session: SessionMixin = LocalProxy( # type: ignore[assignment]
_cv_request, "session", unbound_message=_no_req_msg _cv_execution, "session", unbound_message=_no_req_msg
) )