diff --git a/CHANGES.rst b/CHANGES.rst index 504e319f..5fda08e3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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. diff --git a/docs/api.rst b/docs/api.rst index 217473bc..67772a77 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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: diff --git a/docs/appcontext.rst b/docs/appcontext.rst index b214f254..a4ae3861 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -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 ------------------ diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 25ced187..95745119 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -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 ---------------- diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 12336fb1..5932589f 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -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('/') diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 96e17e3b..2b109770 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -37,12 +37,14 @@ context, which also pushes an :doc:`app context `. 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. diff --git a/setup.py b/setup.py index a28763b5..bd31435c 100644 --- a/setup.py +++ b/setup.py @@ -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", diff --git a/src/flask/__init__.py b/src/flask/__init__.py index 8ca1dbad..7eb8c24b 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -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) diff --git a/src/flask/app.py b/src/flask/app.py index 736061b9..19d77351 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -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 diff --git a/src/flask/cli.py b/src/flask/cli.py index af29b2c1..ba0db467 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -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 diff --git a/src/flask/ctx.py b/src/flask/ctx.py index dc1f23b3..1ea2719b 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -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"" + ctx = _cv_app.get(None) + if ctx is not None: + return f"" 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() diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index b1e3ce1b..b0639892 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -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): diff --git a/src/flask/globals.py b/src/flask/globals.py index 7824204f..af079877 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -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) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 79c562c0..bc074b6a 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -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: diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 7f099f40..1f6882ac 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -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. """ diff --git a/src/flask/templating.py b/src/flask/templating.py index 7d92cf1e..24a672b7 100644 --- a/src/flask/templating.py +++ b/src/flask/templating.py @@ -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( diff --git a/src/flask/testing.py b/src/flask/testing.py index f652c23a..bf84f848 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -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): diff --git a/tests/conftest.py b/tests/conftest.py index 1e1ba0d4..670acc88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 == [] diff --git a/tests/test_appctx.py b/tests/test_appctx.py index aeb75a55..f5ca0bde 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -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("/") diff --git a/tests/test_basic.py b/tests/test_basic.py index dca48e2d..3e98fdac 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -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]) diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py index 1a478917..abfacb98 100644 --- a/tests/test_reqctx.py +++ b/tests/test_reqctx.py @@ -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 diff --git a/tests/test_session_interface.py b/tests/test_session_interface.py index 39562f5a..613da37f 100644 --- a/tests/test_session_interface.py +++ b/tests/test_session_interface.py @@ -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__) diff --git a/tests/test_testing.py b/tests/test_testing.py index dd6347e5..0b37a2e5 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -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