Merge pull request #4682 from pallets/refactor-context-stack
remove use of `LocalStack`
This commit is contained in:
commit
cbebdae698
23 changed files with 351 additions and 352 deletions
|
|
@ -15,6 +15,15 @@ Unreleased
|
||||||
- The ``RequestContext.g`` property returning ``AppContext.g`` is
|
- The ``RequestContext.g`` property returning ``AppContext.g`` is
|
||||||
removed.
|
removed.
|
||||||
|
|
||||||
|
- The app and request contexts are managed using Python context vars
|
||||||
|
directly rather than Werkzeug's ``LocalStack``. This should result
|
||||||
|
in better performance and memory use. :pr:`4672`
|
||||||
|
|
||||||
|
- Extension maintainers, be aware that ``_app_ctx_stack.top``
|
||||||
|
and ``_request_ctx_stack.top`` are deprecated. Store data on
|
||||||
|
``g`` instead using a unique prefix, like
|
||||||
|
``g._extension_name_attr``.
|
||||||
|
|
||||||
- Add new customization points to the ``Flask`` app object for many
|
- Add new customization points to the ``Flask`` app object for many
|
||||||
previously global behaviors.
|
previously global behaviors.
|
||||||
|
|
||||||
|
|
|
||||||
56
docs/api.rst
56
docs/api.rst
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
------------------
|
------------------
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
----------------
|
----------------
|
||||||
|
|
|
||||||
|
|
@ -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('/')
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
139
src/flask/ctx.py
139
src/flask/ctx.py
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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 == []
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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("/")
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue