From f658409ee3bc11ad161577bdf725795d5152408d Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 4 Jan 2018 08:40:12 -0800 Subject: [PATCH] implement session.new for the default session improve documentation for session attributes add test for session attributes --- CHANGES | 8 +++++- flask/sessions.py | 68 ++++++++++++++++++++++++++++----------------- tests/test_basic.py | 10 ++++++- 3 files changed, 58 insertions(+), 28 deletions(-) diff --git a/CHANGES b/CHANGES index 11fb7df2..19846057 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,7 @@ Flask Changelog Here you can see the full list of changes between each Flask release. + Version 0.13 ------------ @@ -114,7 +115,9 @@ Major release, unreleased depending on ``app.debug``. No handlers are removed, and a handler is only added if no handlers are already configured. (`#2436`_) - Blueprint view function name may not contain dots. (`#2450`_) -- The dev server now uses threads by default. +- The dev server now uses threads by default. (`#2529`_) +- ``session.new`` is implemented for the default session implementation. + (`#2579`_) .. _pallets/meta#24: https://github.com/pallets/meta/issues/24 .. _#1421: https://github.com/pallets/flask/issues/1421 @@ -149,6 +152,9 @@ Major release, unreleased .. _#2430: https://github.com/pallets/flask/pull/2430 .. _#2436: https://github.com/pallets/flask/pull/2436 .. _#2450: https://github.com/pallets/flask/pull/2450 +.. _#2529: https://github.com/pallets/flask/pull/2529 +.. _#2579: https://github.com/pallets/flask/pull/2579 + Version 0.12.3 -------------- diff --git a/flask/sessions.py b/flask/sessions.py index 82b588bc..ade9e8d0 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -10,6 +10,7 @@ """ import hashlib import warnings +from collections import MutableMapping from datetime import datetime from itsdangerous import BadSignature, URLSafeTimedSerializer @@ -19,52 +20,67 @@ from flask.helpers import is_ip, total_seconds from flask.json.tag import TaggedJSONSerializer -class SessionMixin(object): - """Expands a basic dictionary with an accessors that are expected - by Flask extensions and users for the session. - """ +class SessionMixin(MutableMapping): + """Expands a basic dictionary with session attributes.""" - def _get_permanent(self): + @property + def permanent(self): + """This reflects the ``'_permanent'`` key in the dict.""" return self.get('_permanent', False) - def _set_permanent(self, value): + @permanent.setter + def permanent(self, value): self['_permanent'] = bool(value) - #: this reflects the ``'_permanent'`` key in the dict. - permanent = property(_get_permanent, _set_permanent) - del _get_permanent, _set_permanent - - #: some session backends can tell you if a session is new, but that is - #: not necessarily guaranteed. Use with caution. The default mixin - #: implementation just hardcodes ``False`` in. + #: Some implementations can track whether a session is new, but + #: that is not guaranteed. Use with caution. The mixin default is + #: hard-coded ``False``. new = False - #: for some backends this will always be ``True``, but some backends will - #: default this to false and detect changes in the dictionary for as - #: long as changes do not happen on mutable structures in the session. - #: The default mixin implementation just hardcodes ``True`` in. + #: Some implementations can detect changes to the session and set + #: this when that happens. The mixin default is hard coded to + #: ``True``. modified = True - #: the accessed variable indicates whether or not the session object has - #: been accessed in that request. This allows flask to append a `Vary: - #: Cookie` header to the response if the session is being accessed. This - #: allows caching proxy servers, like Varnish, to use both the URL and the - #: session cookie as keys when caching pages, preventing multiple users - #: from being served the same cache. + #: Some implementations can detect when session data is read or + #: written and set this when that happens. The mixin default is hard + #: coded to ``True``. accessed = True class SecureCookieSession(CallbackDict, SessionMixin): - """Base class for sessions based on signed cookies.""" + """Base class for sessions based on signed cookies. + + This session backend will set the :attr:`new`, :attr:`modified`, + and :attr:`accessed` attributes. + """ + + #: The session is considered "new" if there was no session data + #: loaded. This means that the session will be new until there is + #: data in the session, because no cookie will be written otherwise. + new = True + + #: When data is changed, this is set to ``True``. Only the session + #: dictionary itself is tracked; if the session contains mutable + #: data (for example a nested dict) then this must be set to + #: ``True`` manually when modifying that data. The session cookie + #: will only be written to the response if this is ``True``. + modified = False + + #: When data is read or written, this is set to ``True``. Used by + # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` + #: header, which allows caching proxies to cache different pages for + #: different users. + accessed = False def __init__(self, initial=None): + self.new = initial is None + def on_update(self): self.modified = True self.accessed = True super(SecureCookieSession, self).__init__(initial, on_update) - self.modified = False - self.accessed = False def __getitem__(self, key): self.accessed = True diff --git a/tests/test_basic.py b/tests/test_basic.py index 0e3076df..3db7d859 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -17,6 +17,7 @@ from threading import Thread import pytest import werkzeug.serving +from werkzeug.datastructures import MultiDict from werkzeug.exceptions import BadRequest, Forbidden, NotFound from werkzeug.http import parse_date from werkzeug.routing import BuildError @@ -221,13 +222,20 @@ def test_endpoint_decorator(app, client): def test_session(app, client): @app.route('/set', methods=['POST']) def set(): + assert not flask.session.modified flask.session['value'] = flask.request.form['value'] + assert flask.session.modified return 'value set' @app.route('/get') def get(): - return flask.session['value'] + assert flask.session.new == ('new' in flask.request.args) + assert not flask.session.accessed + v = flask.session.get('value', 'None') + assert flask.session.accessed + return v + client.get('/get?new=1') assert client.post('/set', data={'value': '42'}).data == b'value set' assert client.get('/get').data == b'42'