From 96b4613a8f1c126ad607c5472bc38e6f5adb4390 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Sun, 9 Apr 2023 23:59:55 -0700 Subject: [PATCH 1/9] Blackened --- examples/hello.py | 12 +- src/flask_session/__init__.py | 102 +++++++++------- src/flask_session/sessions.py | 177 +++++++++++++++++---------- test_session.py | 218 ++++++++++++++++++---------------- 4 files changed, 299 insertions(+), 210 deletions(-) diff --git a/examples/hello.py b/examples/hello.py index 5fd7257c..4de8aa6a 100644 --- a/examples/hello.py +++ b/examples/hello.py @@ -2,7 +2,7 @@ from flask_session import Session -SESSION_TYPE = 'redis' +SESSION_TYPE = "redis" app = Flask(__name__) @@ -10,15 +10,15 @@ Session(app) -@app.route('/set/') +@app.route("/set/") def set(): - session['key'] = 'value' - return 'ok' + session["key"] = "value" + return "ok" -@app.route('/get/') +@app.route("/get/") def get(): - return session.get('key', 'not set') + return session.get("key", "not set") if __name__ == "__main__": diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index e4d28547..a50cd05c 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -1,10 +1,15 @@ import os -from .sessions import NullSessionInterface, RedisSessionInterface, \ - MemcachedSessionInterface, FileSystemSessionInterface, \ - MongoDBSessionInterface, SqlAlchemySessionInterface +from .sessions import ( + NullSessionInterface, + RedisSessionInterface, + MemcachedSessionInterface, + FileSystemSessionInterface, + MongoDBSessionInterface, + SqlAlchemySessionInterface, +) -__version__ = '0.5.0' +__version__ = "0.5.0" class Session(object): @@ -51,47 +56,64 @@ def init_app(self, app): def _get_interface(self, app): config = app.config.copy() - config.setdefault('SESSION_TYPE', 'null') - config.setdefault('SESSION_PERMANENT', True) - config.setdefault('SESSION_USE_SIGNER', False) - config.setdefault('SESSION_KEY_PREFIX', 'session:') - config.setdefault('SESSION_REDIS', None) - config.setdefault('SESSION_MEMCACHED', None) - config.setdefault('SESSION_FILE_DIR', - os.path.join(os.getcwd(), 'flask_session')) - config.setdefault('SESSION_FILE_THRESHOLD', 500) - config.setdefault('SESSION_FILE_MODE', 384) - config.setdefault('SESSION_MONGODB', None) - config.setdefault('SESSION_MONGODB_DB', 'flask_session') - config.setdefault('SESSION_MONGODB_COLLECT', 'sessions') - config.setdefault('SESSION_SQLALCHEMY', None) - config.setdefault('SESSION_SQLALCHEMY_TABLE', 'sessions') - - if config['SESSION_TYPE'] == 'redis': + config.setdefault("SESSION_TYPE", "null") + config.setdefault("SESSION_PERMANENT", True) + config.setdefault("SESSION_USE_SIGNER", False) + config.setdefault("SESSION_KEY_PREFIX", "session:") + config.setdefault("SESSION_REDIS", None) + config.setdefault("SESSION_MEMCACHED", None) + config.setdefault( + "SESSION_FILE_DIR", os.path.join(os.getcwd(), "flask_session") + ) + config.setdefault("SESSION_FILE_THRESHOLD", 500) + config.setdefault("SESSION_FILE_MODE", 384) + config.setdefault("SESSION_MONGODB", None) + config.setdefault("SESSION_MONGODB_DB", "flask_session") + config.setdefault("SESSION_MONGODB_COLLECT", "sessions") + config.setdefault("SESSION_SQLALCHEMY", None) + config.setdefault("SESSION_SQLALCHEMY_TABLE", "sessions") + + if config["SESSION_TYPE"] == "redis": session_interface = RedisSessionInterface( - config['SESSION_REDIS'], config['SESSION_KEY_PREFIX'], - config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) - elif config['SESSION_TYPE'] == 'memcached': + config["SESSION_REDIS"], + config["SESSION_KEY_PREFIX"], + config["SESSION_USE_SIGNER"], + config["SESSION_PERMANENT"], + ) + elif config["SESSION_TYPE"] == "memcached": session_interface = MemcachedSessionInterface( - config['SESSION_MEMCACHED'], config['SESSION_KEY_PREFIX'], - config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) - elif config['SESSION_TYPE'] == 'filesystem': + config["SESSION_MEMCACHED"], + config["SESSION_KEY_PREFIX"], + config["SESSION_USE_SIGNER"], + config["SESSION_PERMANENT"], + ) + elif config["SESSION_TYPE"] == "filesystem": session_interface = FileSystemSessionInterface( - config['SESSION_FILE_DIR'], config['SESSION_FILE_THRESHOLD'], - config['SESSION_FILE_MODE'], config['SESSION_KEY_PREFIX'], - config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) - elif config['SESSION_TYPE'] == 'mongodb': + config["SESSION_FILE_DIR"], + config["SESSION_FILE_THRESHOLD"], + config["SESSION_FILE_MODE"], + config["SESSION_KEY_PREFIX"], + config["SESSION_USE_SIGNER"], + config["SESSION_PERMANENT"], + ) + elif config["SESSION_TYPE"] == "mongodb": session_interface = MongoDBSessionInterface( - config['SESSION_MONGODB'], config['SESSION_MONGODB_DB'], - config['SESSION_MONGODB_COLLECT'], - config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], - config['SESSION_PERMANENT']) - elif config['SESSION_TYPE'] == 'sqlalchemy': + config["SESSION_MONGODB"], + config["SESSION_MONGODB_DB"], + config["SESSION_MONGODB_COLLECT"], + config["SESSION_KEY_PREFIX"], + config["SESSION_USE_SIGNER"], + config["SESSION_PERMANENT"], + ) + elif config["SESSION_TYPE"] == "sqlalchemy": session_interface = SqlAlchemySessionInterface( - app, config['SESSION_SQLALCHEMY'], - config['SESSION_SQLALCHEMY_TABLE'], - config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], - config['SESSION_PERMANENT']) + app, + config["SESSION_SQLALCHEMY"], + config["SESSION_SQLALCHEMY_TABLE"], + config["SESSION_KEY_PREFIX"], + config["SESSION_USE_SIGNER"], + config["SESSION_PERMANENT"], + ) else: session_interface = NullSessionInterface() diff --git a/src/flask_session/sessions.py b/src/flask_session/sessions.py index b7bd122f..56b826b8 100644 --- a/src/flask_session/sessions.py +++ b/src/flask_session/sessions.py @@ -2,6 +2,7 @@ import time from datetime import datetime from uuid import uuid4 + try: import cPickle as pickle except ImportError: @@ -30,6 +31,7 @@ class ServerSideSession(CallbackDict, SessionMixin): def __init__(self, initial=None, sid=None, permanent=None): def on_update(self): self.modified = True + CallbackDict.__init__(self, initial, on_update) self.sid = sid if permanent: @@ -58,20 +60,17 @@ class SqlAlchemySession(ServerSideSession): class SessionInterface(FlaskSessionInterface): - def _generate_sid(self): return str(uuid4()) def _get_signer(self, app): if not app.secret_key: return None - return Signer(app.secret_key, salt='flask-session', - key_derivation='hmac') + return Signer(app.secret_key, salt="flask-session", key_derivation="hmac") class NullSessionInterface(SessionInterface): - """Used to open a :class:`flask.sessions.NullSession` instance. - """ + """Used to open a :class:`flask.sessions.NullSession` instance.""" def open_session(self, app, request): return None @@ -95,6 +94,7 @@ class RedisSessionInterface(SessionInterface): def __init__(self, redis, key_prefix, use_signer=False, permanent=True): if redis is None: from redis import Redis + redis = Redis() self.redis = redis self.key_prefix = key_prefix @@ -119,7 +119,7 @@ def open_session(self, app, request): return self.session_class(sid=sid, permanent=self.permanent) if not PY2 and not isinstance(sid, text_type): - sid = sid.decode('utf-8', 'strict') + sid = sid.decode("utf-8", "strict") val = self.redis.get(self.key_prefix + sid) if val is not None: try: @@ -135,8 +135,9 @@ def save_session(self, app, session, response): if not session: if session.modified: self.redis.delete(self.key_prefix + session.sid) - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return # Modification case. There are upsides and downsides to @@ -155,16 +156,25 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session)) - self.redis.setex(name=self.key_prefix + session.sid, value=val, - time=total_seconds(app.permanent_session_lifetime)) + self.redis.setex( + name=self.key_prefix + session.sid, + value=val, + time=total_seconds(app.permanent_session_lifetime), + ) if self.use_signer: session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) class MemcachedSessionInterface(SessionInterface): @@ -186,7 +196,7 @@ def __init__(self, client, key_prefix, use_signer=False, permanent=True): if client is None: client = self._get_preferred_memcache_client() if client is None: - raise RuntimeError('no memcache module found') + raise RuntimeError("no memcache module found") self.client = client self.key_prefix = key_prefix self.use_signer = use_signer @@ -194,7 +204,7 @@ def __init__(self, client, key_prefix, use_signer=False, permanent=True): self.has_same_site_capability = hasattr(self, "get_cookie_samesite") def _get_preferred_memcache_client(self): - servers = ['127.0.0.1:11211'] + servers = ["127.0.0.1:11211"] try: import pylibmc except ImportError: @@ -242,7 +252,7 @@ def open_session(self, app, request): full_session_key = self.key_prefix + sid if PY2 and isinstance(full_session_key, unicode): - full_session_key = full_session_key.encode('utf-8') + full_session_key = full_session_key.encode("utf-8") val = self.client.get(full_session_key) if val is not None: try: @@ -259,12 +269,13 @@ def save_session(self, app, session, response): path = self.get_cookie_path(app) full_session_key = self.key_prefix + session.sid if PY2 and isinstance(full_session_key, unicode): - full_session_key = full_session_key.encode('utf-8') + full_session_key = full_session_key.encode("utf-8") if not session: if session.modified: self.client.delete(full_session_key) - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return conditional_cookie_kwargs = {} @@ -277,16 +288,25 @@ def save_session(self, app, session, response): val = self.serializer.dumps(dict(session), 0) else: val = self.serializer.dumps(dict(session)) - self.client.set(full_session_key, val, self._get_memcache_timeout( - total_seconds(app.permanent_session_lifetime))) + self.client.set( + full_session_key, + val, + self._get_memcache_timeout(total_seconds(app.permanent_session_lifetime)), + ) if self.use_signer: session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) class FileSystemSessionInterface(SessionInterface): @@ -306,9 +326,11 @@ class FileSystemSessionInterface(SessionInterface): session_class = FileSystemSession - def __init__(self, cache_dir, threshold, mode, key_prefix, - use_signer=False, permanent=True): + def __init__( + self, cache_dir, threshold, mode, key_prefix, use_signer=False, permanent=True + ): from cachelib.file import FileSystemCache + self.cache = FileSystemCache(cache_dir, threshold=threshold, mode=mode) self.key_prefix = key_prefix self.use_signer = use_signer @@ -342,8 +364,9 @@ def save_session(self, app, session, response): if not session: if session.modified: self.cache.delete(self.key_prefix + session.sid) - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return conditional_cookie_kwargs = {} @@ -353,16 +376,25 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) data = dict(session) - self.cache.set(self.key_prefix + session.sid, data, - total_seconds(app.permanent_session_lifetime)) + self.cache.set( + self.key_prefix + session.sid, + data, + total_seconds(app.permanent_session_lifetime), + ) if self.use_signer: session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) class MongoDBSessionInterface(SessionInterface): @@ -382,10 +414,12 @@ class MongoDBSessionInterface(SessionInterface): serializer = pickle session_class = MongoDBSession - def __init__(self, client, db, collection, key_prefix, use_signer=False, - permanent=True): + def __init__( + self, client, db, collection, key_prefix, use_signer=False, permanent=True + ): if client is None: from pymongo import MongoClient + client = MongoClient() self.client = client self.store = client[db][collection] @@ -411,14 +445,14 @@ def open_session(self, app, request): return self.session_class(sid=sid, permanent=self.permanent) store_id = self.key_prefix + sid - document = self.store.find_one({'id': store_id}) - if document and document.get('expiration') <= datetime.utcnow(): + document = self.store.find_one({"id": store_id}) + if document and document.get("expiration") <= datetime.utcnow(): # Delete expired session - self.store.remove({'id': store_id}) + self.store.remove({"id": store_id}) document = None if document is not None: try: - val = document['val'] + val = document["val"] data = self.serializer.loads(want_bytes(val)) return self.session_class(data, sid=sid) except: @@ -431,9 +465,10 @@ def save_session(self, app, session, response): store_id = self.key_prefix + session.sid if not session: if session.modified: - self.store.remove({'id': store_id}) - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + self.store.remove({"id": store_id}) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return conditional_cookie_kwargs = {} @@ -443,18 +478,23 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session)) - self.store.update({'id': store_id}, - {'id': store_id, - 'val': val, - 'expiration': expires}, True) + self.store.update( + {"id": store_id}, {"id": store_id, "val": val, "expiration": expires}, True + ) if self.use_signer: session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) class SqlAlchemySessionInterface(SessionInterface): @@ -473,10 +513,10 @@ class SqlAlchemySessionInterface(SessionInterface): serializer = pickle session_class = SqlAlchemySession - def __init__(self, app, db, table, key_prefix, use_signer=False, - permanent=True): + def __init__(self, app, db, table, key_prefix, use_signer=False, permanent=True): if db is None: from flask_sqlalchemy import SQLAlchemy + db = SQLAlchemy(app) self.db = db self.key_prefix = key_prefix @@ -498,7 +538,7 @@ def __init__(self, session_id, data, expiry): self.expiry = expiry def __repr__(self): - return '' % self.data + return "" % self.data # self.db.create_all() self.sql_session_model = Session @@ -521,7 +561,8 @@ def open_session(self, app, request): store_id = self.key_prefix + sid saved_session = self.sql_session_model.query.filter_by( - session_id=store_id).first() + session_id=store_id + ).first() if saved_session and saved_session.expiry <= datetime.utcnow(): # Delete expired session self.db.session.delete(saved_session) @@ -541,14 +582,16 @@ def save_session(self, app, session, response): path = self.get_cookie_path(app) store_id = self.key_prefix + session.sid saved_session = self.sql_session_model.query.filter_by( - session_id=store_id).first() + session_id=store_id + ).first() if not session: if session.modified: if saved_session: self.db.session.delete(saved_session) self.db.session.commit() - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return conditional_cookie_kwargs = {} @@ -570,7 +613,13 @@ def save_session(self, app, session, response): session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) diff --git a/test_session.py b/test_session.py index 6fb72a4e..8df03450 100644 --- a/test_session.py +++ b/test_session.py @@ -6,177 +6,195 @@ class FlaskSessionTestCase(unittest.TestCase): - def test_null_session(self): app = flask.Flask(__name__) Session(app) + def expect_exception(f, *args, **kwargs): try: f(*args, **kwargs) except RuntimeError as e: - self.assertTrue(e.args and 'session is unavailable' in e.args[0]) + self.assertTrue(e.args and "session is unavailable" in e.args[0]) else: - self.assertTrue(False, 'expected exception') + self.assertTrue(False, "expected exception") + with app.test_request_context(): - self.assertTrue(flask.session.get('missing_key') is None) - expect_exception(flask.session.__setitem__, 'foo', 42) - expect_exception(flask.session.pop, 'foo') + self.assertTrue(flask.session.get("missing_key") is None) + expect_exception(flask.session.__setitem__, "foo", 42) + expect_exception(flask.session.pop, "foo") def test_redis_session(self): app = flask.Flask(__name__) - app.config['SESSION_TYPE'] = 'redis' + app.config["SESSION_TYPE"] = "redis" Session(app) - @app.route('/set', methods=['POST']) + + @app.route("/set", methods=["POST"]) def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get") def get(): - return flask.session['value'] - @app.route('/delete', methods=['POST']) + return flask.session["value"] + + @app.route("/delete", methods=["POST"]) def delete(): - del flask.session['value'] - return 'value deleted' + del flask.session["value"] + return "value deleted" c = app.test_client() - self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') - self.assertEqual(c.get('/get').data, b'42') - c.post('/delete') - - + self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") + self.assertEqual(c.get("/get").data, b"42") + c.post("/delete") + def test_memcached_session(self): app = flask.Flask(__name__) - app.config['SESSION_TYPE'] = 'memcached' + app.config["SESSION_TYPE"] = "memcached" Session(app) - @app.route('/set', methods=['POST']) + + @app.route("/set", methods=["POST"]) def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get") def get(): - return flask.session['value'] - @app.route('/delete', methods=['POST']) + return flask.session["value"] + + @app.route("/delete", methods=["POST"]) def delete(): - del flask.session['value'] - return 'value deleted' + del flask.session["value"] + return "value deleted" c = app.test_client() - self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') - self.assertEqual(c.get('/get').data, b'42') - c.post('/delete') - - + self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") + self.assertEqual(c.get("/get").data, b"42") + c.post("/delete") + def test_filesystem_session(self): app = flask.Flask(__name__) - app.config['SESSION_TYPE'] = 'filesystem' - app.config['SESSION_FILE_DIR'] = tempfile.gettempdir() + app.config["SESSION_TYPE"] = "filesystem" + app.config["SESSION_FILE_DIR"] = tempfile.gettempdir() Session(app) - @app.route('/set', methods=['POST']) + + @app.route("/set", methods=["POST"]) def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get") def get(): - return flask.session['value'] - @app.route('/delete', methods=['POST']) + return flask.session["value"] + + @app.route("/delete", methods=["POST"]) def delete(): - del flask.session['value'] - return 'value deleted' + del flask.session["value"] + return "value deleted" c = app.test_client() - self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') - self.assertEqual(c.get('/get').data, b'42') - c.post('/delete') - + self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") + self.assertEqual(c.get("/get").data, b"42") + c.post("/delete") + def test_mongodb_session(self): app = flask.Flask(__name__) app.testing = True - app.config['SESSION_TYPE'] = 'mongodb' + app.config["SESSION_TYPE"] = "mongodb" Session(app) - @app.route('/set', methods=['POST']) + + @app.route("/set", methods=["POST"]) def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get") def get(): - return flask.session['value'] - @app.route('/delete', methods=['POST']) + return flask.session["value"] + + @app.route("/delete", methods=["POST"]) def delete(): - del flask.session['value'] - return 'value deleted' + del flask.session["value"] + return "value deleted" c = app.test_client() - self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') - self.assertEqual(c.get('/get').data, b'42') - c.post('/delete') + self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") + self.assertEqual(c.get("/get").data, b"42") + c.post("/delete") def test_flasksqlalchemy_session(self): app = flask.Flask(__name__) app.debug = True - app.config['SESSION_TYPE'] = 'sqlalchemy' - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config["SESSION_TYPE"] = "sqlalchemy" + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" Session(app) - @app.route('/set', methods=['POST']) + + @app.route("/set", methods=["POST"]) def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get") def get(): - return flask.session['value'] - @app.route('/delete', methods=['POST']) + return flask.session["value"] + + @app.route("/delete", methods=["POST"]) def delete(): - del flask.session['value'] - return 'value deleted' + del flask.session["value"] + return "value deleted" c = app.test_client() - self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value ' - b'set') - self.assertEqual(c.get('/get').data, b'42') - c.post('/delete') + self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value " b"set") + self.assertEqual(c.get("/get").data, b"42") + c.post("/delete") def test_flasksqlalchemy_session_with_signer(self): app = flask.Flask(__name__) app.debug = True - app.secret_key = 'test_secret_key' - app.config['SESSION_TYPE'] = 'sqlalchemy' - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' - app.config['SESSION_USE_SIGNER'] = True + app.secret_key = "test_secret_key" + app.config["SESSION_TYPE"] = "sqlalchemy" + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + app.config["SESSION_USE_SIGNER"] = True session = Session(app) - @app.route('/set', methods=['POST']) + + @app.route("/set", methods=["POST"]) def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get") def get(): - return flask.session['value'] - @app.route('/delete', methods=['POST']) + return flask.session["value"] + + @app.route("/delete", methods=["POST"]) def delete(): - del flask.session['value'] - return 'value deleted' + del flask.session["value"] + return "value deleted" c = app.test_client() - self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value ' - b'set') - self.assertEqual(c.get('/get').data, b'42') - c.post('/delete') + self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value " b"set") + self.assertEqual(c.get("/get").data, b"42") + c.post("/delete") def test_session_use_signer(self): app = flask.Flask(__name__) - app.secret_key = 'test_secret_key' - app.config['SESSION_TYPE'] = 'redis' - app.config['SESSION_USE_SIGNER'] = True + app.secret_key = "test_secret_key" + app.config["SESSION_TYPE"] = "redis" + app.config["SESSION_USE_SIGNER"] = True Session(app) - @app.route('/set', methods=['POST']) + + @app.route("/set", methods=["POST"]) def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get") def get(): - return flask.session['value'] + return flask.session["value"] c = app.test_client() - self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') - self.assertEqual(c.get('/get').data, b'42') + self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") + self.assertEqual(c.get("/get").data, b"42") + if __name__ == "__main__": unittest.main() From 73166f72c34c92f92794fabe24839da10ed3670d Mon Sep 17 00:00:00 2001 From: Lukas Holecek Date: Mon, 2 Oct 2023 11:22:36 +0200 Subject: [PATCH 2/9] Add support for werkzeug-3.0.0 ...and in turn Flask 3.0.0 which requires that version. Werkzeug removed support for passing bytes where str is expected. See: https://github.com/pallets/werkzeug/commit/5ff0a573f4b78d9724f1f063fb058fd6bc76b24d --- src/flask_session/sessions.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/flask_session/sessions.py b/src/flask_session/sessions.py index b7bd122f..db2438a0 100644 --- a/src/flask_session/sessions.py +++ b/src/flask_session/sessions.py @@ -68,6 +68,9 @@ def _get_signer(self, app): return Signer(app.secret_key, salt='flask-session', key_derivation='hmac') + def _sign(self, app, sid): + return self._get_signer(app).sign(want_bytes(sid)).decode("utf-8") + class NullSessionInterface(SessionInterface): """Used to open a :class:`flask.sessions.NullSession` instance. @@ -158,7 +161,7 @@ def save_session(self, app, session, response): self.redis.setex(name=self.key_prefix + session.sid, value=val, time=total_seconds(app.permanent_session_lifetime)) if self.use_signer: - session_id = self._get_signer(app).sign(want_bytes(session.sid)) + session_id = self._sign(app, session.sid) else: session_id = session.sid response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, @@ -280,7 +283,7 @@ def save_session(self, app, session, response): self.client.set(full_session_key, val, self._get_memcache_timeout( total_seconds(app.permanent_session_lifetime))) if self.use_signer: - session_id = self._get_signer(app).sign(want_bytes(session.sid)) + session_id = self._sign(app, session.sid) else: session_id = session.sid response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, @@ -356,7 +359,7 @@ def save_session(self, app, session, response): self.cache.set(self.key_prefix + session.sid, data, total_seconds(app.permanent_session_lifetime)) if self.use_signer: - session_id = self._get_signer(app).sign(want_bytes(session.sid)) + session_id = self._sign(app, session.sid) else: session_id = session.sid response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, @@ -448,7 +451,7 @@ def save_session(self, app, session, response): 'val': val, 'expiration': expires}, True) if self.use_signer: - session_id = self._get_signer(app).sign(want_bytes(session.sid)) + session_id = self._sign(app, session.sid) else: session_id = session.sid response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, @@ -567,7 +570,7 @@ def save_session(self, app, session, response): self.db.session.add(new_session) self.db.session.commit() if self.use_signer: - session_id = self._get_signer(app).sign(want_bytes(session.sid)) + session_id = self._sign(app, session.sid) else: session_id = session.sid response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, From 3829eede572c0b95e96e7b0425d64b268ee450a3 Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 8 Nov 2023 15:57:04 +1100 Subject: [PATCH 3/9] Black formatting --- src/flask_session/sessions.py | 177 ++++++++++++++++++++++------------ 1 file changed, 113 insertions(+), 64 deletions(-) diff --git a/src/flask_session/sessions.py b/src/flask_session/sessions.py index db2438a0..9e1c1431 100644 --- a/src/flask_session/sessions.py +++ b/src/flask_session/sessions.py @@ -2,6 +2,7 @@ import time from datetime import datetime from uuid import uuid4 + try: import cPickle as pickle except ImportError: @@ -30,6 +31,7 @@ class ServerSideSession(CallbackDict, SessionMixin): def __init__(self, initial=None, sid=None, permanent=None): def on_update(self): self.modified = True + CallbackDict.__init__(self, initial, on_update) self.sid = sid if permanent: @@ -58,23 +60,20 @@ class SqlAlchemySession(ServerSideSession): class SessionInterface(FlaskSessionInterface): - def _generate_sid(self): return str(uuid4()) def _get_signer(self, app): if not app.secret_key: return None - return Signer(app.secret_key, salt='flask-session', - key_derivation='hmac') + return Signer(app.secret_key, salt="flask-session", key_derivation="hmac") def _sign(self, app, sid): return self._get_signer(app).sign(want_bytes(sid)).decode("utf-8") class NullSessionInterface(SessionInterface): - """Used to open a :class:`flask.sessions.NullSession` instance. - """ + """Used to open a :class:`flask.sessions.NullSession` instance.""" def open_session(self, app, request): return None @@ -98,6 +97,7 @@ class RedisSessionInterface(SessionInterface): def __init__(self, redis, key_prefix, use_signer=False, permanent=True): if redis is None: from redis import Redis + redis = Redis() self.redis = redis self.key_prefix = key_prefix @@ -122,7 +122,7 @@ def open_session(self, app, request): return self.session_class(sid=sid, permanent=self.permanent) if not PY2 and not isinstance(sid, text_type): - sid = sid.decode('utf-8', 'strict') + sid = sid.decode("utf-8", "strict") val = self.redis.get(self.key_prefix + sid) if val is not None: try: @@ -138,8 +138,9 @@ def save_session(self, app, session, response): if not session: if session.modified: self.redis.delete(self.key_prefix + session.sid) - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return # Modification case. There are upsides and downsides to @@ -158,16 +159,25 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session)) - self.redis.setex(name=self.key_prefix + session.sid, value=val, - time=total_seconds(app.permanent_session_lifetime)) + self.redis.setex( + name=self.key_prefix + session.sid, + value=val, + time=total_seconds(app.permanent_session_lifetime), + ) if self.use_signer: session_id = self._sign(app, session.sid) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) class MemcachedSessionInterface(SessionInterface): @@ -189,7 +199,7 @@ def __init__(self, client, key_prefix, use_signer=False, permanent=True): if client is None: client = self._get_preferred_memcache_client() if client is None: - raise RuntimeError('no memcache module found') + raise RuntimeError("no memcache module found") self.client = client self.key_prefix = key_prefix self.use_signer = use_signer @@ -197,7 +207,7 @@ def __init__(self, client, key_prefix, use_signer=False, permanent=True): self.has_same_site_capability = hasattr(self, "get_cookie_samesite") def _get_preferred_memcache_client(self): - servers = ['127.0.0.1:11211'] + servers = ["127.0.0.1:11211"] try: import pylibmc except ImportError: @@ -245,7 +255,7 @@ def open_session(self, app, request): full_session_key = self.key_prefix + sid if PY2 and isinstance(full_session_key, unicode): - full_session_key = full_session_key.encode('utf-8') + full_session_key = full_session_key.encode("utf-8") val = self.client.get(full_session_key) if val is not None: try: @@ -262,12 +272,13 @@ def save_session(self, app, session, response): path = self.get_cookie_path(app) full_session_key = self.key_prefix + session.sid if PY2 and isinstance(full_session_key, unicode): - full_session_key = full_session_key.encode('utf-8') + full_session_key = full_session_key.encode("utf-8") if not session: if session.modified: self.client.delete(full_session_key) - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return conditional_cookie_kwargs = {} @@ -280,16 +291,25 @@ def save_session(self, app, session, response): val = self.serializer.dumps(dict(session), 0) else: val = self.serializer.dumps(dict(session)) - self.client.set(full_session_key, val, self._get_memcache_timeout( - total_seconds(app.permanent_session_lifetime))) + self.client.set( + full_session_key, + val, + self._get_memcache_timeout(total_seconds(app.permanent_session_lifetime)), + ) if self.use_signer: session_id = self._sign(app, session.sid) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) class FileSystemSessionInterface(SessionInterface): @@ -309,9 +329,11 @@ class FileSystemSessionInterface(SessionInterface): session_class = FileSystemSession - def __init__(self, cache_dir, threshold, mode, key_prefix, - use_signer=False, permanent=True): + def __init__( + self, cache_dir, threshold, mode, key_prefix, use_signer=False, permanent=True + ): from cachelib.file import FileSystemCache + self.cache = FileSystemCache(cache_dir, threshold=threshold, mode=mode) self.key_prefix = key_prefix self.use_signer = use_signer @@ -345,8 +367,9 @@ def save_session(self, app, session, response): if not session: if session.modified: self.cache.delete(self.key_prefix + session.sid) - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return conditional_cookie_kwargs = {} @@ -356,16 +379,25 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) data = dict(session) - self.cache.set(self.key_prefix + session.sid, data, - total_seconds(app.permanent_session_lifetime)) + self.cache.set( + self.key_prefix + session.sid, + data, + total_seconds(app.permanent_session_lifetime), + ) if self.use_signer: session_id = self._sign(app, session.sid) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) class MongoDBSessionInterface(SessionInterface): @@ -385,10 +417,12 @@ class MongoDBSessionInterface(SessionInterface): serializer = pickle session_class = MongoDBSession - def __init__(self, client, db, collection, key_prefix, use_signer=False, - permanent=True): + def __init__( + self, client, db, collection, key_prefix, use_signer=False, permanent=True + ): if client is None: from pymongo import MongoClient + client = MongoClient() self.client = client self.store = client[db][collection] @@ -414,14 +448,14 @@ def open_session(self, app, request): return self.session_class(sid=sid, permanent=self.permanent) store_id = self.key_prefix + sid - document = self.store.find_one({'id': store_id}) - if document and document.get('expiration') <= datetime.utcnow(): + document = self.store.find_one({"id": store_id}) + if document and document.get("expiration") <= datetime.utcnow(): # Delete expired session - self.store.remove({'id': store_id}) + self.store.remove({"id": store_id}) document = None if document is not None: try: - val = document['val'] + val = document["val"] data = self.serializer.loads(want_bytes(val)) return self.session_class(data, sid=sid) except: @@ -434,9 +468,10 @@ def save_session(self, app, session, response): store_id = self.key_prefix + session.sid if not session: if session.modified: - self.store.remove({'id': store_id}) - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + self.store.remove({"id": store_id}) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return conditional_cookie_kwargs = {} @@ -446,18 +481,23 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session)) - self.store.update({'id': store_id}, - {'id': store_id, - 'val': val, - 'expiration': expires}, True) + self.store.update( + {"id": store_id}, {"id": store_id, "val": val, "expiration": expires}, True + ) if self.use_signer: session_id = self._sign(app, session.sid) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) class SqlAlchemySessionInterface(SessionInterface): @@ -476,10 +516,10 @@ class SqlAlchemySessionInterface(SessionInterface): serializer = pickle session_class = SqlAlchemySession - def __init__(self, app, db, table, key_prefix, use_signer=False, - permanent=True): + def __init__(self, app, db, table, key_prefix, use_signer=False, permanent=True): if db is None: from flask_sqlalchemy import SQLAlchemy + db = SQLAlchemy(app) self.db = db self.key_prefix = key_prefix @@ -501,7 +541,7 @@ def __init__(self, session_id, data, expiry): self.expiry = expiry def __repr__(self): - return '' % self.data + return "" % self.data # self.db.create_all() self.sql_session_model = Session @@ -524,7 +564,8 @@ def open_session(self, app, request): store_id = self.key_prefix + sid saved_session = self.sql_session_model.query.filter_by( - session_id=store_id).first() + session_id=store_id + ).first() if saved_session and saved_session.expiry <= datetime.utcnow(): # Delete expired session self.db.session.delete(saved_session) @@ -544,14 +585,16 @@ def save_session(self, app, session, response): path = self.get_cookie_path(app) store_id = self.key_prefix + session.sid saved_session = self.sql_session_model.query.filter_by( - session_id=store_id).first() + session_id=store_id + ).first() if not session: if session.modified: if saved_session: self.db.session.delete(saved_session) self.db.session.commit() - response.delete_cookie(app.config["SESSION_COOKIE_NAME"], - domain=domain, path=path) + response.delete_cookie( + app.config["SESSION_COOKIE_NAME"], domain=domain, path=path + ) return conditional_cookie_kwargs = {} @@ -573,7 +616,13 @@ def save_session(self, app, session, response): session_id = self._sign(app, session.sid) else: session_id = session.sid - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - **conditional_cookie_kwargs) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + **conditional_cookie_kwargs, + ) From 0fd8a7c1066d380da9515c70fdc6d42ffaa741f0 Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 8 Nov 2023 19:52:39 +1100 Subject: [PATCH 4/9] Explicit sign and unsign, improve absent secret_key error message Previous lack of SECRET_KEY with SESSION_USE_SIGNER=True was: AttributeError: 'NoneType' object has no attribute 'sign' Now it is: KeyError: 'SECRET_KEY must be set when SESSION_USE_SIGNER=True' --- src/flask_session/sessions.py | 44 +++++++++++++---------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/flask_session/sessions.py b/src/flask_session/sessions.py index 9e1c1431..920e905e 100644 --- a/src/flask_session/sessions.py +++ b/src/flask_session/sessions.py @@ -64,12 +64,20 @@ def _generate_sid(self): return str(uuid4()) def _get_signer(self, app): - if not app.secret_key: - return None + if not hasattr(app, "secret_key") or not app.secret_key: + raise KeyError("SECRET_KEY must be set when SESSION_USE_SIGNER=True") return Signer(app.secret_key, salt="flask-session", key_derivation="hmac") + def _unsign(self, app, sid): + signer = self._get_signer(app) + sid_as_bytes = signer.unsign(sid) + sid = sid_as_bytes.decode() + return sid + def _sign(self, app, sid): - return self._get_signer(app).sign(want_bytes(sid)).decode("utf-8") + signer = self._get_signer(app) + sid_as_bytes = want_bytes(sid) + return signer.sign(sid_as_bytes).decode("utf-8") class NullSessionInterface(SessionInterface): @@ -111,12 +119,8 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: - signer = self._get_signer(app) - if signer is None: - return None try: - sid_as_bytes = signer.unsign(sid) - sid = sid_as_bytes.decode() + sid = self._unsign(app, sid) except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -243,12 +247,8 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: - signer = self._get_signer(app) - if signer is None: - return None try: - sid_as_bytes = signer.unsign(sid) - sid = sid_as_bytes.decode() + sid = self._unsign(app, sid) except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -346,12 +346,8 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: - signer = self._get_signer(app) - if signer is None: - return None try: - sid_as_bytes = signer.unsign(sid) - sid = sid_as_bytes.decode() + sid = self._unsign(app, sid) except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -437,12 +433,8 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: - signer = self._get_signer(app) - if signer is None: - return None try: - sid_as_bytes = signer.unsign(sid) - sid = sid_as_bytes.decode() + sid = self._unsign(app, sid) except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -552,12 +544,8 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: - signer = self._get_signer(app) - if signer is None: - return None try: - sid_as_bytes = signer.unsign(sid) - sid = sid_as_bytes.decode() + sid = self._unsign(app, sid) except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) From 6f9f6a3c3812456c1fdb713f43f66849eccb99ce Mon Sep 17 00:00:00 2001 From: Lex Date: Sat, 13 Jan 2024 14:48:38 +1000 Subject: [PATCH 5/9] Revert a9d001 --- src/flask_session/sessions.py | 55 ++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/flask_session/sessions.py b/src/flask_session/sessions.py index 920e905e..56b826b8 100644 --- a/src/flask_session/sessions.py +++ b/src/flask_session/sessions.py @@ -64,21 +64,10 @@ def _generate_sid(self): return str(uuid4()) def _get_signer(self, app): - if not hasattr(app, "secret_key") or not app.secret_key: - raise KeyError("SECRET_KEY must be set when SESSION_USE_SIGNER=True") + if not app.secret_key: + return None return Signer(app.secret_key, salt="flask-session", key_derivation="hmac") - def _unsign(self, app, sid): - signer = self._get_signer(app) - sid_as_bytes = signer.unsign(sid) - sid = sid_as_bytes.decode() - return sid - - def _sign(self, app, sid): - signer = self._get_signer(app) - sid_as_bytes = want_bytes(sid) - return signer.sign(sid_as_bytes).decode("utf-8") - class NullSessionInterface(SessionInterface): """Used to open a :class:`flask.sessions.NullSession` instance.""" @@ -119,8 +108,12 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: + signer = self._get_signer(app) + if signer is None: + return None try: - sid = self._unsign(app, sid) + sid_as_bytes = signer.unsign(sid) + sid = sid_as_bytes.decode() except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -169,7 +162,7 @@ def save_session(self, app, session, response): time=total_seconds(app.permanent_session_lifetime), ) if self.use_signer: - session_id = self._sign(app, session.sid) + session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid response.set_cookie( @@ -247,8 +240,12 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: + signer = self._get_signer(app) + if signer is None: + return None try: - sid = self._unsign(app, sid) + sid_as_bytes = signer.unsign(sid) + sid = sid_as_bytes.decode() except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -297,7 +294,7 @@ def save_session(self, app, session, response): self._get_memcache_timeout(total_seconds(app.permanent_session_lifetime)), ) if self.use_signer: - session_id = self._sign(app, session.sid) + session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid response.set_cookie( @@ -346,8 +343,12 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: + signer = self._get_signer(app) + if signer is None: + return None try: - sid = self._unsign(app, sid) + sid_as_bytes = signer.unsign(sid) + sid = sid_as_bytes.decode() except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -381,7 +382,7 @@ def save_session(self, app, session, response): total_seconds(app.permanent_session_lifetime), ) if self.use_signer: - session_id = self._sign(app, session.sid) + session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid response.set_cookie( @@ -433,8 +434,12 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: + signer = self._get_signer(app) + if signer is None: + return None try: - sid = self._unsign(app, sid) + sid_as_bytes = signer.unsign(sid) + sid = sid_as_bytes.decode() except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -477,7 +482,7 @@ def save_session(self, app, session, response): {"id": store_id}, {"id": store_id, "val": val, "expiration": expires}, True ) if self.use_signer: - session_id = self._sign(app, session.sid) + session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid response.set_cookie( @@ -544,8 +549,12 @@ def open_session(self, app, request): sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) if self.use_signer: + signer = self._get_signer(app) + if signer is None: + return None try: - sid = self._unsign(app, sid) + sid_as_bytes = signer.unsign(sid) + sid = sid_as_bytes.decode() except BadSignature: sid = self._generate_sid() return self.session_class(sid=sid, permanent=self.permanent) @@ -601,7 +610,7 @@ def save_session(self, app, session, response): self.db.session.add(new_session) self.db.session.commit() if self.use_signer: - session_id = self._sign(app, session.sid) + session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid response.set_cookie( From 35986d2a6e0beabf1a8aecb10bb7c8dac206b587 Mon Sep 17 00:00:00 2001 From: Lex Date: Sat, 13 Jan 2024 14:50:24 +1000 Subject: [PATCH 6/9] Revert 4f086f Revert these changes in order to merge Development branch --- examples/hello.py | 12 +- src/flask_session/__init__.py | 102 +++++++--------- src/flask_session/sessions.py | 177 ++++++++++----------------- test_session.py | 218 ++++++++++++++++------------------ 4 files changed, 210 insertions(+), 299 deletions(-) diff --git a/examples/hello.py b/examples/hello.py index 4de8aa6a..5fd7257c 100644 --- a/examples/hello.py +++ b/examples/hello.py @@ -2,7 +2,7 @@ from flask_session import Session -SESSION_TYPE = "redis" +SESSION_TYPE = 'redis' app = Flask(__name__) @@ -10,15 +10,15 @@ Session(app) -@app.route("/set/") +@app.route('/set/') def set(): - session["key"] = "value" - return "ok" + session['key'] = 'value' + return 'ok' -@app.route("/get/") +@app.route('/get/') def get(): - return session.get("key", "not set") + return session.get('key', 'not set') if __name__ == "__main__": diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index a50cd05c..e4d28547 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -1,15 +1,10 @@ import os -from .sessions import ( - NullSessionInterface, - RedisSessionInterface, - MemcachedSessionInterface, - FileSystemSessionInterface, - MongoDBSessionInterface, - SqlAlchemySessionInterface, -) +from .sessions import NullSessionInterface, RedisSessionInterface, \ + MemcachedSessionInterface, FileSystemSessionInterface, \ + MongoDBSessionInterface, SqlAlchemySessionInterface -__version__ = "0.5.0" +__version__ = '0.5.0' class Session(object): @@ -56,64 +51,47 @@ def init_app(self, app): def _get_interface(self, app): config = app.config.copy() - config.setdefault("SESSION_TYPE", "null") - config.setdefault("SESSION_PERMANENT", True) - config.setdefault("SESSION_USE_SIGNER", False) - config.setdefault("SESSION_KEY_PREFIX", "session:") - config.setdefault("SESSION_REDIS", None) - config.setdefault("SESSION_MEMCACHED", None) - config.setdefault( - "SESSION_FILE_DIR", os.path.join(os.getcwd(), "flask_session") - ) - config.setdefault("SESSION_FILE_THRESHOLD", 500) - config.setdefault("SESSION_FILE_MODE", 384) - config.setdefault("SESSION_MONGODB", None) - config.setdefault("SESSION_MONGODB_DB", "flask_session") - config.setdefault("SESSION_MONGODB_COLLECT", "sessions") - config.setdefault("SESSION_SQLALCHEMY", None) - config.setdefault("SESSION_SQLALCHEMY_TABLE", "sessions") - - if config["SESSION_TYPE"] == "redis": + config.setdefault('SESSION_TYPE', 'null') + config.setdefault('SESSION_PERMANENT', True) + config.setdefault('SESSION_USE_SIGNER', False) + config.setdefault('SESSION_KEY_PREFIX', 'session:') + config.setdefault('SESSION_REDIS', None) + config.setdefault('SESSION_MEMCACHED', None) + config.setdefault('SESSION_FILE_DIR', + os.path.join(os.getcwd(), 'flask_session')) + config.setdefault('SESSION_FILE_THRESHOLD', 500) + config.setdefault('SESSION_FILE_MODE', 384) + config.setdefault('SESSION_MONGODB', None) + config.setdefault('SESSION_MONGODB_DB', 'flask_session') + config.setdefault('SESSION_MONGODB_COLLECT', 'sessions') + config.setdefault('SESSION_SQLALCHEMY', None) + config.setdefault('SESSION_SQLALCHEMY_TABLE', 'sessions') + + if config['SESSION_TYPE'] == 'redis': session_interface = RedisSessionInterface( - config["SESSION_REDIS"], - config["SESSION_KEY_PREFIX"], - config["SESSION_USE_SIGNER"], - config["SESSION_PERMANENT"], - ) - elif config["SESSION_TYPE"] == "memcached": + config['SESSION_REDIS'], config['SESSION_KEY_PREFIX'], + config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) + elif config['SESSION_TYPE'] == 'memcached': session_interface = MemcachedSessionInterface( - config["SESSION_MEMCACHED"], - config["SESSION_KEY_PREFIX"], - config["SESSION_USE_SIGNER"], - config["SESSION_PERMANENT"], - ) - elif config["SESSION_TYPE"] == "filesystem": + config['SESSION_MEMCACHED'], config['SESSION_KEY_PREFIX'], + config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) + elif config['SESSION_TYPE'] == 'filesystem': session_interface = FileSystemSessionInterface( - config["SESSION_FILE_DIR"], - config["SESSION_FILE_THRESHOLD"], - config["SESSION_FILE_MODE"], - config["SESSION_KEY_PREFIX"], - config["SESSION_USE_SIGNER"], - config["SESSION_PERMANENT"], - ) - elif config["SESSION_TYPE"] == "mongodb": + config['SESSION_FILE_DIR'], config['SESSION_FILE_THRESHOLD'], + config['SESSION_FILE_MODE'], config['SESSION_KEY_PREFIX'], + config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) + elif config['SESSION_TYPE'] == 'mongodb': session_interface = MongoDBSessionInterface( - config["SESSION_MONGODB"], - config["SESSION_MONGODB_DB"], - config["SESSION_MONGODB_COLLECT"], - config["SESSION_KEY_PREFIX"], - config["SESSION_USE_SIGNER"], - config["SESSION_PERMANENT"], - ) - elif config["SESSION_TYPE"] == "sqlalchemy": + config['SESSION_MONGODB'], config['SESSION_MONGODB_DB'], + config['SESSION_MONGODB_COLLECT'], + config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], + config['SESSION_PERMANENT']) + elif config['SESSION_TYPE'] == 'sqlalchemy': session_interface = SqlAlchemySessionInterface( - app, - config["SESSION_SQLALCHEMY"], - config["SESSION_SQLALCHEMY_TABLE"], - config["SESSION_KEY_PREFIX"], - config["SESSION_USE_SIGNER"], - config["SESSION_PERMANENT"], - ) + app, config['SESSION_SQLALCHEMY'], + config['SESSION_SQLALCHEMY_TABLE'], + config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], + config['SESSION_PERMANENT']) else: session_interface = NullSessionInterface() diff --git a/src/flask_session/sessions.py b/src/flask_session/sessions.py index 56b826b8..b7bd122f 100644 --- a/src/flask_session/sessions.py +++ b/src/flask_session/sessions.py @@ -2,7 +2,6 @@ import time from datetime import datetime from uuid import uuid4 - try: import cPickle as pickle except ImportError: @@ -31,7 +30,6 @@ class ServerSideSession(CallbackDict, SessionMixin): def __init__(self, initial=None, sid=None, permanent=None): def on_update(self): self.modified = True - CallbackDict.__init__(self, initial, on_update) self.sid = sid if permanent: @@ -60,17 +58,20 @@ class SqlAlchemySession(ServerSideSession): class SessionInterface(FlaskSessionInterface): + def _generate_sid(self): return str(uuid4()) def _get_signer(self, app): if not app.secret_key: return None - return Signer(app.secret_key, salt="flask-session", key_derivation="hmac") + return Signer(app.secret_key, salt='flask-session', + key_derivation='hmac') class NullSessionInterface(SessionInterface): - """Used to open a :class:`flask.sessions.NullSession` instance.""" + """Used to open a :class:`flask.sessions.NullSession` instance. + """ def open_session(self, app, request): return None @@ -94,7 +95,6 @@ class RedisSessionInterface(SessionInterface): def __init__(self, redis, key_prefix, use_signer=False, permanent=True): if redis is None: from redis import Redis - redis = Redis() self.redis = redis self.key_prefix = key_prefix @@ -119,7 +119,7 @@ def open_session(self, app, request): return self.session_class(sid=sid, permanent=self.permanent) if not PY2 and not isinstance(sid, text_type): - sid = sid.decode("utf-8", "strict") + sid = sid.decode('utf-8', 'strict') val = self.redis.get(self.key_prefix + sid) if val is not None: try: @@ -135,9 +135,8 @@ def save_session(self, app, session, response): if not session: if session.modified: self.redis.delete(self.key_prefix + session.sid) - response.delete_cookie( - app.config["SESSION_COOKIE_NAME"], domain=domain, path=path - ) + response.delete_cookie(app.config["SESSION_COOKIE_NAME"], + domain=domain, path=path) return # Modification case. There are upsides and downsides to @@ -156,25 +155,16 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session)) - self.redis.setex( - name=self.key_prefix + session.sid, - value=val, - time=total_seconds(app.permanent_session_lifetime), - ) + self.redis.setex(name=self.key_prefix + session.sid, value=val, + time=total_seconds(app.permanent_session_lifetime)) if self.use_signer: session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie( - app.config["SESSION_COOKIE_NAME"], - session_id, - expires=expires, - httponly=httponly, - domain=domain, - path=path, - secure=secure, - **conditional_cookie_kwargs, - ) + response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, + expires=expires, httponly=httponly, + domain=domain, path=path, secure=secure, + **conditional_cookie_kwargs) class MemcachedSessionInterface(SessionInterface): @@ -196,7 +186,7 @@ def __init__(self, client, key_prefix, use_signer=False, permanent=True): if client is None: client = self._get_preferred_memcache_client() if client is None: - raise RuntimeError("no memcache module found") + raise RuntimeError('no memcache module found') self.client = client self.key_prefix = key_prefix self.use_signer = use_signer @@ -204,7 +194,7 @@ def __init__(self, client, key_prefix, use_signer=False, permanent=True): self.has_same_site_capability = hasattr(self, "get_cookie_samesite") def _get_preferred_memcache_client(self): - servers = ["127.0.0.1:11211"] + servers = ['127.0.0.1:11211'] try: import pylibmc except ImportError: @@ -252,7 +242,7 @@ def open_session(self, app, request): full_session_key = self.key_prefix + sid if PY2 and isinstance(full_session_key, unicode): - full_session_key = full_session_key.encode("utf-8") + full_session_key = full_session_key.encode('utf-8') val = self.client.get(full_session_key) if val is not None: try: @@ -269,13 +259,12 @@ def save_session(self, app, session, response): path = self.get_cookie_path(app) full_session_key = self.key_prefix + session.sid if PY2 and isinstance(full_session_key, unicode): - full_session_key = full_session_key.encode("utf-8") + full_session_key = full_session_key.encode('utf-8') if not session: if session.modified: self.client.delete(full_session_key) - response.delete_cookie( - app.config["SESSION_COOKIE_NAME"], domain=domain, path=path - ) + response.delete_cookie(app.config["SESSION_COOKIE_NAME"], + domain=domain, path=path) return conditional_cookie_kwargs = {} @@ -288,25 +277,16 @@ def save_session(self, app, session, response): val = self.serializer.dumps(dict(session), 0) else: val = self.serializer.dumps(dict(session)) - self.client.set( - full_session_key, - val, - self._get_memcache_timeout(total_seconds(app.permanent_session_lifetime)), - ) + self.client.set(full_session_key, val, self._get_memcache_timeout( + total_seconds(app.permanent_session_lifetime))) if self.use_signer: session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie( - app.config["SESSION_COOKIE_NAME"], - session_id, - expires=expires, - httponly=httponly, - domain=domain, - path=path, - secure=secure, - **conditional_cookie_kwargs, - ) + response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, + expires=expires, httponly=httponly, + domain=domain, path=path, secure=secure, + **conditional_cookie_kwargs) class FileSystemSessionInterface(SessionInterface): @@ -326,11 +306,9 @@ class FileSystemSessionInterface(SessionInterface): session_class = FileSystemSession - def __init__( - self, cache_dir, threshold, mode, key_prefix, use_signer=False, permanent=True - ): + def __init__(self, cache_dir, threshold, mode, key_prefix, + use_signer=False, permanent=True): from cachelib.file import FileSystemCache - self.cache = FileSystemCache(cache_dir, threshold=threshold, mode=mode) self.key_prefix = key_prefix self.use_signer = use_signer @@ -364,9 +342,8 @@ def save_session(self, app, session, response): if not session: if session.modified: self.cache.delete(self.key_prefix + session.sid) - response.delete_cookie( - app.config["SESSION_COOKIE_NAME"], domain=domain, path=path - ) + response.delete_cookie(app.config["SESSION_COOKIE_NAME"], + domain=domain, path=path) return conditional_cookie_kwargs = {} @@ -376,25 +353,16 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) data = dict(session) - self.cache.set( - self.key_prefix + session.sid, - data, - total_seconds(app.permanent_session_lifetime), - ) + self.cache.set(self.key_prefix + session.sid, data, + total_seconds(app.permanent_session_lifetime)) if self.use_signer: session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie( - app.config["SESSION_COOKIE_NAME"], - session_id, - expires=expires, - httponly=httponly, - domain=domain, - path=path, - secure=secure, - **conditional_cookie_kwargs, - ) + response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, + expires=expires, httponly=httponly, + domain=domain, path=path, secure=secure, + **conditional_cookie_kwargs) class MongoDBSessionInterface(SessionInterface): @@ -414,12 +382,10 @@ class MongoDBSessionInterface(SessionInterface): serializer = pickle session_class = MongoDBSession - def __init__( - self, client, db, collection, key_prefix, use_signer=False, permanent=True - ): + def __init__(self, client, db, collection, key_prefix, use_signer=False, + permanent=True): if client is None: from pymongo import MongoClient - client = MongoClient() self.client = client self.store = client[db][collection] @@ -445,14 +411,14 @@ def open_session(self, app, request): return self.session_class(sid=sid, permanent=self.permanent) store_id = self.key_prefix + sid - document = self.store.find_one({"id": store_id}) - if document and document.get("expiration") <= datetime.utcnow(): + document = self.store.find_one({'id': store_id}) + if document and document.get('expiration') <= datetime.utcnow(): # Delete expired session - self.store.remove({"id": store_id}) + self.store.remove({'id': store_id}) document = None if document is not None: try: - val = document["val"] + val = document['val'] data = self.serializer.loads(want_bytes(val)) return self.session_class(data, sid=sid) except: @@ -465,10 +431,9 @@ def save_session(self, app, session, response): store_id = self.key_prefix + session.sid if not session: if session.modified: - self.store.remove({"id": store_id}) - response.delete_cookie( - app.config["SESSION_COOKIE_NAME"], domain=domain, path=path - ) + self.store.remove({'id': store_id}) + response.delete_cookie(app.config["SESSION_COOKIE_NAME"], + domain=domain, path=path) return conditional_cookie_kwargs = {} @@ -478,23 +443,18 @@ def save_session(self, app, session, response): conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session)) - self.store.update( - {"id": store_id}, {"id": store_id, "val": val, "expiration": expires}, True - ) + self.store.update({'id': store_id}, + {'id': store_id, + 'val': val, + 'expiration': expires}, True) if self.use_signer: session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie( - app.config["SESSION_COOKIE_NAME"], - session_id, - expires=expires, - httponly=httponly, - domain=domain, - path=path, - secure=secure, - **conditional_cookie_kwargs, - ) + response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, + expires=expires, httponly=httponly, + domain=domain, path=path, secure=secure, + **conditional_cookie_kwargs) class SqlAlchemySessionInterface(SessionInterface): @@ -513,10 +473,10 @@ class SqlAlchemySessionInterface(SessionInterface): serializer = pickle session_class = SqlAlchemySession - def __init__(self, app, db, table, key_prefix, use_signer=False, permanent=True): + def __init__(self, app, db, table, key_prefix, use_signer=False, + permanent=True): if db is None: from flask_sqlalchemy import SQLAlchemy - db = SQLAlchemy(app) self.db = db self.key_prefix = key_prefix @@ -538,7 +498,7 @@ def __init__(self, session_id, data, expiry): self.expiry = expiry def __repr__(self): - return "" % self.data + return '' % self.data # self.db.create_all() self.sql_session_model = Session @@ -561,8 +521,7 @@ def open_session(self, app, request): store_id = self.key_prefix + sid saved_session = self.sql_session_model.query.filter_by( - session_id=store_id - ).first() + session_id=store_id).first() if saved_session and saved_session.expiry <= datetime.utcnow(): # Delete expired session self.db.session.delete(saved_session) @@ -582,16 +541,14 @@ def save_session(self, app, session, response): path = self.get_cookie_path(app) store_id = self.key_prefix + session.sid saved_session = self.sql_session_model.query.filter_by( - session_id=store_id - ).first() + session_id=store_id).first() if not session: if session.modified: if saved_session: self.db.session.delete(saved_session) self.db.session.commit() - response.delete_cookie( - app.config["SESSION_COOKIE_NAME"], domain=domain, path=path - ) + response.delete_cookie(app.config["SESSION_COOKIE_NAME"], + domain=domain, path=path) return conditional_cookie_kwargs = {} @@ -613,13 +570,7 @@ def save_session(self, app, session, response): session_id = self._get_signer(app).sign(want_bytes(session.sid)) else: session_id = session.sid - response.set_cookie( - app.config["SESSION_COOKIE_NAME"], - session_id, - expires=expires, - httponly=httponly, - domain=domain, - path=path, - secure=secure, - **conditional_cookie_kwargs, - ) + response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, + expires=expires, httponly=httponly, + domain=domain, path=path, secure=secure, + **conditional_cookie_kwargs) diff --git a/test_session.py b/test_session.py index 8df03450..6fb72a4e 100644 --- a/test_session.py +++ b/test_session.py @@ -6,195 +6,177 @@ class FlaskSessionTestCase(unittest.TestCase): + def test_null_session(self): app = flask.Flask(__name__) Session(app) - def expect_exception(f, *args, **kwargs): try: f(*args, **kwargs) except RuntimeError as e: - self.assertTrue(e.args and "session is unavailable" in e.args[0]) + self.assertTrue(e.args and 'session is unavailable' in e.args[0]) else: - self.assertTrue(False, "expected exception") - + self.assertTrue(False, 'expected exception') with app.test_request_context(): - self.assertTrue(flask.session.get("missing_key") is None) - expect_exception(flask.session.__setitem__, "foo", 42) - expect_exception(flask.session.pop, "foo") + self.assertTrue(flask.session.get('missing_key') is None) + expect_exception(flask.session.__setitem__, 'foo', 42) + expect_exception(flask.session.pop, 'foo') def test_redis_session(self): app = flask.Flask(__name__) - app.config["SESSION_TYPE"] = "redis" + app.config['SESSION_TYPE'] = 'redis' Session(app) - - @app.route("/set", methods=["POST"]) + @app.route('/set', methods=['POST']) def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') def get(): - return flask.session["value"] - - @app.route("/delete", methods=["POST"]) + return flask.session['value'] + @app.route('/delete', methods=['POST']) def delete(): - del flask.session["value"] - return "value deleted" + del flask.session['value'] + return 'value deleted' c = app.test_client() - self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") - self.assertEqual(c.get("/get").data, b"42") - c.post("/delete") - + self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') + self.assertEqual(c.get('/get').data, b'42') + c.post('/delete') + + def test_memcached_session(self): app = flask.Flask(__name__) - app.config["SESSION_TYPE"] = "memcached" + app.config['SESSION_TYPE'] = 'memcached' Session(app) - - @app.route("/set", methods=["POST"]) + @app.route('/set', methods=['POST']) def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') def get(): - return flask.session["value"] - - @app.route("/delete", methods=["POST"]) + return flask.session['value'] + @app.route('/delete', methods=['POST']) def delete(): - del flask.session["value"] - return "value deleted" + del flask.session['value'] + return 'value deleted' c = app.test_client() - self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") - self.assertEqual(c.get("/get").data, b"42") - c.post("/delete") - + self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') + self.assertEqual(c.get('/get').data, b'42') + c.post('/delete') + + def test_filesystem_session(self): app = flask.Flask(__name__) - app.config["SESSION_TYPE"] = "filesystem" - app.config["SESSION_FILE_DIR"] = tempfile.gettempdir() + app.config['SESSION_TYPE'] = 'filesystem' + app.config['SESSION_FILE_DIR'] = tempfile.gettempdir() Session(app) - - @app.route("/set", methods=["POST"]) + @app.route('/set', methods=['POST']) def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') def get(): - return flask.session["value"] - - @app.route("/delete", methods=["POST"]) + return flask.session['value'] + @app.route('/delete', methods=['POST']) def delete(): - del flask.session["value"] - return "value deleted" + del flask.session['value'] + return 'value deleted' c = app.test_client() - self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") - self.assertEqual(c.get("/get").data, b"42") - c.post("/delete") - + self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') + self.assertEqual(c.get('/get').data, b'42') + c.post('/delete') + def test_mongodb_session(self): app = flask.Flask(__name__) app.testing = True - app.config["SESSION_TYPE"] = "mongodb" + app.config['SESSION_TYPE'] = 'mongodb' Session(app) - - @app.route("/set", methods=["POST"]) + @app.route('/set', methods=['POST']) def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') def get(): - return flask.session["value"] - - @app.route("/delete", methods=["POST"]) + return flask.session['value'] + @app.route('/delete', methods=['POST']) def delete(): - del flask.session["value"] - return "value deleted" + del flask.session['value'] + return 'value deleted' c = app.test_client() - self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") - self.assertEqual(c.get("/get").data, b"42") - c.post("/delete") + self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') + self.assertEqual(c.get('/get').data, b'42') + c.post('/delete') def test_flasksqlalchemy_session(self): app = flask.Flask(__name__) app.debug = True - app.config["SESSION_TYPE"] = "sqlalchemy" - app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + app.config['SESSION_TYPE'] = 'sqlalchemy' + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' Session(app) - - @app.route("/set", methods=["POST"]) + @app.route('/set', methods=['POST']) def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') def get(): - return flask.session["value"] - - @app.route("/delete", methods=["POST"]) + return flask.session['value'] + @app.route('/delete', methods=['POST']) def delete(): - del flask.session["value"] - return "value deleted" + del flask.session['value'] + return 'value deleted' c = app.test_client() - self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value " b"set") - self.assertEqual(c.get("/get").data, b"42") - c.post("/delete") + self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value ' + b'set') + self.assertEqual(c.get('/get').data, b'42') + c.post('/delete') def test_flasksqlalchemy_session_with_signer(self): app = flask.Flask(__name__) app.debug = True - app.secret_key = "test_secret_key" - app.config["SESSION_TYPE"] = "sqlalchemy" - app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" - app.config["SESSION_USE_SIGNER"] = True + app.secret_key = 'test_secret_key' + app.config['SESSION_TYPE'] = 'sqlalchemy' + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['SESSION_USE_SIGNER'] = True session = Session(app) - - @app.route("/set", methods=["POST"]) + @app.route('/set', methods=['POST']) def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') def get(): - return flask.session["value"] - - @app.route("/delete", methods=["POST"]) + return flask.session['value'] + @app.route('/delete', methods=['POST']) def delete(): - del flask.session["value"] - return "value deleted" + del flask.session['value'] + return 'value deleted' c = app.test_client() - self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value " b"set") - self.assertEqual(c.get("/get").data, b"42") - c.post("/delete") + self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value ' + b'set') + self.assertEqual(c.get('/get').data, b'42') + c.post('/delete') def test_session_use_signer(self): app = flask.Flask(__name__) - app.secret_key = "test_secret_key" - app.config["SESSION_TYPE"] = "redis" - app.config["SESSION_USE_SIGNER"] = True + app.secret_key = 'test_secret_key' + app.config['SESSION_TYPE'] = 'redis' + app.config['SESSION_USE_SIGNER'] = True Session(app) - - @app.route("/set", methods=["POST"]) + @app.route('/set', methods=['POST']) def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') def get(): - return flask.session["value"] + return flask.session['value'] c = app.test_client() - self.assertEqual(c.post("/set", data={"value": "42"}).data, b"value set") - self.assertEqual(c.get("/get").data, b"42") - + self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') + self.assertEqual(c.get('/get').data, b'42') if __name__ == "__main__": unittest.main() From 43bf8f5143feaae13e64d09ec51192c4ecb0f55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eirik=20V=C3=A5rli=20Lid?= Date: Thu, 21 Mar 2024 14:21:29 +0100 Subject: [PATCH 7/9] Let Flask-Session be installed with optional dependencies. The possible optional groups are cachelib, filesystem, mongodb, redis, sqlalchemy and dev. `Flask-Session[dev]` installs all optional-dependencies. --- pyproject.toml | 8 ++++++++ src/flask_session/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b31e38f2..0da5272f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,14 @@ dependencies = [ ] dynamic = ["version"] +[project.optional-dependencies] +cachelib = ["cachelib>=0.10.2"] +filesystem = ["cachelib>=0.10.2"] +mongodb = ["pymongo>=4.6.2"] +redis = ["redis>=5.0.3"] +sqlalchemy = ["flask-sqlalchemy>=3.0.5"] +dev = ["Flask-Session[cachelib, filesystem, mongodb, redis, sqlalchemy]"] + [project.urls] Documentation = "https://flask-session.readthedocs.io" Changes = "https://flask-session.readthedocs.io/changes.html" diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index b1abf810..f8f1d6a6 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -1,6 +1,6 @@ from .defaults import Defaults -__version__ = "0.7.0" +__version__ = "0.8.0" class Session: From 0c62d405df2824017552a51798384d0c816ed9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eirik=20V=C3=A5rli=20Lid?= Date: Thu, 21 Mar 2024 16:37:19 +0100 Subject: [PATCH 8/9] Update installation.rst. Add memcached as optional-dependency. Found a typo in CONTRIBUTING.rst --- CONTRIBUTING.rst | 14 +++++----- docs/installation.rst | 62 ++++++++++++++++++++++++++++++++++--------- pyproject.toml | 3 ++- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d9b7f0c7..f2f7cf0b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -8,7 +8,7 @@ Navigate to the project directory and run the following commands: Create and activate a virtual environment .. code-block:: bash - + $ python -m venv .venv $ source .venv/bin/activate @@ -16,7 +16,7 @@ Install dependencies .. code-block:: bash - $ pip install -r requirements/dev.in + $ pip install -r requirements/dev.txt $ pip install -r requirements/docs.in Install the package in editable mode @@ -47,11 +47,11 @@ or Run the tests together or individually .. code-block:: bash - + $ pytest tests $ pytest tests/test_basic.py -For easier startup and teardown of storage for testing you may use +For easier startup and teardown of storage for testing you may use .. code-block:: bash @@ -62,13 +62,13 @@ Using rye ~~~~~~~~~~~ .. code-block:: bash - + $ rye pin 3.11 $ rye sync .. code-block:: bash - + $ rye run python examples/hello.py @@ -76,4 +76,4 @@ etc. Pull requests -------------- -Please check previous pull requests before submitting a new one. \ No newline at end of file +Please check previous pull requests before submitting a new one. diff --git a/docs/installation.rst b/docs/installation.rst index 1bf19d40..ccbe4496 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -8,13 +8,13 @@ Install from PyPI using an installer such as pip: $ pip install Flask-Session -Flask-Session's only required dependency is msgspec for serialization, which has no sub-dependencies. +Flask-Session's only required dependency is msgspec for serialization, which has no sub-dependencies. -However, you also need to choose a storage type and install an appropriate client library so the app can communicate with storage. For example, if you want to use Redis as your storage, you will need to install the redis-py client library: +However, you also need to choose a storage type and install an appropriate client library so the app can communicate with storage. For example, if you want to use Redis as your storage, you will need to install the redis optional dependency: .. code-block:: bash - $ pip install redis + $ pip install Flask-Session[redis] Redis is the recommended storage type for Flask-Session, as it has the most complete support for the features of Flask-Session with minimal configuration. @@ -23,10 +23,46 @@ Redis is the recommended storage type for Flask-Session, as it has the most comp Flask-Session versions below 1.0.0 (not yet released), use pickle_ as the default serializer, which may have security implications in production if your storage is ever compromised. -Direct support ---------------- +Enhanced Installation Options +----------------------------- -Flask-Session has an increasing number of directly supported storage and client libraries. +Flask-Session now supports a variety of storage backends directly through optional dependencies. This simplifies the installation process, allowing you to specify the required storage backend at the time of Flask-Session installation itself. + +Below is a guide on how to install Flask-Session with support for the desired storage backend: + +Available Storage Options +^^^^^^^^^^^^^^^^^^^^^^^^^ + +To install Flask-Session with support for a specific storage backend, use the following command, replacing ```` with your chosen backend from the list below: + +.. code-block:: bash + + pip install Flask-Session[] + +Available storage options and their corresponding ```` values are: + +- **Redis**: ``redis`` +- **Memcached**: ``memcached`` +- **MongoDB**: ``mongodb`` +- **SQLAlchemy**: ``sqlalchemy`` +- **FileSystem**: ``filesystem`` +- **CacheLib**: ``cachelib`` + +For Developers and Contributors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you're developing with Flask-Session or contributing to its codebase, you might want to install all supported backends along with additional development tools. You can do so by specifying the ``dev`` option: + +.. code-block:: bash + + pip install Flask-Session[dev] + +This will install Flask-Session along with all the optional dependencies listed under the ``dev`` category in ``pyproject.toml``, facilitating a comprehensive development setup. + +Direct Support +-------------- + +Flask-Session provides built-in support for a number of storage backends through the use of client libraries. Below is a list of supported storage options along with their respective client libraries: .. list-table:: :header-rows: 1 @@ -35,15 +71,15 @@ Flask-Session has an increasing number of directly supported storage and client * - Storage - Client Library * - Redis - - redis-py_ + - ``redis`` (via ``redis-py``) * - Memcached - - pylibmc_, python-memcached_, libmc_ or pymemcache_ + - ``pymemcache`` * - MongoDB - - pymongo_ + - ``pymongo`` * - SQL Alchemy - - flask-sqlalchemy_ + - ``flask-sqlalchemy`` -Other libraries may work if they use the same commands as the ones listed above. +Other storage backends might be compatible with Flask-Session as long as they adhere to the command interfaces used by the libraries listed above. Cachelib -------- @@ -70,7 +106,7 @@ Flask-Session also indirectly supports storage and client libraries via cachelib - pymongo_ * - DynamoDB - boto3_ - + .. warning:: @@ -88,4 +124,4 @@ Flask-Session also indirectly supports storage and client libraries via cachelib .. _boto3: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html .. _libmc: https://github.com/douban/libmc .. _uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html -.. _pickle: https://docs.python.org/3/library/pickle.html \ No newline at end of file +.. _pickle: https://docs.python.org/3/library/pickle.html diff --git a/pyproject.toml b/pyproject.toml index 0da5272f..4f6952b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,10 +28,11 @@ dynamic = ["version"] [project.optional-dependencies] cachelib = ["cachelib>=0.10.2"] filesystem = ["cachelib>=0.10.2"] +memcached = ["pymemcache"] mongodb = ["pymongo>=4.6.2"] redis = ["redis>=5.0.3"] sqlalchemy = ["flask-sqlalchemy>=3.0.5"] -dev = ["Flask-Session[cachelib, filesystem, mongodb, redis, sqlalchemy]"] +dev = ["Flask-Session[cachelib, filesystem, memcached, mongodb, redis, sqlalchemy]"] [project.urls] Documentation = "https://flask-session.readthedocs.io" From b7a219bcac97e74276d8e419e6174aea2c4ffdad Mon Sep 17 00:00:00 2001 From: Lex Date: Sun, 24 Mar 2024 21:54:59 +1000 Subject: [PATCH 9/9] Reduce documentation and remove filesystem extra --- CONTRIBUTORS.md | 1 + docs/installation.rst | 75 +++++++++++++++++++------------------------ docs/usage.rst | 26 ++++++++++++++- pyproject.toml | 3 +- 4 files changed, 60 insertions(+), 45 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f8aafac8..07122b42 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,5 +1,6 @@ ## Contributors +- [eiriklid](https://github.com/eiriklid) - [nebolax](https://github.com/nebolax) - [Taragolis](https://github.com/Taragolis) - [Lxstr](https://github.com/Lxstr) diff --git a/docs/installation.rst b/docs/installation.rst index ccbe4496..99313035 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -10,7 +10,8 @@ Install from PyPI using an installer such as pip: Flask-Session's only required dependency is msgspec for serialization, which has no sub-dependencies. -However, you also need to choose a storage type and install an appropriate client library so the app can communicate with storage. For example, if you want to use Redis as your storage, you will need to install the redis optional dependency: +However, you also need to choose a storage type and install an appropriate client library so the app can communicate with storage. +For example, if you want to use Redis as your storage, you will need to install the redis-py_ library either directly or as an optional dependency like below: .. code-block:: bash @@ -23,14 +24,7 @@ Redis is the recommended storage type for Flask-Session, as it has the most comp Flask-Session versions below 1.0.0 (not yet released), use pickle_ as the default serializer, which may have security implications in production if your storage is ever compromised. -Enhanced Installation Options ------------------------------ - -Flask-Session now supports a variety of storage backends directly through optional dependencies. This simplifies the installation process, allowing you to specify the required storage backend at the time of Flask-Session installation itself. - -Below is a guide on how to install Flask-Session with support for the desired storage backend: - -Available Storage Options +Available storage options ^^^^^^^^^^^^^^^^^^^^^^^^^ To install Flask-Session with support for a specific storage backend, use the following command, replacing ```` with your chosen backend from the list below: @@ -41,50 +35,47 @@ To install Flask-Session with support for a specific storage backend, use the fo Available storage options and their corresponding ```` values are: -- **Redis**: ``redis`` -- **Memcached**: ``memcached`` -- **MongoDB**: ``mongodb`` -- **SQLAlchemy**: ``sqlalchemy`` -- **FileSystem**: ``filesystem`` -- **CacheLib**: ``cachelib`` - -For Developers and Contributors -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you're developing with Flask-Session or contributing to its codebase, you might want to install all supported backends along with additional development tools. You can do so by specifying the ``dev`` option: - -.. code-block:: bash - - pip install Flask-Session[dev] - -This will install Flask-Session along with all the optional dependencies listed under the ``dev`` category in ``pyproject.toml``, facilitating a comprehensive development setup. - -Direct Support --------------- - -Flask-Session provides built-in support for a number of storage backends through the use of client libraries. Below is a list of supported storage options along with their respective client libraries: .. list-table:: :header-rows: 1 :align: left * - Storage - - Client Library - * - Redis - - ``redis`` (via ``redis-py``) - * - Memcached - - ``pymemcache`` - * - MongoDB - - ``pymongo`` - * - SQL Alchemy - - ``flask-sqlalchemy`` + - + - Default client library + - Alternative client libraries + * - **Redis** + - ``redis`` + - redis-py_ + - + * - **Memcached** + - ``memcached`` + - pymemcache_ + - pylibmc_, python-memcached_, libmc_ + * - **MongoDB** + - ``mongodb`` + - pymongo_ + - + * - **CacheLib** + - ``cachelib`` + - cachelib_ + - + * - **SQLAlchemy** + - ``sqlalchemy`` + - flask-sqlalchemy_ + - + * - **DynamoDB** + - ``dynamodb`` + - boto3_ + - Other storage backends might be compatible with Flask-Session as long as they adhere to the command interfaces used by the libraries listed above. Cachelib -------- -Flask-Session also indirectly supports storage and client libraries via cachelib_, which is a wrapper around various cache libraries. You must also install cachelib_ itself to use these. +Flask-Session also indirectly supports storage and client libraries via cachelib_, which is a wrapper around various cache libraries. +You must also install cachelib_ itselfand the relevant client library to use these. .. list-table:: :header-rows: 1 @@ -118,7 +109,7 @@ Flask-Session also indirectly supports storage and client libraries via cachelib .. _python-memcached: https://github.com/linsomniac/python-memcached .. _pymemcache: https://github.com/pinterest/pymemcache .. _pymongo: https://pymongo.readthedocs.io/en/stable -.. _Flask-SQLAlchemy: https://github.com/pallets-eco/flask-sqlalchemy +.. _flask-sqlalchemy: https://github.com/pallets-eco/flask-sqlalchemy .. _cachelib: https://cachelib.readthedocs.io/en/stable/ .. _google.appengine.api.memcached: https://cloud.google.com/appengine/docs/legacy/standard/python/memcache .. _boto3: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html diff --git a/docs/usage.rst b/docs/usage.rst index ce389615..675d9e9e 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -62,4 +62,28 @@ Or, if you prefer to directly set parameters rather than using the configuration app = Flask(__name__) redis = Redis(host='localhost', port=6379) - app.session_interface = RedisSessionInterface(client=redis) \ No newline at end of file + app.session_interface = RedisSessionInterface(client=redis) + + +Using CacheLib as a session backend +------------------------------------ + +.. note:: + + FileSystemSession was recently deprecated in favor of CacheLib, which is what is was using under the hood. + +The following example demonstrates how to use CacheLib as a session backend with the file system cache. This might be useful for rapid development or testing. + +.. code-block:: python + + from flask import Flask, session + from flask_session import Session + from cachelib.file import FileSystemCache + + app = Flask(__name__) + + SESSION_TYPE = 'cachelib' + SESSION_SERIALIZATION_FORMAT = 'json' + SESSION_CACHELIB = FileSystemCache(threshold=500, cache_dir="/sessions"), + app.config.from_object(__name__) + Session(app) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4f6952b0..4c828700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,12 +27,11 @@ dynamic = ["version"] [project.optional-dependencies] cachelib = ["cachelib>=0.10.2"] -filesystem = ["cachelib>=0.10.2"] memcached = ["pymemcache"] mongodb = ["pymongo>=4.6.2"] redis = ["redis>=5.0.3"] sqlalchemy = ["flask-sqlalchemy>=3.0.5"] -dev = ["Flask-Session[cachelib, filesystem, memcached, mongodb, redis, sqlalchemy]"] +all = ["Flask-Session[cachelib, memcached, mongodb, redis, sqlalchemy]"] [project.urls] Documentation = "https://flask-session.readthedocs.io"