remove uses of LocalStack

This commit is contained in:
David Lord 2022-07-05 06:33:03 -07:00
parent d597db67de
commit 82c2e0366c
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
13 changed files with 114 additions and 131 deletions

View file

@ -38,10 +38,11 @@ from .config import ConfigAttribute
from .ctx import _AppCtxGlobals from .ctx import _AppCtxGlobals
from .ctx import AppContext from .ctx import AppContext
from .ctx import RequestContext from .ctx import RequestContext
from .globals import _app_ctx_stack from .globals import _cv_app
from .globals import _request_ctx_stack from .globals import _cv_req
from .globals import g from .globals import g
from .globals import request from .globals import request
from .globals import request_ctx
from .globals import session from .globals import session
from .helpers import _split_blueprint_path from .helpers import _split_blueprint_path
from .helpers import get_debug_flag from .helpers import get_debug_flag
@ -1554,10 +1555,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 +1567,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 +1633,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 +1742,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 +1761,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 +1792,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 +2101,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 +2307,8 @@ class Flask(Scaffold):
return response(environ, start_response) return response(environ, start_response)
finally: finally:
if "werkzeug.debug.preserve_context" in environ: if "werkzeug.debug.preserve_context" in environ:
environ["werkzeug.debug.preserve_context"](_app_ctx_stack.top) environ["werkzeug.debug.preserve_context"](_cv_app.get())
environ["werkzeug.debug.preserve_context"](_request_ctx_stack.top) environ["werkzeug.debug.preserve_context"](_cv_req.get())
if error is not None and self.should_ignore_error(error): if error is not None and self.should_ignore_error(error):
error = None error = None

View file

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

View file

@ -7,10 +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 _cv_app
from .globals import _cv_req from .globals import _cv_req
from .globals import _request_ctx_stack
from .signals import appcontext_popped from .signals import appcontext_popped
from .signals import appcontext_pushed from .signals import appcontext_pushed
@ -106,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)
@ -133,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
@ -169,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)
@ -240,7 +238,7 @@ class AppContext:
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] = [] self._cv_tokens: t.List[contextvars.Token] = []
def push(self) -> None: def push(self) -> None:
@ -311,14 +309,14 @@ 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
# 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.

View file

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

View file

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

View file

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

View file

@ -163,7 +163,7 @@ 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
token = _cv_req.set(outer_reqctx) token = _cv_req.set(outer_reqctx) # type: ignore[arg-type]
try: try:
yield sess yield sess
finally: finally:

View file

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

View file

@ -1,6 +1,8 @@
import pytest import pytest
import flask import flask
from flask.globals import app_ctx
from flask.globals import request_ctx
def test_basic_url_generation(app): def test_basic_url_generation(app):
@ -29,14 +31,14 @@ def test_url_generation_without_context_fails():
def test_request_context_means_app_context(app): def test_request_context_means_app_context(app):
with app.test_request_context(): with app.test_request_context():
assert flask.current_app._get_current_object() == app assert flask.current_app._get_current_object() is app
assert flask._app_ctx_stack.top is None assert not flask.current_app
def test_app_context_provides_current_app(app): def test_app_context_provides_current_app(app):
with app.app_context(): with app.app_context():
assert flask.current_app._get_current_object() == app assert flask.current_app._get_current_object() is app
assert flask._app_ctx_stack.top is None assert not flask.current_app
def test_app_tearing_down(app): def test_app_tearing_down(app):
@ -175,11 +177,11 @@ def test_context_refcounts(app, client):
@app.route("/") @app.route("/")
def index(): def index():
with flask._app_ctx_stack.top: with app_ctx:
with flask._request_ctx_stack.top: with request_ctx:
pass pass
env = flask._request_ctx_stack.top.request.environ
assert env["werkzeug.request"] is not None assert flask.request.environ["werkzeug.request"] is not None
return "" return ""
res = client.get("/") res = client.get("/")

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import werkzeug
import flask import flask
from flask import appcontext_popped from flask import appcontext_popped
from flask.cli import ScriptInfo from flask.cli import ScriptInfo
from flask.globals import _cv_req
from flask.json import jsonify from flask.json import jsonify
from flask.testing import EnvironBuilder from flask.testing import EnvironBuilder
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
@ -399,4 +400,4 @@ def test_client_pop_all_preserved(app, req_ctx, client):
# close the response, releasing the context held by stream_with_context # close the response, releasing the context held by stream_with_context
rv.close() rv.close()
# only req_ctx fixture should still be pushed # only req_ctx fixture should still be pushed
assert flask._request_ctx_stack.top is req_ctx assert _cv_req.get(None) is req_ctx