Skip to content

Commit d597db6

Browse files
committed
contexts no longer use LocalStack
1 parent 0b2f809 commit d597db6

File tree

5 files changed

+146
-71
lines changed

5 files changed

+146
-71
lines changed

CHANGES.rst

+9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ Unreleased
1515
- The ``RequestContext.g`` property returning ``AppContext.g`` is
1616
removed.
1717

18+
- The app and request contexts are managed using Python context vars
19+
directly rather than Werkzeug's ``LocalStack``. This should result
20+
in better performance and memory use. :pr:`4672`
21+
22+
- Extension maintainers, be aware that ``_app_ctx_stack.top``
23+
and ``_request_ctx_stack.top`` are deprecated. Store data on
24+
``g`` instead using a unique prefix, like
25+
``g._extension_name_attr``.
26+
1827
- Add new customization points to the ``Flask`` app object for many
1928
previously global behaviors.
2029

src/flask/__init__.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
from .ctx import copy_current_request_context as copy_current_request_context
1212
from .ctx import has_app_context as has_app_context
1313
from .ctx import has_request_context as has_request_context
14-
from .globals import _app_ctx_stack as _app_ctx_stack
15-
from .globals import _request_ctx_stack as _request_ctx_stack
1614
from .globals import current_app as current_app
1715
from .globals import g as g
1816
from .globals import request as request
@@ -45,3 +43,29 @@
4543
from .templating import stream_template_string as stream_template_string
4644

4745
__version__ = "2.2.0.dev0"
46+
47+
48+
def __getattr__(name):
49+
if name == "_app_ctx_stack":
50+
import warnings
51+
from .globals import __app_ctx_stack
52+
53+
warnings.warn(
54+
"'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.",
55+
DeprecationWarning,
56+
stacklevel=2,
57+
)
58+
return __app_ctx_stack
59+
60+
if name == "_request_ctx_stack":
61+
import warnings
62+
from .globals import __request_ctx_stack
63+
64+
warnings.warn(
65+
"'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.",
66+
DeprecationWarning,
67+
stacklevel=2,
68+
)
69+
return __request_ctx_stack
70+
71+
raise AttributeError(name)

src/flask/ctx.py

+33-48
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextvars
12
import sys
23
import typing as t
34
from functools import update_wrapper
@@ -7,6 +8,8 @@
78

89
from . import typing as ft
910
from .globals import _app_ctx_stack
11+
from .globals import _cv_app
12+
from .globals import _cv_req
1013
from .globals import _request_ctx_stack
1114
from .signals import appcontext_popped
1215
from .signals import appcontext_pushed
@@ -212,7 +215,7 @@ def __init__(self, username, remote_addr=None):
212215
213216
.. versionadded:: 0.7
214217
"""
215-
return _request_ctx_stack.top is not None
218+
return _cv_app.get(None) is not None
216219

217220

218221
def has_app_context() -> bool:
@@ -222,7 +225,7 @@ def has_app_context() -> bool:
222225
223226
.. versionadded:: 0.9
224227
"""
225-
return _app_ctx_stack.top is not None
228+
return _cv_req.get(None) is not None
226229

227230

228231
class AppContext:
@@ -238,28 +241,29 @@ def __init__(self, app: "Flask") -> None:
238241
self.app = app
239242
self.url_adapter = app.create_url_adapter(None)
240243
self.g = app.app_ctx_globals_class()
241-
242-
# Like request context, app contexts can be pushed multiple times
243-
# but there a basic "refcount" is enough to track them.
244-
self._refcnt = 0
244+
self._cv_tokens: t.List[contextvars.Token] = []
245245

246246
def push(self) -> None:
247247
"""Binds the app context to the current context."""
248-
self._refcnt += 1
249-
_app_ctx_stack.push(self)
248+
self._cv_tokens.append(_cv_app.set(self))
250249
appcontext_pushed.send(self.app)
251250

252251
def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore
253252
"""Pops the app context."""
254253
try:
255-
self._refcnt -= 1
256-
if self._refcnt <= 0:
254+
if len(self._cv_tokens) == 1:
257255
if exc is _sentinel:
258256
exc = sys.exc_info()[1]
259257
self.app.do_teardown_appcontext(exc)
260258
finally:
261-
rv = _app_ctx_stack.pop()
262-
assert rv is self, f"Popped wrong app context. ({rv!r} instead of {self!r})"
259+
ctx = _cv_app.get()
260+
_cv_app.reset(self._cv_tokens.pop())
261+
262+
if ctx is not self:
263+
raise AssertionError(
264+
f"Popped wrong app context. ({ctx!r} instead of {self!r})"
265+
)
266+
263267
appcontext_popped.send(self.app)
264268

265269
def __enter__(self) -> "AppContext":
@@ -315,18 +319,13 @@ def __init__(
315319
self.request.routing_exception = e
316320
self.flashes = None
317321
self.session = session
318-
319-
# Request contexts can be pushed multiple times and interleaved with
320-
# other request contexts. Now only if the last level is popped we
321-
# get rid of them. Additionally if an application context is missing
322-
# one is created implicitly so for each level we add this information
323-
self._implicit_app_ctx_stack: t.List[t.Optional["AppContext"]] = []
324-
325322
# Functions that should be executed after the request on the response
326323
# object. These will be called before the regular "after_request"
327324
# functions.
328325
self._after_request_functions: t.List[ft.AfterRequestCallable] = []
329326

327+
self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = []
328+
330329
def copy(self) -> "RequestContext":
331330
"""Creates a copy of this request context with the same request object.
332331
This can be used to move a request context to a different greenlet.
@@ -360,15 +359,15 @@ def match_request(self) -> None:
360359
def push(self) -> None:
361360
# Before we push the request context we have to ensure that there
362361
# is an application context.
363-
app_ctx = _app_ctx_stack.top
364-
if app_ctx is None or app_ctx.app != self.app:
362+
app_ctx = _cv_app.get(None)
363+
364+
if app_ctx is None or app_ctx.app is not self.app:
365365
app_ctx = self.app.app_context()
366366
app_ctx.push()
367-
self._implicit_app_ctx_stack.append(app_ctx)
368367
else:
369-
self._implicit_app_ctx_stack.append(None)
368+
app_ctx = None
370369

371-
_request_ctx_stack.push(self)
370+
self._cv_tokens.append((_cv_req.set(self), app_ctx))
372371

373372
# Open the session at the moment that the request context is available.
374373
# This allows a custom open_session method to use the request context.
@@ -394,48 +393,34 @@ def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: igno
394393
.. versionchanged:: 0.9
395394
Added the `exc` argument.
396395
"""
397-
app_ctx = self._implicit_app_ctx_stack.pop()
398-
clear_request = False
396+
clear_request = len(self._cv_tokens) == 1
399397

400398
try:
401-
if not self._implicit_app_ctx_stack:
399+
if clear_request:
402400
if exc is _sentinel:
403401
exc = sys.exc_info()[1]
404402
self.app.do_teardown_request(exc)
405403

406404
request_close = getattr(self.request, "close", None)
407405
if request_close is not None:
408406
request_close()
409-
clear_request = True
410407
finally:
411-
rv = _request_ctx_stack.pop()
408+
ctx = _cv_req.get()
409+
token, app_ctx = self._cv_tokens.pop()
410+
_cv_req.reset(token)
412411

413412
# get rid of circular dependencies at the end of the request
414413
# so that we don't require the GC to be active.
415414
if clear_request:
416-
rv.request.environ["werkzeug.request"] = None
415+
ctx.request.environ["werkzeug.request"] = None
417416

418-
# Get rid of the app as well if necessary.
419417
if app_ctx is not None:
420418
app_ctx.pop(exc)
421419

422-
assert (
423-
rv is self
424-
), f"Popped wrong request context. ({rv!r} instead of {self!r})"
425-
426-
def auto_pop(self, exc: t.Optional[BaseException]) -> None:
427-
"""
428-
.. deprecated:: 2.2
429-
Will be removed in Flask 2.3.
430-
"""
431-
import warnings
432-
433-
warnings.warn(
434-
"'ctx.auto_pop' is deprecated and will be removed in Flask 2.3.",
435-
DeprecationWarning,
436-
stacklevel=2,
437-
)
438-
self.pop(exc)
420+
if ctx is not self:
421+
raise AssertionError(
422+
f"Popped wrong request context. ({ctx!r} instead of {self!r})"
423+
)
439424

440425
def __enter__(self) -> "RequestContext":
441426
self.push()

src/flask/globals.py

+74-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import typing as t
22
from contextvars import ContextVar
33

4-
from werkzeug.local import LocalStack
4+
from werkzeug.local import LocalProxy
55

66
if t.TYPE_CHECKING: # pragma: no cover
77
from .app import Flask
@@ -11,23 +11,56 @@
1111
from .sessions import SessionMixin
1212
from .wrappers import Request
1313

14+
15+
class _FakeStack:
16+
def __init__(self, name: str, cv: ContextVar[t.Any]) -> None:
17+
self.name = name
18+
self.cv = cv
19+
20+
def _warn(self):
21+
import warnings
22+
23+
warnings.warn(
24+
f"'_{self.name}_ctx_stack' is deprecated and will be"
25+
" removed in Flask 2.3. Use 'g' to store data, or"
26+
f" '{self.name}_ctx' to access the current context.",
27+
DeprecationWarning,
28+
stacklevel=3,
29+
)
30+
31+
def push(self, obj: t.Any) -> None:
32+
self._warn()
33+
self.cv.set(obj)
34+
35+
def pop(self) -> t.Any:
36+
self._warn()
37+
ctx = self.cv.get(None)
38+
self.cv.set(None)
39+
return ctx
40+
41+
@property
42+
def top(self) -> t.Optional[t.Any]:
43+
self._warn()
44+
return self.cv.get(None)
45+
46+
1447
_no_app_msg = """\
1548
Working outside of application context.
1649
1750
This typically means that you attempted to use functionality that needed
1851
the current application. To solve this, set up an application context
1952
with app.app_context(). See the documentation for more information.\
2053
"""
21-
_cv_app: ContextVar[t.List["AppContext"]] = ContextVar("flask.app_ctx")
22-
_app_ctx_stack: LocalStack["AppContext"] = LocalStack(_cv_app)
23-
app_ctx: "AppContext" = _app_ctx_stack( # type: ignore[assignment]
24-
unbound_message=_no_app_msg
54+
_cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx")
55+
__app_ctx_stack = _FakeStack("app", _cv_app)
56+
app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment]
57+
_cv_app, unbound_message=_no_app_msg
2558
)
26-
current_app: "Flask" = _app_ctx_stack( # type: ignore[assignment]
27-
"app", unbound_message=_no_app_msg
59+
current_app: "Flask" = LocalProxy( # type: ignore[assignment]
60+
_cv_app, "app", unbound_message=_no_app_msg
2861
)
29-
g: "_AppCtxGlobals" = _app_ctx_stack( # type: ignore[assignment]
30-
"g", unbound_message=_no_app_msg
62+
g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment]
63+
_cv_app, "g", unbound_message=_no_app_msg
3164
)
3265

3366
_no_req_msg = """\
@@ -37,14 +70,38 @@
3770
an active HTTP request. Consult the documentation on testing for
3871
information about how to avoid this problem.\
3972
"""
40-
_cv_req: ContextVar[t.List["RequestContext"]] = ContextVar("flask.request_ctx")
41-
_request_ctx_stack: LocalStack["RequestContext"] = LocalStack(_cv_req)
42-
request_ctx: "RequestContext" = _request_ctx_stack( # type: ignore[assignment]
43-
unbound_message=_no_req_msg
73+
_cv_req: ContextVar["RequestContext"] = ContextVar("flask.request_ctx")
74+
__request_ctx_stack = _FakeStack("request", _cv_req)
75+
request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment]
76+
_cv_req, unbound_message=_no_req_msg
4477
)
45-
request: "Request" = _request_ctx_stack( # type: ignore[assignment]
46-
"request", unbound_message=_no_req_msg
78+
request: "Request" = LocalProxy( # type: ignore[assignment]
79+
_cv_req, "request", unbound_message=_no_req_msg
4780
)
48-
session: "SessionMixin" = _request_ctx_stack( # type: ignore[assignment]
49-
"session", unbound_message=_no_req_msg
81+
session: "SessionMixin" = LocalProxy( # type: ignore[assignment]
82+
_cv_req, "session", unbound_message=_no_req_msg
5083
)
84+
85+
86+
def __getattr__(name: str) -> t.Any:
87+
if name == "_app_ctx_stack":
88+
import warnings
89+
90+
warnings.warn(
91+
"'_app_ctx_stack' is deprecated and will be remoevd in Flask 2.3.",
92+
DeprecationWarning,
93+
stacklevel=2,
94+
)
95+
return __app_ctx_stack
96+
97+
if name == "_request_ctx_stack":
98+
import warnings
99+
100+
warnings.warn(
101+
"'_request_ctx_stack' is deprecated and will be remoevd in Flask 2.3.",
102+
DeprecationWarning,
103+
stacklevel=2,
104+
)
105+
return __request_ctx_stack
106+
107+
raise AttributeError(name)

src/flask/testing.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from werkzeug.wrappers import Request as BaseRequest
1212

1313
from .cli import ScriptInfo
14-
from .globals import _request_ctx_stack
14+
from .globals import _cv_req
1515
from .json import dumps as json_dumps
1616
from .sessions import SessionMixin
1717

@@ -147,7 +147,7 @@ def session_transaction(
147147
app = self.application
148148
environ_overrides = kwargs.setdefault("environ_overrides", {})
149149
self.cookie_jar.inject_wsgi(environ_overrides)
150-
outer_reqctx = _request_ctx_stack.top
150+
outer_reqctx = _cv_req.get(None)
151151
with app.test_request_context(*args, **kwargs) as c:
152152
session_interface = app.session_interface
153153
sess = session_interface.open_session(app, c.request)
@@ -163,11 +163,11 @@ def session_transaction(
163163
# behavior. It's important to not use the push and pop
164164
# methods of the actual request context object since that would
165165
# mean that cleanup handlers are called
166-
_request_ctx_stack.push(outer_reqctx)
166+
token = _cv_req.set(outer_reqctx)
167167
try:
168168
yield sess
169169
finally:
170-
_request_ctx_stack.pop()
170+
_cv_req.reset(token)
171171

172172
resp = app.response_class()
173173
if not session_interface.is_null_session(sess):

0 commit comments

Comments
 (0)