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
removed.
- The app and request contexts are managed using Python context vars
directly rather than Werkzeug's ``LocalStack``. This should result
in better performance and memory use. :pr:`4672`
- Extension maintainers, be aware that ``_app_ctx_stack.top``
and ``_request_ctx_stack.top`` are deprecated. Store data on
``g`` instead using a unique prefix, like
``g._extension_name_attr``.
- Add new customization points to the ``Flask`` app object for many
previously global behaviors.

View file

@ -311,56 +311,28 @@ Useful Internals
.. autoclass:: flask.ctx.RequestContext
:members:
.. data:: _request_ctx_stack
.. data:: flask.globals.request_ctx
The internal :class:`~werkzeug.local.LocalStack` that holds
:class:`~flask.ctx.RequestContext` instances. Typically, the
:data:`request` and :data:`session` proxies should be accessed
instead of the stack. It may be useful to access the stack in
extension code.
The current :class:`~flask.ctx.RequestContext`. If a request context
is not active, accessing attributes on this proxy will raise a
``RuntimeError``.
The following attributes are always present on each layer of the
stack:
`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
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:`request` and :data:`session` instead.
.. autoclass:: flask.ctx.AppContext
:members:
.. data:: _app_ctx_stack
.. data:: flask.globals.app_ctx
The internal :class:`~werkzeug.local.LocalStack` that holds
:class:`~flask.ctx.AppContext` instances. Typically, the
:data:`current_app` and :data:`g` proxies should be accessed instead
of the stack. Extensions can access the contexts on the stack as a
namespace to store data.
The current :class:`~flask.ctx.AppContext`. If an app context is not
active, accessing attributes on this proxy will raise a
``RuntimeError``.
.. 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
:members:

View file

@ -136,14 +136,6 @@ local from ``get_db()``::
Accessing ``db`` will call ``get_db`` internally, in the same way that
: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
------------------

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
: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
----------------

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
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::
@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.
The context is unique to each thread (or other worker type).
:data:`request` cannot be passed to another thread, the other thread
will have a different context stack and will not know about the request
the parent thread was pointing to.
:data:`request` cannot be passed to another thread, the other thread has
a different context space and will not know about the request the parent
thread was pointing to.
Context locals are implemented in Werkzeug. See :doc:`werkzeug:local`
for more information on how this works internally.
Context locals are implemented using Python's :mod:`contextvars` and
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
@ -87,10 +89,9 @@ How the Context Works
The :meth:`Flask.wsgi_app` method is called to handle each request. It
manages the contexts during the request. Internally, the request and
application contexts work as stacks, :data:`_request_ctx_stack` and
:data:`_app_ctx_stack`. When contexts are pushed onto the stack, the
application contexts work like stacks. When contexts are pushed, the
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
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
original thread handling the request.
Because the contexts are stacks, other contexts may be pushed to change
the proxies during a request. While this is not a common pattern, it
can be used in advanced applications to, for example, do internal
redirects or chain different applications together.
Other contexts may be pushed to change the proxies during a request.
While this is not a common pattern, it can be used in advanced
applications to, for example, do internal redirects or chain different
applications together.
After the request is dispatched and a response is generated and sent,
the request context is popped, which then pops the application context.

View file

@ -4,7 +4,7 @@ from setuptools import setup
setup(
name="Flask",
install_requires=[
"Werkzeug >= 2.0",
"Werkzeug >= 2.2.0a1",
"Jinja2 >= 3.0",
"itsdangerous >= 2.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 has_app_context as has_app_context
from .ctx import has_request_context as has_request_context
from .globals import _app_ctx_stack as _app_ctx_stack
from .globals import _request_ctx_stack as _request_ctx_stack
from .globals import current_app as current_app
from .globals import g as g
from .globals import request as request
@ -45,3 +43,29 @@ from .templating import stream_template as stream_template
from .templating import stream_template_string as stream_template_string
__version__ = "2.2.0.dev0"
def __getattr__(name):
if name == "_app_ctx_stack":
import warnings
from .globals import __app_ctx_stack
warnings.warn(
"'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.",
DeprecationWarning,
stacklevel=2,
)
return __app_ctx_stack
if name == "_request_ctx_stack":
import warnings
from .globals import __request_ctx_stack
warnings.warn(
"'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.",
DeprecationWarning,
stacklevel=2,
)
return __request_ctx_stack
raise AttributeError(name)

View file

@ -38,10 +38,11 @@ from .config import ConfigAttribute
from .ctx import _AppCtxGlobals
from .ctx import AppContext
from .ctx import RequestContext
from .globals import _app_ctx_stack
from .globals import _request_ctx_stack
from .globals import _cv_app
from .globals import _cv_req
from .globals import g
from .globals import request
from .globals import request_ctx
from .globals import session
from .helpers import _split_blueprint_path
from .helpers import get_debug_flag
@ -1283,29 +1284,30 @@ class Flask(Scaffold):
@setupmethod
def teardown_appcontext(self, f: T_teardown) -> T_teardown:
"""Registers a function to be called when the application context
ends. These functions are typically also called when the request
context is popped.
"""Registers a function to be called when the application
context is popped. The application context is typically 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()
ctx.push()
...
ctx.pop()
with app.app_context():
...
When ``ctx.pop()`` is executed in the above example, the teardown
functions are called just before the app context moves from the
stack of active contexts. This becomes relevant if you are using
such constructs in tests.
When the ``with`` block exits (or ``ctx.pop()`` is called), the
teardown functions are called just before the app context is
made inactive. Since a request context typically also manages an
application context it would also be called when you pop a
request context.
Since a request context typically also manages an application
context it would also be called when you pop a request context.
When a teardown function was called because of an unhandled
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
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.
Teardown functions must avoid raising exceptions. If they
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.
@ -1554,10 +1556,10 @@ class Flask(Scaffold):
This no longer does the exception handling, this code was
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:
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
# request came with the OPTIONS method, reply automatically
if (
@ -1566,7 +1568,8 @@ class Flask(Scaffold):
):
return self.make_default_options_response()
# 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:
"""Dispatches the request and on top of that performs request
@ -1631,8 +1634,8 @@ class Flask(Scaffold):
.. versionadded:: 0.7
"""
adapter = _request_ctx_stack.top.url_adapter
methods = adapter.allowed_methods()
adapter = request_ctx.url_adapter
methods = adapter.allowed_methods() # type: ignore[union-attr]
rv = self.response_class()
rv.allow.update(methods)
return rv
@ -1740,7 +1743,7 @@ class Flask(Scaffold):
.. versionadded:: 2.2
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:
url_adapter = req_ctx.url_adapter
@ -1759,7 +1762,7 @@ class Flask(Scaffold):
if _external is None:
_external = _scheme is not None
else:
app_ctx = _app_ctx_stack.top
app_ctx = _cv_app.get(None)
# If called by helpers.url_for, an app context is active,
# use its url_adapter. Otherwise, app.url_for was called
@ -1790,7 +1793,7 @@ class Flask(Scaffold):
self.inject_url_defaults(endpoint, values)
try:
rv = url_adapter.build(
rv = url_adapter.build( # type: ignore[union-attr]
endpoint,
values,
method=_method,
@ -2099,7 +2102,7 @@ class Flask(Scaffold):
:return: a new response object or the same, has to be an
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:
response = self.ensure_sync(func)(response)
@ -2305,8 +2308,8 @@ class Flask(Scaffold):
return response(environ, start_response)
finally:
if "werkzeug.debug.preserve_context" in environ:
environ["werkzeug.debug.preserve_context"](_app_ctx_stack.top)
environ["werkzeug.debug.preserve_context"](_request_ctx_stack.top)
environ["werkzeug.debug.preserve_context"](_cv_app.get())
environ["werkzeug.debug.preserve_context"](_cv_req.get())
if error is not None and self.should_ignore_error(error):
error = None

View file

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

View file

@ -1,3 +1,4 @@
import contextvars
import sys
import typing as t
from functools import update_wrapper
@ -6,8 +7,8 @@ from types import TracebackType
from werkzeug.exceptions import HTTPException
from . import typing as ft
from .globals import _app_ctx_stack
from .globals import _request_ctx_stack
from .globals import _cv_app
from .globals import _cv_req
from .signals import appcontext_popped
from .signals import appcontext_pushed
@ -103,9 +104,9 @@ class _AppCtxGlobals:
return iter(self.__dict__)
def __repr__(self) -> str:
top = _app_ctx_stack.top
if top is not None:
return f"<flask.g of {top.app.name!r}>"
ctx = _cv_app.get(None)
if ctx is not None:
return f"<flask.g of '{ctx.app.name}'>"
return object.__repr__(self)
@ -130,15 +131,15 @@ def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable:
.. versionadded:: 0.9
"""
top = _request_ctx_stack.top
ctx = _cv_req.get(None)
if top is None:
if ctx is None:
raise RuntimeError(
"This decorator can only be used when a request context is"
" active, such as within a view function."
"'after_this_request' can only be used when a request"
" context is active, such as in a view function."
)
top._after_request_functions.append(f)
ctx._after_request_functions.append(f)
return f
@ -166,19 +167,19 @@ def copy_current_request_context(f: t.Callable) -> t.Callable:
.. versionadded:: 0.10
"""
top = _request_ctx_stack.top
ctx = _cv_req.get(None)
if top is None:
if ctx is None:
raise RuntimeError(
"This decorator can only be used when a request context is"
" active, such as within a view function."
"'copy_current_request_context' can only be used when a"
" request context is active, such as in a view function."
)
reqctx = top.copy()
ctx = ctx.copy()
def wrapper(*args, **kwargs):
with reqctx:
return reqctx.app.ensure_sync(f)(*args, **kwargs)
with ctx:
return ctx.app.ensure_sync(f)(*args, **kwargs)
return update_wrapper(wrapper, f)
@ -212,7 +213,7 @@ def has_request_context() -> bool:
.. versionadded:: 0.7
"""
return _request_ctx_stack.top is not None
return _cv_app.get(None) is not None
def has_app_context() -> bool:
@ -222,44 +223,43 @@ def has_app_context() -> bool:
.. versionadded:: 0.9
"""
return _app_ctx_stack.top is not None
return _cv_req.get(None) is not None
class AppContext:
"""The application context binds an application object implicitly
to the current thread or greenlet, similar to how the
:class:`RequestContext` binds request information. The application
context is also implicitly created if a request context is created
but the application is not on top of the individual application
context.
"""The app context contains application-specific information. An app
context is created and pushed at the beginning of each request if
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 = app.app_ctx_globals_class()
# Like request context, app contexts can be pushed multiple times
# but there a basic "refcount" is enough to track them.
self._refcnt = 0
self.g: _AppCtxGlobals = app.app_ctx_globals_class()
self._cv_tokens: t.List[contextvars.Token] = []
def push(self) -> None:
"""Binds the app context to the current context."""
self._refcnt += 1
_app_ctx_stack.push(self)
self._cv_tokens.append(_cv_app.set(self))
appcontext_pushed.send(self.app)
def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore
"""Pops the app context."""
try:
self._refcnt -= 1
if self._refcnt <= 0:
if len(self._cv_tokens) == 1:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_appcontext(exc)
finally:
rv = _app_ctx_stack.pop()
assert rv is self, f"Popped wrong app context. ({rv!r} instead of {self!r})"
ctx = _cv_app.get()
_cv_app.reset(self._cv_tokens.pop())
if ctx is not self:
raise AssertionError(
f"Popped wrong app context. ({ctx!r} instead of {self!r})"
)
appcontext_popped.send(self.app)
def __enter__(self) -> "AppContext":
@ -276,10 +276,10 @@ class AppContext:
class RequestContext:
"""The request context contains all request relevant information. It is
created at the beginning of the request and pushed to the
`_request_ctx_stack` and removed at the end of it. It will create the
URL adapter and request object for the WSGI environment provided.
"""The request context contains per-request information. The Flask
app creates and pushes it at the beginning of the request, then pops
it at the end of the request. It will create the URL adapter and
request object for the WSGI environment provided.
Do not attempt to use this class directly, instead use
:meth:`~flask.Flask.test_request_context` and
@ -307,26 +307,21 @@ class RequestContext:
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
self.request: Request = request
self.url_adapter = None
try:
self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e:
self.request.routing_exception = e
self.flashes = None
self.session = session
# Request contexts can be pushed multiple times and interleaved with
# other request contexts. Now only if the last level is popped we
# get rid of them. Additionally if an application context is missing
# one is created implicitly so for each level we add this information
self._implicit_app_ctx_stack: t.List[t.Optional["AppContext"]] = []
self.flashes: t.Optional[t.List[t.Tuple[str, str]]] = None
self.session: t.Optional["SessionMixin"] = 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: t.List[ft.AfterRequestCallable] = []
self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = []
def copy(self) -> "RequestContext":
"""Creates a copy of this request context with the same request object.
This can be used to move a request context to a different greenlet.
@ -360,15 +355,15 @@ class RequestContext:
def push(self) -> None:
# Before we push the request context we have to ensure that there
# is an application context.
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
app_ctx = _cv_app.get(None)
if app_ctx is None or app_ctx.app is not self.app:
app_ctx = self.app.app_context()
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
app_ctx = None
_request_ctx_stack.push(self)
self._cv_tokens.append((_cv_req.set(self), app_ctx))
# Open the session at the moment that the request context is available.
# This allows a custom open_session method to use the request context.
@ -394,11 +389,10 @@ class RequestContext:
.. versionchanged:: 0.9
Added the `exc` argument.
"""
app_ctx = self._implicit_app_ctx_stack.pop()
clear_request = False
clear_request = len(self._cv_tokens) == 1
try:
if not self._implicit_app_ctx_stack:
if clear_request:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)
@ -406,36 +400,23 @@ class RequestContext:
request_close = getattr(self.request, "close", None)
if request_close is not None:
request_close()
clear_request = True
finally:
rv = _request_ctx_stack.pop()
ctx = _cv_req.get()
token, app_ctx = self._cv_tokens.pop()
_cv_req.reset(token)
# get rid of circular dependencies at the end of the request
# so that we don't require the GC to be active.
if clear_request:
rv.request.environ["werkzeug.request"] = None
ctx.request.environ["werkzeug.request"] = None
# Get rid of the app as well if necessary.
if app_ctx is not None:
app_ctx.pop(exc)
assert (
rv is self
), f"Popped wrong request context. ({rv!r} instead of {self!r})"
def auto_pop(self, exc: t.Optional[BaseException]) -> None:
"""
.. deprecated:: 2.2
Will be removed in Flask 2.3.
"""
import warnings
warnings.warn(
"'ctx.auto_pop' is deprecated and will be removed in Flask 2.3.",
DeprecationWarning,
stacklevel=2,
)
self.pop(exc)
if ctx is not self:
raise AssertionError(
f"Popped wrong request context. ({ctx!r} instead of {self!r})"
)
def __enter__(self) -> "RequestContext":
self.push()

View file

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

View file

@ -1,59 +1,107 @@
import typing as t
from functools import partial
from contextvars import ContextVar
from werkzeug.local import LocalProxy
from werkzeug.local import LocalStack
if t.TYPE_CHECKING: # pragma: no cover
from .app import Flask
from .ctx import _AppCtxGlobals
from .ctx import AppContext
from .ctx import RequestContext
from .sessions import SessionMixin
from .wrappers import Request
_request_ctx_err_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.\
"""
_app_ctx_err_msg = """\
class _FakeStack:
def __init__(self, name: str, cv: ContextVar[t.Any]) -> None:
self.name = name
self.cv = cv
def _warn(self):
import warnings
warnings.warn(
f"'_{self.name}_ctx_stack' is deprecated and will be"
" removed in Flask 2.3. Use 'g' to store data, or"
f" '{self.name}_ctx' to access the current context.",
DeprecationWarning,
stacklevel=3,
)
def push(self, obj: t.Any) -> None:
self._warn()
self.cv.set(obj)
def pop(self) -> t.Any:
self._warn()
ctx = self.cv.get(None)
self.cv.set(None)
return ctx
@property
def top(self) -> t.Optional[t.Any]:
self._warn()
return self.cv.get(None)
_no_app_msg = """\
Working outside of application context.
This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve
this, set up an application context with app.app_context(). See the
documentation for more information.\
the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.\
"""
def _lookup_req_object(name):
top = _request_ctx_stack.top
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")
_cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx")
__app_ctx_stack = _FakeStack("app", _cv_app)
app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment]
_cv_app, unbound_message=_no_app_msg
)
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.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 request
from .globals import request_ctx
from .globals import session
from .signals import message_flashed
@ -110,11 +111,11 @@ def stream_with_context(
return update_wrapper(decorator, generator_or_function) # type: ignore
def generator() -> t.Generator:
ctx = _request_ctx_stack.top
ctx = _cv_req.get(None)
if ctx is None:
raise RuntimeError(
"Attempted to stream with context but "
"there was no context in the first place to keep around."
"'stream_with_context' can only be used when a request"
" context is active, such as in a view function."
)
with ctx:
# 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
categories in the list will be returned.
"""
flashes = _request_ctx_stack.top.flashes
flashes = request_ctx.flashes
if flashes is None:
_request_ctx_stack.top.flashes = flashes = (
session.pop("_flashes") if "_flashes" in session else []
)
flashes = session.pop("_flashes") if "_flashes" in session else []
request_ctx.flashes = flashes
if category_filter:
flashes = list(filter(lambda f: f[0] in category_filter, flashes))
if not with_categories:

View file

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

View file

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

View file

@ -11,7 +11,7 @@ from werkzeug.urls import url_parse
from werkzeug.wrappers import Request as BaseRequest
from .cli import ScriptInfo
from .globals import _request_ctx_stack
from .globals import _cv_req
from .json import dumps as json_dumps
from .sessions import SessionMixin
@ -94,11 +94,10 @@ class EnvironBuilder(werkzeug.test.EnvironBuilder):
class FlaskClient(Client):
"""Works like a regular Werkzeug test client but has some knowledge about
how Flask works to defer the cleanup of the request context stack to the
end of a ``with`` body when used in a ``with`` statement. For general
information about how to use this class refer to
:class:`werkzeug.test.Client`.
"""Works like a regular Werkzeug test client but has knowledge about
Flask's contexts to defer the cleanup of the request context until
the end of a ``with`` block. For general information about how to
use this class refer to :class:`werkzeug.test.Client`.
.. versionchanged:: 0.12
`app.test_client()` includes preset default environment, which can be
@ -147,7 +146,7 @@ class FlaskClient(Client):
app = self.application
environ_overrides = kwargs.setdefault("environ_overrides", {})
self.cookie_jar.inject_wsgi(environ_overrides)
outer_reqctx = _request_ctx_stack.top
outer_reqctx = _cv_req.get(None)
with app.test_request_context(*args, **kwargs) as c:
session_interface = app.session_interface
sess = session_interface.open_session(app, c.request)
@ -163,11 +162,11 @@ class FlaskClient(Client):
# behavior. It's important to not use the push and pop
# methods of the actual request context object since that would
# mean that cleanup handlers are called
_request_ctx_stack.push(outer_reqctx)
token = _cv_req.set(outer_reqctx) # type: ignore[arg-type]
try:
yield sess
finally:
_request_ctx_stack.pop()
_cv_req.reset(token)
resp = app.response_class()
if not session_interface.is_null_session(sess):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import werkzeug
import flask
from flask import appcontext_popped
from flask.cli import ScriptInfo
from flask.globals import _cv_req
from flask.json import jsonify
from flask.testing import EnvironBuilder
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
rv.close()
# 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