Merge pull request #4682 from pallets/refactor-context-stack

remove use of `LocalStack`
This commit is contained in:
David Lord 2022-07-08 11:58:44 -07:00 committed by GitHub
commit cbebdae698
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 351 additions and 352 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

@ -311,56 +311,28 @@ Useful Internals
.. autoclass:: flask.ctx.RequestContext .. autoclass:: flask.ctx.RequestContext
:members: :members:
.. data:: _request_ctx_stack .. data:: flask.globals.request_ctx
The internal :class:`~werkzeug.local.LocalStack` that holds The current :class:`~flask.ctx.RequestContext`. If a request context
:class:`~flask.ctx.RequestContext` instances. Typically, the is not active, accessing attributes on this proxy will raise a
:data:`request` and :data:`session` proxies should be accessed ``RuntimeError``.
instead of the stack. It may be useful to access the stack in
extension code.
The following attributes are always present on each layer of the This is an internal object that is essential to how Flask handles
stack: requests. Accessing this should not be needed in most cases. Most
likely you want :data:`request` and :data:`session` instead.
`app`
the active Flask application.
`url_adapter`
the URL adapter that was used to match the request.
`request`
the current request object.
`session`
the active session object.
`g`
an object with all the attributes of the :data:`flask.g` object.
`flashes`
an internal cache for the flashed messages.
Example usage::
from flask import _request_ctx_stack
def get_session():
ctx = _request_ctx_stack.top
if ctx is not None:
return ctx.session
.. autoclass:: flask.ctx.AppContext .. autoclass:: flask.ctx.AppContext
:members: :members:
.. data:: _app_ctx_stack .. data:: flask.globals.app_ctx
The internal :class:`~werkzeug.local.LocalStack` that holds The current :class:`~flask.ctx.AppContext`. If an app context is not
:class:`~flask.ctx.AppContext` instances. Typically, the active, accessing attributes on this proxy will raise a
:data:`current_app` and :data:`g` proxies should be accessed instead ``RuntimeError``.
of the stack. Extensions can access the contexts on the stack as a
namespace to store data.
.. versionadded:: 0.9 This is an internal object that is essential to how Flask handles
requests. Accessing this should not be needed in most cases. Most
likely you want :data:`current_app` and :data:`g` instead.
.. autoclass:: flask.blueprints.BlueprintSetupState .. autoclass:: flask.blueprints.BlueprintSetupState
:members: :members:

View file

@ -136,14 +136,6 @@ local from ``get_db()``::
Accessing ``db`` will call ``get_db`` internally, in the same way that Accessing ``db`` will call ``get_db`` internally, in the same way that
:data:`current_app` works. :data:`current_app` works.
----
If you're writing an extension, :data:`g` should be reserved for user
code. You may store internal data on the context itself, but be sure to
use a sufficiently unique name. The current context is accessed with
:data:`_app_ctx_stack.top <_app_ctx_stack>`. For more information see
:doc:`/extensiondev`.
Events and Signals Events and Signals
------------------ ------------------

View file

@ -187,12 +187,6 @@ when the application context ends. If it should only be valid during a
request, or would not be used in the CLI outside a reqeust, use request, or would not be used in the CLI outside a reqeust, use
:meth:`~flask.Flask.teardown_request`. :meth:`~flask.Flask.teardown_request`.
An older technique for storing context data was to store it on
``_app_ctx_stack.top`` or ``_request_ctx_stack.top``. However, this just
moves the same namespace collision problem elsewhere (although less
likely) and modifies objects that are very internal to Flask's
operations. Prefer storing data under a unique name in ``g``.
Views and Models Views and Models
---------------- ----------------

View file

@ -30,10 +30,6 @@ or create an application context itself. At that point the ``get_db``
function can be used to get the current database connection. Whenever the function can be used to get the current database connection. Whenever the
context is destroyed the database connection will be terminated. context is destroyed the database connection will be terminated.
Note: if you use Flask 0.9 or older you need to use
``flask._app_ctx_stack.top`` instead of ``g`` as the :data:`flask.g`
object was bound to the request and not application context.
Example:: Example::
@app.route('/') @app.route('/')

View file

@ -37,12 +37,14 @@ context, which also pushes an :doc:`app context </appcontext>`. When the
request ends it pops the request context then the application context. request ends it pops the request context then the application context.
The context is unique to each thread (or other worker type). The context is unique to each thread (or other worker type).
:data:`request` cannot be passed to another thread, the other thread :data:`request` cannot be passed to another thread, the other thread has
will have a different context stack and will not know about the request a different context space and will not know about the request the parent
the parent thread was pointing to. thread was pointing to.
Context locals are implemented in Werkzeug. See :doc:`werkzeug:local` Context locals are implemented using Python's :mod:`contextvars` and
for more information on how this works internally. Werkzeug's :class:`~werkzeug.local.LocalProxy`. Python manages the
lifetime of context vars automatically, and local proxy wraps that
low-level interface to make the data easier to work with.
Manually Push a Context Manually Push a Context
@ -87,10 +89,9 @@ How the Context Works
The :meth:`Flask.wsgi_app` method is called to handle each request. It The :meth:`Flask.wsgi_app` method is called to handle each request. It
manages the contexts during the request. Internally, the request and manages the contexts during the request. Internally, the request and
application contexts work as stacks, :data:`_request_ctx_stack` and application contexts work like stacks. When contexts are pushed, the
:data:`_app_ctx_stack`. When contexts are pushed onto the stack, the
proxies that depend on them are available and point at information from proxies that depend on them are available and point at information from
the top context on the stack. the top item.
When the request starts, a :class:`~ctx.RequestContext` is created and When the request starts, a :class:`~ctx.RequestContext` is created and
pushed, which creates and pushes an :class:`~ctx.AppContext` first if pushed, which creates and pushes an :class:`~ctx.AppContext` first if
@ -99,10 +100,10 @@ these contexts are pushed, the :data:`current_app`, :data:`g`,
:data:`request`, and :data:`session` proxies are available to the :data:`request`, and :data:`session` proxies are available to the
original thread handling the request. original thread handling the request.
Because the contexts are stacks, other contexts may be pushed to change Other contexts may be pushed to change the proxies during a request.
the proxies during a request. While this is not a common pattern, it While this is not a common pattern, it can be used in advanced
can be used in advanced applications to, for example, do internal applications to, for example, do internal redirects or chain different
redirects or chain different applications together. applications together.
After the request is dispatched and a response is generated and sent, After the request is dispatched and a response is generated and sent,
the request context is popped, which then pops the application context. the request context is popped, which then pops the application context.

View file

@ -4,7 +4,7 @@ from setuptools import setup
setup( setup(
name="Flask", name="Flask",
install_requires=[ install_requires=[
"Werkzeug >= 2.0", "Werkzeug >= 2.2.0a1",
"Jinja2 >= 3.0", "Jinja2 >= 3.0",
"itsdangerous >= 2.0", "itsdangerous >= 2.0",
"click >= 8.0", "click >= 8.0",

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

@ -38,10 +38,11 @@ from .config import ConfigAttribute
from .ctx import _AppCtxGlobals from .ctx import _AppCtxGlobals
from .ctx import AppContext from .ctx import AppContext
from .ctx import RequestContext from .ctx import RequestContext
from .globals import _app_ctx_stack from .globals import _cv_app
from .globals import _request_ctx_stack from .globals import _cv_req
from .globals import g from .globals import g
from .globals import request from .globals import request
from .globals import request_ctx
from .globals import session from .globals import session
from .helpers import _split_blueprint_path from .helpers import _split_blueprint_path
from .helpers import get_debug_flag from .helpers import get_debug_flag
@ -1283,29 +1284,30 @@ class Flask(Scaffold):
@setupmethod @setupmethod
def teardown_appcontext(self, f: T_teardown) -> T_teardown: def teardown_appcontext(self, f: T_teardown) -> T_teardown:
"""Registers a function to be called when the application context """Registers a function to be called when the application
ends. These functions are typically also called when the request context is popped. The application context is typically popped
context is popped. after the request context for each request, at the end of CLI
commands, or after a manually pushed context ends.
Example:: .. code-block:: python
ctx = app.app_context() with app.app_context():
ctx.push() ...
...
ctx.pop()
When ``ctx.pop()`` is executed in the above example, the teardown When the ``with`` block exits (or ``ctx.pop()`` is called), the
functions are called just before the app context moves from the teardown functions are called just before the app context is
stack of active contexts. This becomes relevant if you are using made inactive. Since a request context typically also manages an
such constructs in tests. application context it would also be called when you pop a
request context.
Since a request context typically also manages an application When a teardown function was called because of an unhandled
context it would also be called when you pop a request context. exception it will be passed an error object. If an
:meth:`errorhandler` is registered, it will handle the exception
and the teardown will not receive it.
When a teardown function was called because of an unhandled exception Teardown functions must avoid raising exceptions. If they
it will be passed an error object. If an :meth:`errorhandler` is execute code that might fail they must surround that code with a
registered, it will handle the exception and the teardown will not ``try``/``except`` block and log any errors.
receive it.
The return values of teardown functions are ignored. The return values of teardown functions are ignored.
@ -1554,10 +1556,10 @@ class Flask(Scaffold):
This no longer does the exception handling, this code was This no longer does the exception handling, this code was
moved to the new :meth:`full_dispatch_request`. 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: if req.routing_exception is not None:
self.raise_routing_exception(req) 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 # if we provide automatic options for this URL and the
# request came with the OPTIONS method, reply automatically # request came with the OPTIONS method, reply automatically
if ( if (
@ -1566,7 +1568,8 @@ class Flask(Scaffold):
): ):
return self.make_default_options_response() return self.make_default_options_response()
# otherwise dispatch to the handler for that endpoint # 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: def full_dispatch_request(self) -> Response:
"""Dispatches the request and on top of that performs request """Dispatches the request and on top of that performs request
@ -1631,8 +1634,8 @@ class Flask(Scaffold):
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
adapter = _request_ctx_stack.top.url_adapter adapter = request_ctx.url_adapter
methods = adapter.allowed_methods() methods = adapter.allowed_methods() # type: ignore[union-attr]
rv = self.response_class() rv = self.response_class()
rv.allow.update(methods) rv.allow.update(methods)
return rv return rv
@ -1740,7 +1743,7 @@ class Flask(Scaffold):
.. 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 = _request_ctx_stack.top req_ctx = _cv_req.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
@ -1759,7 +1762,7 @@ class Flask(Scaffold):
if _external is None: if _external is None:
_external = _scheme is not None _external = _scheme is not None
else: else:
app_ctx = _app_ctx_stack.top app_ctx = _cv_app.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
@ -1790,7 +1793,7 @@ class Flask(Scaffold):
self.inject_url_defaults(endpoint, values) self.inject_url_defaults(endpoint, values)
try: try:
rv = url_adapter.build( rv = url_adapter.build( # type: ignore[union-attr]
endpoint, endpoint,
values, values,
method=_method, method=_method,
@ -2099,7 +2102,7 @@ class Flask(Scaffold):
:return: a new response object or the same, has to be an :return: a new response object or the same, has to be an
instance of :attr:`response_class`. 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: for func in ctx._after_request_functions:
response = self.ensure_sync(func)(response) response = self.ensure_sync(func)(response)
@ -2305,8 +2308,8 @@ class Flask(Scaffold):
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"](_app_ctx_stack.top) environ["werkzeug.debug.preserve_context"](_cv_app.get())
environ["werkzeug.debug.preserve_context"](_request_ctx_stack.top) environ["werkzeug.debug.preserve_context"](_cv_req.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

@ -1051,13 +1051,11 @@ def shell_command() -> None:
without having to manually configure the application. without having to manually configure the application.
""" """
import code import code
from .globals import _app_ctx_stack
app = _app_ctx_stack.top.app
banner = ( banner = (
f"Python {sys.version} on {sys.platform}\n" f"Python {sys.version} on {sys.platform}\n"
f"App: {app.import_name} [{app.env}]\n" f"App: {current_app.import_name} [{current_app.env}]\n"
f"Instance: {app.instance_path}" f"Instance: {current_app.instance_path}"
) )
ctx: dict = {} ctx: dict = {}
@ -1068,7 +1066,7 @@ def shell_command() -> None:
with open(startup) as f: with open(startup) as f:
eval(compile(f.read(), startup, "exec"), ctx) 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 # Site, customize, or startup script can set a hook to call when
# entering interactive mode. The default one sets up readline with # entering interactive mode. The default one sets up readline with

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
@ -6,8 +7,8 @@ from types import TracebackType
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from . import typing as ft from . import typing as ft
from .globals import _app_ctx_stack from .globals import _cv_app
from .globals import _request_ctx_stack from .globals import _cv_req
from .signals import appcontext_popped from .signals import appcontext_popped
from .signals import appcontext_pushed from .signals import appcontext_pushed
@ -103,9 +104,9 @@ class _AppCtxGlobals:
return iter(self.__dict__) return iter(self.__dict__)
def __repr__(self) -> str: def __repr__(self) -> str:
top = _app_ctx_stack.top ctx = _cv_app.get(None)
if top is not None: if ctx is not None:
return f"<flask.g of {top.app.name!r}>" return f"<flask.g of '{ctx.app.name}'>"
return object.__repr__(self) return object.__repr__(self)
@ -130,15 +131,15 @@ def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable:
.. versionadded:: 0.9 .. versionadded:: 0.9
""" """
top = _request_ctx_stack.top ctx = _cv_req.get(None)
if top is None: if ctx is None:
raise RuntimeError( raise RuntimeError(
"This decorator can only be used when a request context is" "'after_this_request' can only be used when a request"
" active, such as within a view function." " context is active, such as in a view function."
) )
top._after_request_functions.append(f) ctx._after_request_functions.append(f)
return f return f
@ -166,19 +167,19 @@ def copy_current_request_context(f: t.Callable) -> t.Callable:
.. versionadded:: 0.10 .. versionadded:: 0.10
""" """
top = _request_ctx_stack.top ctx = _cv_req.get(None)
if top is None: if ctx is None:
raise RuntimeError( raise RuntimeError(
"This decorator can only be used when a request context is" "'copy_current_request_context' can only be used when a"
" active, such as within a view function." " request context is active, such as in a view function."
) )
reqctx = top.copy() ctx = ctx.copy()
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
with reqctx: with ctx:
return reqctx.app.ensure_sync(f)(*args, **kwargs) return ctx.app.ensure_sync(f)(*args, **kwargs)
return update_wrapper(wrapper, f) return update_wrapper(wrapper, f)
@ -212,7 +213,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,44 +223,43 @@ 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:
"""The application context binds an application object implicitly """The app context contains application-specific information. An app
to the current thread or greenlet, similar to how the context is created and pushed at the beginning of each request if
:class:`RequestContext` binds request information. The application one is not already active. An app context is also pushed when
context is also implicitly created if a request context is created running CLI commands.
but the application is not on top of the individual application
context.
""" """
def __init__(self, app: "Flask") -> None: def __init__(self, app: "Flask") -> None:
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: _AppCtxGlobals = 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":
@ -276,10 +276,10 @@ class AppContext:
class RequestContext: class RequestContext:
"""The request context contains all request relevant information. It is """The request context contains per-request information. The Flask
created at the beginning of the request and pushed to the app creates and pushes it at the beginning of the request, then pops
`_request_ctx_stack` and removed at the end of it. It will create the it at the end of the request. It will create the URL adapter and
URL adapter and request object for the WSGI environment provided. request object for the WSGI environment provided.
Do not attempt to use this class directly, instead use Do not attempt to use this class directly, instead use
:meth:`~flask.Flask.test_request_context` and :meth:`~flask.Flask.test_request_context` and
@ -307,26 +307,21 @@ class RequestContext:
self.app = app self.app = app
if request is None: if request is None:
request = app.request_class(environ) request = app.request_class(environ)
self.request = request self.request: Request = request
self.url_adapter = None self.url_adapter = None
try: try:
self.url_adapter = app.create_url_adapter(self.request) self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e: except HTTPException as e:
self.request.routing_exception = e self.request.routing_exception = e
self.flashes = None self.flashes: t.Optional[t.List[t.Tuple[str, str]]] = None
self.session = session self.session: t.Optional["SessionMixin"] = 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 +355,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 +389,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 +400,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

@ -2,7 +2,7 @@ import typing as t
from .app import Flask from .app import Flask
from .blueprints import Blueprint from .blueprints import Blueprint
from .globals import _request_ctx_stack from .globals import request_ctx
class UnexpectedUnicodeError(AssertionError, UnicodeError): class UnexpectedUnicodeError(AssertionError, UnicodeError):
@ -116,9 +116,8 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None:
info = [f"Locating template {template!r}:"] info = [f"Locating template {template!r}:"]
total_found = 0 total_found = 0
blueprint = None blueprint = None
reqctx = _request_ctx_stack.top if request_ctx and request_ctx.request.blueprint is not None:
if reqctx is not None and reqctx.request.blueprint is not None: blueprint = request_ctx.request.blueprint
blueprint = reqctx.request.blueprint
for idx, (loader, srcobj, triple) in enumerate(attempts): for idx, (loader, srcobj, triple) in enumerate(attempts):
if isinstance(srcobj, Flask): if isinstance(srcobj, Flask):

View file

@ -1,59 +1,107 @@
import typing as t import typing as t
from functools import partial from contextvars import ContextVar
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from werkzeug.local import LocalStack
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 RequestContext
from .sessions import SessionMixin from .sessions import SessionMixin
from .wrappers import Request from .wrappers import Request
_request_ctx_err_msg = """\
Working outside of request context.
This typically means that you attempted to use functionality that needed class _FakeStack:
an active HTTP request. Consult the documentation on testing for def __init__(self, name: str, cv: ContextVar[t.Any]) -> None:
information about how to avoid this problem.\ self.name = name
""" self.cv = cv
_app_ctx_err_msg = """\
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. Working outside of application context.
This typically means that you attempted to use functionality that needed This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve the current application. To solve this, set up an application context
this, set up an application context with app.app_context(). See the with app.app_context(). See the documentation for more information.\
documentation for more information.\
""" """
_cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx")
__app_ctx_stack = _FakeStack("app", _cv_app)
def _lookup_req_object(name): app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment]
top = _request_ctx_stack.top _cv_app, unbound_message=_no_app_msg
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return getattr(top, name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app: "Flask" = LocalProxy(_find_app) # type: ignore
request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore
session: "SessionMixin" = LocalProxy( # type: ignore
partial(_lookup_req_object, "session")
) )
g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore current_app: "Flask" = LocalProxy( # type: ignore[assignment]
_cv_app, "app", unbound_message=_no_app_msg
)
g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment]
_cv_app, "g", unbound_message=_no_app_msg
)
_no_req_msg = """\
Working outside of request context.
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["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" = LocalProxy( # type: ignore[assignment]
_cv_req, "request", 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)

View file

@ -12,9 +12,10 @@ import werkzeug.utils
from werkzeug.exceptions import abort as _wz_abort from werkzeug.exceptions import abort as _wz_abort
from werkzeug.utils import redirect as _wz_redirect 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 current_app
from .globals import request from .globals import request
from .globals import request_ctx
from .globals import session from .globals import session
from .signals import message_flashed from .signals import message_flashed
@ -110,11 +111,11 @@ def stream_with_context(
return update_wrapper(decorator, generator_or_function) # type: ignore return update_wrapper(decorator, generator_or_function) # type: ignore
def generator() -> t.Generator: def generator() -> t.Generator:
ctx = _request_ctx_stack.top ctx = _cv_req.get(None)
if ctx is None: if ctx is None:
raise RuntimeError( raise RuntimeError(
"Attempted to stream with context but " "'stream_with_context' can only be used when a request"
"there was no context in the first place to keep around." " context is active, such as in a view function."
) )
with ctx: with ctx:
# Dummy sentinel. Has to be inside the context block or we're # 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 :param category_filter: filter of categories to limit return values. Only
categories in the list will be returned. categories in the list will be returned.
""" """
flashes = _request_ctx_stack.top.flashes flashes = request_ctx.flashes
if flashes is None: if flashes is None:
_request_ctx_stack.top.flashes = flashes = ( flashes = session.pop("_flashes") if "_flashes" in session else []
session.pop("_flashes") if "_flashes" in session else [] request_ctx.flashes = flashes
)
if category_filter: if category_filter:
flashes = list(filter(lambda f: f[0] in category_filter, flashes)) flashes = list(filter(lambda f: f[0] in category_filter, flashes))
if not with_categories: if not with_categories:

View file

@ -574,30 +574,27 @@ class Scaffold:
@setupmethod @setupmethod
def teardown_request(self, f: T_teardown) -> T_teardown: def teardown_request(self, f: T_teardown) -> T_teardown:
"""Register a function to be run at the end of each request, """Register a function to be called when the request context is
regardless of whether there was an exception or not. These functions popped. Typically this happens at the end of each request, but
are executed when the request context is popped, even if not an contexts may be pushed manually as well during testing.
actual request was performed.
Example:: .. code-block:: python
ctx = app.test_request_context() with app.test_request_context():
ctx.push() ...
...
ctx.pop()
When ``ctx.pop()`` is executed in the above example, the teardown When the ``with`` block exits (or ``ctx.pop()`` is called), the
functions are called just before the request context moves from the teardown functions are called just before the request context is
stack of active contexts. This becomes relevant if you are using made inactive.
such constructs in tests.
Teardown functions must avoid raising exceptions. If When a teardown function was called because of an unhandled
they execute code that might fail they exception it will be passed an error object. If an
will have to surround the execution of that code with try/except :meth:`errorhandler` is registered, it will handle the exception
statements and log any errors. and the teardown will not receive it.
When a teardown function was called because of an exception it will Teardown functions must avoid raising exceptions. If they
be passed an error object. execute code that might fail they must surround that code with a
``try``/``except`` block and log any errors.
The return values of teardown functions are ignored. The return values of teardown functions are ignored.
""" """

View file

@ -5,8 +5,8 @@ from jinja2 import Environment as BaseEnvironment
from jinja2 import Template from jinja2 import Template
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
from .globals import _app_ctx_stack from .globals import _cv_app
from .globals import _request_ctx_stack from .globals import _cv_req
from .globals import current_app from .globals import current_app
from .globals import request from .globals import request
from .helpers import stream_with_context 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`, """Default template context processor. Injects `request`,
`session` and `g`. `session` and `g`.
""" """
reqctx = _request_ctx_stack.top appctx = _cv_app.get(None)
appctx = _app_ctx_stack.top reqctx = _cv_req.get(None)
rv = {} rv: t.Dict[str, t.Any] = {}
if appctx is not None: if appctx is not None:
rv["g"] = appctx.g rv["g"] = appctx.g
if reqctx is not None: if reqctx is not None:
@ -124,7 +124,8 @@ class DispatchingJinjaLoader(BaseLoader):
return list(result) 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) before_render_template.send(app, template=template, context=context)
rv = template.render(context) rv = template.render(context)
template_rendered.send(app, template=template, context=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]]], template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]],
**context: t.Any **context: t.Any
) -> str: ) -> str:
"""Renders a template from the template folder with the given """Render a template by name with the given context.
context.
:param template_name_or_list: the name of the template to be :param template_name_or_list: The name of the template to render. If
rendered, or an iterable with template names a list is given, the first name to exist will be rendered.
the first one existing will be rendered :param context: The variables to make available in the template.
:param context: the variables that should be available in the
context of the template.
""" """
ctx = _app_ctx_stack.top app = current_app._get_current_object() # type: ignore[attr-defined]
ctx.app.update_template_context(context) template = app.jinja_env.get_or_select_template(template_name_or_list)
return _render( return _render(app, template, context)
ctx.app.jinja_env.get_or_select_template(template_name_or_list),
context,
ctx.app,
)
def render_template_string(source: str, **context: t.Any) -> str: def render_template_string(source: str, **context: t.Any) -> str:
"""Renders a template from the given template source string """Render a template from the given source string with the given
with the given context. Template variables will be autoescaped. context.
:param source: the source code of the template to be :param source: The source code of the template to render.
rendered :param context: The variables to make available in the template.
:param context: the variables that should be available in the
context of the template.
""" """
ctx = _app_ctx_stack.top app = current_app._get_current_object() # type: ignore[attr-defined]
ctx.app.update_template_context(context) template = app.jinja_env.from_string(source)
return _render(ctx.app.jinja_env.from_string(source), context, ctx.app) return _render(app, template, context)
def _stream( def _stream(

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
@ -94,11 +94,10 @@ class EnvironBuilder(werkzeug.test.EnvironBuilder):
class FlaskClient(Client): class FlaskClient(Client):
"""Works like a regular Werkzeug test client but has some knowledge about """Works like a regular Werkzeug test client but has knowledge about
how Flask works to defer the cleanup of the request context stack to the Flask's contexts to defer the cleanup of the request context until
end of a ``with`` body when used in a ``with`` statement. For general the end of a ``with`` block. For general information about how to
information about how to use this class refer to use this class refer to :class:`werkzeug.test.Client`.
:class:`werkzeug.test.Client`.
.. versionchanged:: 0.12 .. versionchanged:: 0.12
`app.test_client()` includes preset default environment, which can be `app.test_client()` includes preset default environment, which can be
@ -147,7 +146,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 +162,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) # type: ignore[arg-type]
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):

View file

@ -6,8 +6,8 @@ import textwrap
import pytest import pytest
from _pytest import monkeypatch from _pytest import monkeypatch
import flask from flask import Flask
from flask import Flask as _Flask from flask.globals import request_ctx
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
@ -44,14 +44,13 @@ def _reset_os_environ(monkeypatch, _standard_os_environ):
monkeypatch._setitem.extend(_standard_os_environ) monkeypatch._setitem.extend(_standard_os_environ)
class Flask(_Flask):
testing = True
secret_key = "test key"
@pytest.fixture @pytest.fixture
def app(): def app():
app = Flask("flask_test", root_path=os.path.dirname(__file__)) app = Flask("flask_test", root_path=os.path.dirname(__file__))
app.config.update(
TESTING=True,
SECRET_KEY="test key",
)
return app return app
@ -92,8 +91,10 @@ def leak_detector():
# make sure we're not leaking a request context since we are # make sure we're not leaking a request context since we are
# testing flask internally in debug mode in a few cases # testing flask internally in debug mode in a few cases
leaks = [] leaks = []
while flask._request_ctx_stack.top is not None: while request_ctx:
leaks.append(flask._request_ctx_stack.pop()) leaks.append(request_ctx._get_current_object())
request_ctx.pop()
assert leaks == [] assert leaks == []

View file

@ -1,6 +1,8 @@
import pytest import pytest
import flask import flask
from flask.globals import app_ctx
from flask.globals import request_ctx
def test_basic_url_generation(app): 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): def test_request_context_means_app_context(app):
with app.test_request_context(): with app.test_request_context():
assert flask.current_app._get_current_object() == app assert flask.current_app._get_current_object() is app
assert flask._app_ctx_stack.top is None assert not flask.current_app
def test_app_context_provides_current_app(app): def test_app_context_provides_current_app(app):
with app.app_context(): with app.app_context():
assert flask.current_app._get_current_object() == app assert flask.current_app._get_current_object() is app
assert flask._app_ctx_stack.top is None assert not flask.current_app
def test_app_tearing_down(app): def test_app_tearing_down(app):
@ -175,11 +177,11 @@ def test_context_refcounts(app, client):
@app.route("/") @app.route("/")
def index(): def index():
with flask._app_ctx_stack.top: with app_ctx:
with flask._request_ctx_stack.top: with request_ctx:
pass 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 "" return ""
res = client.get("/") res = client.get("/")

View file

@ -1110,14 +1110,10 @@ def test_enctype_debug_helper(app, client):
def index(): def index():
return flask.request.files["foo"].filename return flask.request.files["foo"].filename
# with statement is important because we leave an exception on the with pytest.raises(DebugFilesKeyError) as e:
# stack otherwise and we want to ensure that this is not the case client.post("/fail", data={"foo": "index.txt"})
# to not negatively affect other tests. assert "no file contents were transmitted" in str(e.value)
with client: 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): def test_response_types(app, client):
@ -1548,29 +1544,21 @@ def test_server_name_subdomain():
assert rv.data == b"subdomain" assert rv.data == b"subdomain"
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") @pytest.mark.parametrize("key", ["TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None])
@pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") def test_exception_propagation(app, client, key):
def test_exception_propagation(app, client): app.testing = False
def apprunner(config_key):
@app.route("/")
def index():
1 // 0
if config_key is not None: @app.route("/")
app.config[config_key] = True def index():
with pytest.raises(Exception): 1 // 0
client.get("/")
else:
assert client.get("/").status_code == 500
# we have to run this test in an isolated thread because if the if key is not None:
# debug flag is set to true and an exception happens the context is app.config[key] = True
# not torn down. This causes other tests that run after this fail
# when they expect no exception on the stack. with pytest.raises(ZeroDivisionError):
for config_key in "TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None: client.get("/")
t = Thread(target=apprunner, args=(config_key,)) else:
t.start() assert client.get("/").status_code == 500
t.join()
@pytest.mark.parametrize("debug", [True, False]) @pytest.mark.parametrize("debug", [True, False])

View file

@ -3,6 +3,7 @@ import warnings
import pytest import pytest
import flask import flask
from flask.globals import request_ctx
from flask.sessions import SecureCookieSessionInterface from flask.sessions import SecureCookieSessionInterface
from flask.sessions import SessionInterface from flask.sessions import SessionInterface
@ -116,7 +117,7 @@ def test_context_binding(app):
assert index() == "Hello World!" assert index() == "Hello World!"
with app.test_request_context("/meh"): with app.test_request_context("/meh"):
assert meh() == "http://localhost/meh" assert meh() == "http://localhost/meh"
assert flask._request_ctx_stack.top is None assert not flask.request
def test_context_test(app): def test_context_test(app):
@ -152,7 +153,7 @@ class TestGreenletContextCopying:
@app.route("/") @app.route("/")
def index(): def index():
flask.session["fizz"] = "buzz" flask.session["fizz"] = "buzz"
reqctx = flask._request_ctx_stack.top.copy() reqctx = request_ctx.copy()
def g(): def g():
assert not flask.request assert not flask.request

View file

@ -1,4 +1,5 @@
import flask import flask
from flask.globals import request_ctx
from flask.sessions import SessionInterface from flask.sessions import SessionInterface
@ -13,7 +14,7 @@ def test_open_session_with_endpoint():
pass pass
def open_session(self, app, request): def open_session(self, app, request):
flask._request_ctx_stack.top.match_request() request_ctx.match_request()
assert request.endpoint is not None assert request.endpoint is not None
app = flask.Flask(__name__) app = flask.Flask(__name__)

View file

@ -5,6 +5,7 @@ import werkzeug
import flask import flask
from flask import appcontext_popped from flask import appcontext_popped
from flask.cli import ScriptInfo from flask.cli import ScriptInfo
from flask.globals import _cv_req
from flask.json import jsonify from flask.json import jsonify
from flask.testing import EnvironBuilder from flask.testing import EnvironBuilder
from flask.testing import FlaskCliRunner 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 # close the response, releasing the context held by stream_with_context
rv.close() rv.close()
# only req_ctx fixture should still be pushed # 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