diff --git a/src/flask/app.py b/src/flask/app.py index c6dd7833..cc326dbe 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1318,8 +1318,8 @@ class Flask(App): for func in reversed(self.after_request_funcs[name]): response = self.ensure_sync(func)(response) - if not self.session_interface.is_null_session(ctx.session): - self.session_interface.save_session(self, ctx.session, response) + if not self.session_interface.is_null_session(ctx._session): + self.session_interface.save_session(self, ctx._session, response) diff --git a/src/flask/ctx.py b/src/flask/ctx.py index 222e818e..5f7b1f1d 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -324,7 +324,7 @@ class RequestContext: - self.session: SessionMixin | None = session + self._session: SessionMixin | None = session @@ -351,7 +351,7 @@ class RequestContext: request=self.request, - session=self.session, + session=self._session, ) @@ -364,6 +364,16 @@ class RequestContext: + @property + def session(self) -> SessionMixin: + assert self._session is not None, "The session has not yet been opened." + self._session.accessed = True + return self._session @@ -381,12 +391,12 @@ class RequestContext: - if self.session is None: + if self._session is None: session_interface = self.app.session_interface - self.session = session_interface.open_session(self.app, self.request) + self._session = session_interface.open_session(self.app, self.request) - if self.session is None: - self.session = session_interface.make_null_session(self.app) + if self._session is None: + self._session = session_interface.make_null_session(self.app) diff --git a/src/flask/sessions.py b/src/flask/sessions.py index 1841d882..ad357706 100644 --- a/src/flask/sessions.py +++ b/src/flask/sessions.py @@ -43,10 +43,15 @@ class SessionMixin(MutableMapping[str, t.Any]): #: ``True``. modified = True - accessed = True + accessed = False @@ -65,34 +70,15 @@ class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): #: will only be written to the response if this is ``True``. modified = False def __init__( self, - initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, + initial: c.Mapping[str, t.Any] | None = None, ) -> None: def on_update(self: te.Self) -> None: self.modified = True - self.accessed = True super().__init__(initial, on_update) - ... class NullSession(SecureCookieSession): """Class used to generate nicer error messages if sessions are not diff --git a/src/flask/templating.py b/src/flask/templating.py index 16d480f5..c5fb5b99 100644 --- a/src/flask/templating.py +++ b/src/flask/templating.py @@ -22,8 +22,8 @@ if t.TYPE_CHECKING: # pragma: no cover def _default_template_ctx_processor() -> dict[str, t.Any]: - """Default template context processor. Injects `request`, - `session` and `g`. + """Default template context processor. Replaces the ``request`` and ``g`` + proxies with their concrete objects for faster access. """ appctx = _cv_app.get(None) reqctx = _cv_request.get(None) @@ -32,7 +32,8 @@ def _default_template_ctx_processor() -> dict[str, t.Any]: rv["g"] = appctx.g if reqctx is not None: rv["request"] = reqctx.request - rv["session"] = reqctx.session + # The session proxy cannot be replaced, accessing it gets + # RequestContext.session, which sets session.accessed. return rv diff --git a/tests/test_basic.py b/tests/test_basic.py index c372a910..4b3374e2 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -231,27 +233,46 @@ def test_endpoint_decorator(app, client): -def test_session(app, client): +def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None: + @app.post("/") + def do_set(): flask.session["value"] = flask.request.form["value"] return "value set" + @app.get("/") + def do_get(): + return flask.session.get("value", "None") + @app.get("/nothing") + def do_nothing() -> str: + return "" + with client: + rv = client.get("/nothing") + assert "cookie" not in rv.vary + assert not request_ctx._session.accessed + with client: + rv = client.post(data={"value": "42"}) + assert "cookie" in rv.vary + assert request_ctx._session.accessed + assert request_ctx._session.modified + with client: + rv = client.get() + assert rv.text == "42" + assert "cookie" in rv.vary + assert request_ctx._session.accessed + assert not request_ctx._session.modified + with client: + rv = client.get("/nothing") + assert "cookie" not in rv.vary + assert not request_ctx._session.accessed