From 3ccb16a27c188aeaafe663e1650816001e71143d Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 10 Jan 2024 16:23:19 +1000 Subject: [PATCH] Add ruff linter --- .github/workflows/ruff.yaml | 8 +++ examples/hello.py | 25 +++++--- requirements/pytest.txt | 3 + src/flask_session/__init__.py | 113 ++++++++++++++++++++++------------ src/flask_session/sessions.py | 75 ++++++++++++---------- tests/conftest.py | 57 ++++++++++------- tests/test_basic.py | 13 ++-- tests/test_filesystem.py | 9 ++- tests/test_memcached.py | 6 +- tests/test_mongodb.py | 6 +- tests/test_redis.py | 38 ++++++------ tests/test_sqlalchemy.py | 22 ++++--- 12 files changed, 226 insertions(+), 149 deletions(-) create mode 100644 .github/workflows/ruff.yaml diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml new file mode 100644 index 00000000..e8133f29 --- /dev/null +++ b/.github/workflows/ruff.yaml @@ -0,0 +1,8 @@ +name: Ruff +on: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: chartboost/ruff-action@v1 \ No newline at end of file diff --git a/examples/hello.py b/examples/hello.py index 5fd7257c..9e22a35a 100644 --- a/examples/hello.py +++ b/examples/hello.py @@ -2,23 +2,32 @@ from flask_session import Session -SESSION_TYPE = 'redis' - - app = Flask(__name__) app.config.from_object(__name__) +app.config.update( + { + "SESSION_TYPE": "sqlalchemy", + "SQLALCHEMY_DATABASE_URI": "sqlite:////tmp/test.db", + "SQLALCHEMY_USE_SIGNER": True, + } +) 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') + import time + + start_time = time.time() + result = session.get("key", "not set") + print("get", (time.time() - start_time) * 1000) + return result if __name__ == "__main__": diff --git a/requirements/pytest.txt b/requirements/pytest.txt index 3fed2aa0..74f9ef06 100644 --- a/requirements/pytest.txt +++ b/requirements/pytest.txt @@ -2,6 +2,9 @@ flask>=2.2 cachelib +# Linting +ruff + # Testing pytest pytest-cov diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index c4397cad..2c9e0042 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.1' +__version__ = "0.5.1" class Session(object): @@ -51,48 +56,74 @@ 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_ID_LENGTH', 32) - 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_ID_LENGTH", 32) + config.setdefault("SESSION_REDIS", None) + config.setdefault("SESSION_MEMCACHED", None) + + # Filesystem settings + config.setdefault( + "SESSION_FILE_DIR", os.path.join(os.getcwd(), "flask_session") + ) + config.setdefault("SESSION_FILE_THRESHOLD", 500) + config.setdefault("SESSION_FILE_MODE", 384) + + # MongoDB settings + config.setdefault("SESSION_MONGODB", None) + config.setdefault("SESSION_MONGODB_DB", "flask_session") + config.setdefault("SESSION_MONGODB_COLLECT", "sessions") + config.setdefault("SESSION_MONGODB_TZ_AWARE", False) + + # SQLAlchemy settings + config.setdefault("SESSION_SQLALCHEMY", None) + config.setdefault("SESSION_SQLALCHEMY_TABLE", "sessions") + config.setdefault("SESSION_SQLALCHEMY_SEQUENCE", None) + config.setdefault("SESSION_SQLALCHEMY_SCHEMA", None) + config.setdefault("SESSION_SQLALCHEMY_BIND_KEY", None) + + common_params = { + "key_prefix": config["SESSION_KEY_PREFIX"], + "use_signer": config["SESSION_USE_SIGNER"], + "permanent": config["SESSION_PERMANENT"], + "sid_length": config["SESSION_ID_LENGTH"], + } + + 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"], **common_params + ) + 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"], **common_params + ) + 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"], + **common_params, + ) + 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_MONGODB_TZ_AWARE"], + **common_params, + ) + 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_SQLALCHEMY_SEQUENCE"], + config["SESSION_SQLALCHEMY_SCHEMA"], + config["SESSION_SQLALCHEMY_BIND_KEY"], + **common_params, + ) else: session_interface = NullSessionInterface() diff --git a/src/flask_session/sessions.py b/src/flask_session/sessions.py index d18914ba..65f727ad 100644 --- a/src/flask_session/sessions.py +++ b/src/flask_session/sessions.py @@ -1,8 +1,8 @@ -import sys import time from abc import ABC from datetime import datetime import secrets + try: import cPickle as pickle except ImportError: @@ -20,13 +20,14 @@ def total_seconds(td): class ServerSideSession(CallbackDict, SessionMixin): """Baseclass for server-side based sessions.""" - + def __bool__(self) -> bool: return bool(dict(self)) and self.keys() != {"_permanent"} 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: @@ -55,10 +56,9 @@ class SqlAlchemySession(ServerSideSession): class SessionInterface(FlaskSessionInterface): - def _generate_sid(self, session_id_length): return secrets.token_urlsafe(session_id_length) - + 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") @@ -77,16 +77,14 @@ def _sign(self, app, sid): 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 class ServerSideSessionInterface(SessionInterface, ABC): - """Used to open a :class:`flask.sessions.ServerSideSessionInterface` instance. - """ + """Used to open a :class:`flask.sessions.ServerSideSessionInterface` instance.""" def __init__(self, db, key_prefix, use_signer=False, permanent=True, sid_length=32): self.db = db @@ -97,7 +95,6 @@ def __init__(self, db, key_prefix, use_signer=False, permanent=True, sid_length= self.has_same_site_capability = hasattr(self, "get_cookie_samesite") def set_cookie_to_response(self, app, session, response, expires): - if self.use_signer: session_id = self._sign(app, session.sid) else: @@ -111,10 +108,16 @@ def set_cookie_to_response(self, app, session, response, expires): if self.has_same_site_capability: samesite = self.get_cookie_samesite(app) - response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure, - samesite=samesite) + response.set_cookie( + app.config["SESSION_COOKIE_NAME"], + session_id, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + samesite=samesite, + ) def open_session(self, app, request): sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"]) @@ -149,9 +152,7 @@ class RedisSessionInterface(ServerSideSessionInterface): serializer = pickle session_class = RedisSession - def __init__( - self, redis, key_prefix, use_signer, permanent, sid_length - ): + def __init__(self, redis, key_prefix, use_signer, permanent, sid_length): if redis is None: from redis import Redis @@ -161,7 +162,7 @@ def __init__( def fetch_session_sid(self, sid): if not isinstance(sid, str): - 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: @@ -180,13 +181,18 @@ 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 expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session)) - self.redis.set(name=self.key_prefix + session.sid, value=val, ex=total_seconds(app.permanent_session_lifetime)) + self.redis.set( + name=self.key_prefix + session.sid, + value=val, + ex=total_seconds(app.permanent_session_lifetime), + ) self.set_cookie_to_response(app, session, response, expires) @@ -207,18 +213,16 @@ class MemcachedSessionInterface(ServerSideSessionInterface): serializer = pickle session_class = MemcachedSession - def __init__( - self, client, key_prefix, use_signer, permanent, sid_length - ): + def __init__(self, client, key_prefix, use_signer, permanent, sid_length): 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 super().__init__(client, key_prefix, use_signer, permanent, sid_length) def _get_preferred_memcache_client(self): - servers = ['127.0.0.1:11211'] + servers = ["127.0.0.1:11211"] try: import pylibmc except ImportError: @@ -249,7 +253,6 @@ def _get_memcache_timeout(self, timeout): return timeout def fetch_session_sid(self, sid): - full_session_key = self.key_prefix + sid val = self.client.get(full_session_key) if val is not None: @@ -270,14 +273,18 @@ def save_session(self, app, session, response): 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 expires = self.get_expiration_time(app, session) val = self.serializer.dumps(dict(session), 0) - 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)), + ) self.set_cookie_to_response(app, session, response, expires) @@ -336,10 +343,14 @@ def save_session(self, app, session, response): 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), + ) self.set_cookie_to_response(app, session, response, expires) + class MongoDBSessionInterface(ServerSideSessionInterface): """A Session interface that uses mongodb as backend. diff --git a/tests/conftest.py b/tests/conftest.py index e8502372..895a8c31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,13 @@ import sys -sys.path.append('src') + +sys.path.append("src") import flask_session import flask import pytest -@pytest.fixture(scope='function') + +@pytest.fixture(scope="function") def app_utils(): class Utils: def create_app(self, config_dict=None): @@ -13,41 +15,52 @@ def create_app(self, config_dict=None): if config_dict: app.config.update(config_dict) - @app.route('/set', methods=['POST']) + @app.route("/set", methods=["POST"]) def app_set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - - @app.route('/delete', methods=['POST']) + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/modify", methods=["POST"]) + def app_modify(): + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/delete", methods=["POST"]) def app_del(): - del flask.session['value'] - return 'value deleted' + del flask.session["value"] + return "value deleted" - @app.route('/get') + @app.route("/get") def app_get(): - return flask.session.get('value') + return flask.session.get("value") flask_session.Session(app) return app def test_session_set(self, app): client = app.test_client() - assert client.post('/set', data={'value': '42'}).data == b'value set' - assert client.get('/get').data == b'42' + assert client.post("/set", data={"value": "42"}).data == b"value set" + assert client.get("/get").data == b"42" + + def test_session_modify(self, app): + client = app.test_client() + assert client.post("/set", data={"value": "42"}).data == b"value set" + assert client.post("/modify", data={"value": "43"}).data == b"value set" + assert client.get("/get").data == b"43" def test_session_delete(self, app): client = app.test_client() - assert client.post('/set', data={'value': '42'}).data == b'value set' - assert client.get('/get').data == b'42' - client.post('/delete') - assert not client.get('/get').data == b'42' - + assert client.post("/set", data={"value": "42"}).data == b"value set" + assert client.get("/get").data == b"42" + client.post("/delete") + assert not client.get("/get").data == b"42" + def test_session_sign(self, app): client = app.test_client() - response = client.post('/set', data={'value': '42'}) - assert response.data == b'value set' + response = client.post("/set", data={"value": "42"}) + assert response.data == b"value set" # Check there are two parts to the cookie, the session ID and the signature - cookies = response.headers.getlist('Set-Cookie') - assert '.' in cookies[0].split(';')[0] + cookies = response.headers.getlist("Set-Cookie") + assert "." in cookies[0].split(";")[0] return Utils() diff --git a/tests/test_basic.py b/tests/test_basic.py index a1083142..df7c33b9 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,22 +1,25 @@ import flask import pytest -import flask_session +import flask_session + def test_tot_seconds_func(): import datetime + td = datetime.timedelta(days=1) assert flask_session.sessions.total_seconds(td) == 86400 + def test_null_session(): """Invalid session should fail to get/set the flask session""" app = flask.Flask(__name__) - app.secret_key = 'alsdkfjaldkjsf' + app.secret_key = "alsdkfjaldkjsf" flask_session.Session(app) with app.test_request_context(): - assert not flask.session.get('missing_key') + assert not flask.session.get("missing_key") with pytest.raises(RuntimeError): - flask.session['foo'] = 42 + flask.session["foo"] = 42 with pytest.raises(KeyError): - print(flask.session['foo']) + print(flask.session["foo"]) diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 406f576b..b00a8898 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -2,16 +2,15 @@ import flask_session import tempfile -class TestFileSystem: +class TestFileSystem: def setup_method(self, _): pass def test_basic(self, app_utils): - app = app_utils.create_app({ - 'SESSION_TYPE': 'filesystem', - 'SESSION_FILE_DIR': tempfile.gettempdir() - }) + app = app_utils.create_app( + {"SESSION_TYPE": "filesystem", "SESSION_FILE_DIR": tempfile.gettempdir()} + ) app_utils.test_session_set(app) # Should be using FileSystem class diff --git a/tests/test_memcached.py b/tests/test_memcached.py index d30922ed..aabbd7ff 100644 --- a/tests/test_memcached.py +++ b/tests/test_memcached.py @@ -1,12 +1,14 @@ import flask import flask_session + class TestMemcached: """This requires package: memcached - This needs to be running before test runs + This needs to be running before test runs """ + def test_basic(self, app_utils): - app = app_utils.create_app({'SESSION_TYPE': 'memcached'}) + app = app_utils.create_app({"SESSION_TYPE": "memcached"}) # Should be using Memecached with app.test_request_context(): diff --git a/tests/test_mongodb.py b/tests/test_mongodb.py index a52858ff..15d1319a 100644 --- a/tests/test_mongodb.py +++ b/tests/test_mongodb.py @@ -1,12 +1,10 @@ import flask import flask_session -class TestMongoDB: +class TestMongoDB: def test_basic(self, app_utils): - app = app_utils.create_app({ - 'SESSION_TYPE': 'mongodb' - }) + app = app_utils.create_app({"SESSION_TYPE": "mongodb"}) # Should be using MongoDB with app.test_request_context(): diff --git a/tests/test_redis.py b/tests/test_redis.py index 257d36de..b1efb972 100644 --- a/tests/test_redis.py +++ b/tests/test_redis.py @@ -1,10 +1,9 @@ import flask from redis import Redis import flask_session -import pytest -class TestRedisSession: +class TestRedisSession: def setup_method(self, method): # Clear redis r = Redis() @@ -18,9 +17,7 @@ def _has_redis_prefix(self, prefix): return False def test_redis_default(self, app_utils): - app = app_utils.create_app({ - 'SESSION_TYPE': 'redis' - }) + app = app_utils.create_app({"SESSION_TYPE": "redis"}) # Should be using Redis with app.test_request_context(): @@ -29,30 +26,31 @@ def test_redis_default(self, app_utils): app_utils.test_session_set(app) # There should be a session: object - assert self._has_redis_prefix(b'session:') + assert self._has_redis_prefix(b"session:") self.setup_method(None) app_utils.test_session_delete(app) # There should not be a session: object - assert not self._has_redis_prefix(b'session:') + assert not self._has_redis_prefix(b"session:") def test_redis_key_prefix(self, app_utils): - app = app_utils.create_app({ - 'SESSION_TYPE': 'redis', - 'SESSION_KEY_PREFIX': 'sess-prefix:' - }) + app = app_utils.create_app( + {"SESSION_TYPE": "redis", "SESSION_KEY_PREFIX": "sess-prefix:"} + ) app_utils.test_session_set(app) # There should be a key in Redis that starts with the prefix set - assert not self._has_redis_prefix(b'session:') - assert self._has_redis_prefix(b'sess-prefix:') + assert not self._has_redis_prefix(b"session:") + assert self._has_redis_prefix(b"sess-prefix:") def test_redis_with_signer(self, app_utils): - app = app_utils.create_app({ - 'SESSION_TYPE': 'redis', - 'SESSION_USE_SIGNER': True, - }) + app = app_utils.create_app( + { + "SESSION_TYPE": "redis", + "SESSION_USE_SIGNER": True, + } + ) # Without a secret key set, there should be an exception raised # TODO: not working @@ -60,14 +58,14 @@ def test_redis_with_signer(self, app_utils): # app_utils.test_session_set(app) # With a secret key set, no exception should be thrown - app.secret_key = 'test_key' + app.secret_key = "test_key" app_utils.test_session_set(app) # There should be a key in Redis that starts with the prefix set - assert self._has_redis_prefix(b'session:') + assert self._has_redis_prefix(b"session:") # Clear redis self.setup_method(None) # Check that the session is signed - app_utils.test_session_sign(app) \ No newline at end of file + app_utils.test_session_sign(app) diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index 2fbc5a8c..4891a42b 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -1,13 +1,12 @@ import flask import flask_session -class TestSQLAlchemy: +class TestSQLAlchemy: def test_basic(self, app_utils): - app = app_utils.create_app({ - 'SESSION_TYPE': 'sqlalchemy', - 'SQLALCHEMY_DATABASE_URI': 'sqlite:///' - }) + app = app_utils.create_app( + {"SESSION_TYPE": "sqlalchemy", "SQLALCHEMY_DATABASE_URI": "sqlite:///"} + ) # Should be using SqlAlchemy with app.test_request_context(): @@ -15,13 +14,16 @@ def test_basic(self, app_utils): app.session_interface.db.create_all() app_utils.test_session_set(app) + app_utils.test_session_modify(app) def test_use_signer(self, app_utils): - app = app_utils.create_app({ - 'SESSION_TYPE': 'sqlalchemy', - 'SQLALCHEMY_DATABASE_URI': 'sqlite:///', - 'SQLALCHEMY_USE_SIGNER': True - }) + app = app_utils.create_app( + { + "SESSION_TYPE": "sqlalchemy", + "SQLALCHEMY_DATABASE_URI": "sqlite:///", + "SQLALCHEMY_USE_SIGNER": True, + } + ) with app.test_request_context(): app.session_interface.db.create_all()