From f1651ee67bcbf940258fa778fac6c815b3aba382 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Mon, 31 Jul 2023 15:50:16 -0400 Subject: [PATCH 01/17] Drop support for Python 3.7 --- .github/workflows/test.yml | 2 +- pyproject.toml | 2 +- setup.py | 3 +-- tox.ini | 9 ++++----- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1f1859..011b6f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10'] # Service containers to run with `container-job` services: diff --git a/pyproject.toml b/pyproject.toml index bfc30ba..869f22c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 88 -target-version = ['py37', 'py38', 'py39', 'py310'] +target-version = ['py38', 'py39', 'py310'] include = '\.pyi?$' exclude = ''' /( diff --git a/setup.py b/setup.py index e05b33f..6694b6a 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ def read(*parts): "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -48,5 +47,5 @@ def read(*parts): "sanic": ["sanic"], }, zip_safe=False, - python_requires=">=3.7,<4", + python_requires=">=3.8,<4", ) diff --git a/tox.ini b/tox.ini index b28cbcb..fd88e08 100644 --- a/tox.ini +++ b/tox.ini @@ -4,15 +4,14 @@ minversion = 1.8 envlist = py38-lint py38-docs - py{37,38,39,310}-dj32 + py{38,39,310}-dj32 py{38,39,310}-dj{40,41} - py{37,38,39,310}-fl{012,10,11,20,21,22} - py{37,38,39}-s20 - py{37,38,39,310}-s{21,22} + py{38,39,310}-fl{012,10,11,20,21,22} + py{38,39}-s20 + py{38,39,310}-s{21,22} [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 From f454190d601c5340cd75995738626991f68d6d4f Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Mon, 31 Jul 2023 15:54:08 -0400 Subject: [PATCH 02/17] Add support for Python 3.11 --- .github/workflows/test.yml | 2 +- pyproject.toml | 2 +- setup.py | 1 + tox.ini | 9 +++++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 011b6f8..e293d9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] # Service containers to run with `container-job` services: diff --git a/pyproject.toml b/pyproject.toml index 869f22c..73c7f8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 88 -target-version = ['py38', 'py39', 'py310'] +target-version = ['py38', 'py39', 'py310', 'py311'] include = '\.pyi?$' exclude = ''' /( diff --git a/setup.py b/setup.py index 6694b6a..86fdf2e 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ def read(*parts): "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Internet :: WWW/HTTP", ], extras_require={ diff --git a/tox.ini b/tox.ini index fd88e08..c38a269 100644 --- a/tox.ini +++ b/tox.ini @@ -4,17 +4,18 @@ minversion = 1.8 envlist = py38-lint py38-docs - py{38,39,310}-dj32 - py{38,39,310}-dj{40,41} - py{38,39,310}-fl{012,10,11,20,21,22} + py{38,39,310,311}-dj32 + py{38,39,310,311}-dj{40,41} + py{38,39,310,311}-fl{012,10,11,20,21,22} py{38,39}-s20 - py{38,39,310}-s{21,22} + py{38,39,310,311}-s{21,22} [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [testenv] usedevelop = true From 5cc6b9f6be2249bf1dff02972c6567d945343897 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 10:50:45 -0400 Subject: [PATCH 03/17] Revert "Drop support for Python 3.7" This reverts commit f1651ee67bcbf940258fa778fac6c815b3aba382. --- .github/workflows/test.yml | 2 +- pyproject.toml | 2 +- setup.py | 3 ++- tox.ini | 9 +++++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e293d9a..3a6bea8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] # Service containers to run with `container-job` services: diff --git a/pyproject.toml b/pyproject.toml index 73c7f8c..63c8fb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 88 -target-version = ['py38', 'py39', 'py310', 'py311'] +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] include = '\.pyi?$' exclude = ''' /( diff --git a/setup.py b/setup.py index 86fdf2e..d1d6ead 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ def read(*parts): "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -48,5 +49,5 @@ def read(*parts): "sanic": ["sanic"], }, zip_safe=False, - python_requires=">=3.8,<4", + python_requires=">=3.7,<4", ) diff --git a/tox.ini b/tox.ini index c38a269..494de64 100644 --- a/tox.ini +++ b/tox.ini @@ -4,14 +4,15 @@ minversion = 1.8 envlist = py38-lint py38-docs - py{38,39,310,311}-dj32 + py{37,38,39,310,311}-dj32 py{38,39,310,311}-dj{40,41} - py{38,39,310,311}-fl{012,10,11,20,21,22} - py{38,39}-s20 - py{38,39,310,311}-s{21,22} + py{37,38,39,310,311}-fl{012,10,11,20,21,22} + py{37,38,39}-s20 + py{37,38,39,310,311}-s{21,22} [gh-actions] python = + 3.7: py37 3.8: py38 3.9: py39 3.10: py310 From e562030420c42d987731126ee0872c0eb8cfda81 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 11:07:44 -0400 Subject: [PATCH 04/17] Edit Tox config to test libraries that support python >=3.7 --- tests/constraints/flask-0.12.txt | 4 ---- tests/constraints/flask-1.0.txt | 4 ---- tests/constraints/flask-1.1.txt | 2 -- tests/constraints/flask-2.0.txt | 2 -- tests/constraints/flask-2.1.txt | 2 ++ tests/constraints/sanic-20.txt | 3 --- tests/constraints/sanic-21.txt | 2 ++ tests/requirements/default.txt | 2 +- tests/requirements/django.txt | 5 +---- tests/requirements/sanic-20.txt | 7 ------- tox.ini | 11 ++--------- 11 files changed, 8 insertions(+), 36 deletions(-) delete mode 100644 tests/constraints/flask-0.12.txt delete mode 100644 tests/constraints/flask-1.0.txt delete mode 100644 tests/constraints/flask-1.1.txt delete mode 100644 tests/constraints/flask-2.0.txt delete mode 100644 tests/constraints/sanic-20.txt delete mode 100644 tests/requirements/sanic-20.txt diff --git a/tests/constraints/flask-0.12.txt b/tests/constraints/flask-0.12.txt deleted file mode 100644 index 6d7ee9e..0000000 --- a/tests/constraints/flask-0.12.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask<0.13 -Werkzeug<1.0.0 -Jinja2<3.1 -itsdangerous<2.1.0 diff --git a/tests/constraints/flask-1.0.txt b/tests/constraints/flask-1.0.txt deleted file mode 100644 index b1fe3ec..0000000 --- a/tests/constraints/flask-1.0.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask<1.1 -Werkzeug<1.0.0 -Jinja2<3.1 -itsdangerous<2.1.0 diff --git a/tests/constraints/flask-1.1.txt b/tests/constraints/flask-1.1.txt deleted file mode 100644 index 91d38d0..0000000 --- a/tests/constraints/flask-1.1.txt +++ /dev/null @@ -1,2 +0,0 @@ -Flask<1.2 -MarkupSafe<=2.0.1 diff --git a/tests/constraints/flask-2.0.txt b/tests/constraints/flask-2.0.txt deleted file mode 100644 index a6675ab..0000000 --- a/tests/constraints/flask-2.0.txt +++ /dev/null @@ -1,2 +0,0 @@ -Flask<2.1 -Werkzeug<2.1.0 diff --git a/tests/constraints/flask-2.1.txt b/tests/constraints/flask-2.1.txt index 8402758..498f05d 100644 --- a/tests/constraints/flask-2.1.txt +++ b/tests/constraints/flask-2.1.txt @@ -1 +1,3 @@ Flask<2.2 +SQLAlchemy<=1.4 +Flask-SQLAlchemy<3.0 \ No newline at end of file diff --git a/tests/constraints/sanic-20.txt b/tests/constraints/sanic-20.txt deleted file mode 100644 index 7c6c3ea..0000000 --- a/tests/constraints/sanic-20.txt +++ /dev/null @@ -1,3 +0,0 @@ -Sanic>=20,<21 -aioredis<2 -sanic_redis<0.3.0 diff --git a/tests/constraints/sanic-21.txt b/tests/constraints/sanic-21.txt index 0d223de..977b79a 100644 --- a/tests/constraints/sanic-21.txt +++ b/tests/constraints/sanic-21.txt @@ -1 +1,3 @@ Sanic>=21,<22 +websockets<11 +sanic-testing<22 \ No newline at end of file diff --git a/tests/requirements/default.txt b/tests/requirements/default.txt index c684369..d972249 100644 --- a/tests/requirements/default.txt +++ b/tests/requirements/default.txt @@ -6,6 +6,6 @@ pytest-coverage pytest-mock pytest-pythonpath mock -redis<3.2.0 +redis fakeredis jsonschema diff --git a/tests/requirements/django.txt b/tests/requirements/django.txt index b0cb3b4..5b6d7e1 100644 --- a/tests/requirements/django.txt +++ b/tests/requirements/django.txt @@ -1,6 +1,3 @@ django-redis pytest-django -# these are constrained by the files in tests/constraints/*.txt -# to support a triple stack Django/Flask/Sanic -Django; python_version >= '3.0' -Django<2.0; python_version < '3.0' +Django diff --git a/tests/requirements/sanic-20.txt b/tests/requirements/sanic-20.txt deleted file mode 100644 index e11c6f2..0000000 --- a/tests/requirements/sanic-20.txt +++ /dev/null @@ -1,7 +0,0 @@ -# these are constrained by the files in tests/constraints/*.txt -# to support a triple stack Django/Flask/Sanic -aiohttp -aioredis -Sanic -sanic_redis -uvloop>=0.14.0rc1 diff --git a/tox.ini b/tox.ini index 494de64..0d6c414 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,7 @@ envlist = py38-docs py{37,38,39,310,311}-dj32 py{38,39,310,311}-dj{40,41} - py{37,38,39,310,311}-fl{012,10,11,20,21,22} - py{37,38,39}-s20 + py{37,38,39,310,311}-fl{21,22} py{37,38,39,310,311}-s{21,22} [gh-actions] @@ -27,19 +26,13 @@ setenv = deps = -rtests/requirements/default.txt dj{32,40,41}: -rtests/requirements/django.txt - fl{012,10,11,20,21,22}: -rtests/requirements/flask.txt - s20: -rtests/requirements/sanic-20.txt + fl{21,22}: -rtests/requirements/flask.txt s{21,22}: -rtests/requirements/sanic.txt dj32: -ctests/constraints/django-3.2.txt dj40: -ctests/constraints/django-4.0.txt dj41: -ctests/constraints/django-4.1.txt - fl012: -ctests/constraints/flask-0.12.txt - fl10: -ctests/constraints/flask-1.0.txt - fl11: -ctests/constraints/flask-1.1.txt - fl20: -ctests/constraints/flask-2.0.txt fl21: -ctests/constraints/flask-2.1.txt fl22: -ctests/constraints/flask-2.2.txt - s20: -ctests/constraints/sanic-20.txt s21: -ctests/constraints/sanic-21.txt s22: -ctests/constraints/sanic-22.txt commands = From 804a6e0b50c99aea8fdb104ee0c65341f24961f6 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 11:08:47 -0400 Subject: [PATCH 05/17] Edit tests with new library constraints in mind --- src/dockerflow/django/middleware.py | 22 +--- src/dockerflow/flask/checks/__init__.py | 4 +- src/dockerflow/sanic/checks.py | 6 +- tests/django/test_django.py | 6 +- tests/flask/test_flask.py | 133 ++++++++++++------------ 5 files changed, 73 insertions(+), 98 deletions(-) diff --git a/src/dockerflow/django/middleware.py b/src/dockerflow/django/middleware.py index 5b3ffe6..358f3c6 100644 --- a/src/dockerflow/django/middleware.py +++ b/src/dockerflow/django/middleware.py @@ -3,28 +3,10 @@ import time import uuid -from django import VERSION +from django.utils.deprecation import MiddlewareMixin from . import views -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: # pragma: no cover - MiddlewareMixin = object - - -# Computed once, reused in every request -_less_than_django_1_10 = VERSION < (1, 10) - - -def is_authenticated(user): # pragma: no cover - """Check if the user is authenticated but do it in a way that - it doesnt' cause a DeprecationWarning in Django >=1.10""" - if _less_than_django_1_10: - # Prior to Django 1.10, user.is_authenticated was a method - return user.is_authenticated() - return user.is_authenticated - class DockerflowMiddleware(MiddlewareMixin): """ @@ -66,7 +48,7 @@ def _build_extra_meta(self, request): # modified earlier, so be sure to check for existence of these # attributes before trying to use them. if hasattr(request, "user"): - out["uid"] = is_authenticated(request.user) and request.user.pk or "" + out["uid"] = request.user.is_authenticated and request.user.pk or "" if hasattr(request, "_id"): out["rid"] = request._id if hasattr(request, "_start_timestamp"): diff --git a/src/dockerflow/flask/checks/__init__.py b/src/dockerflow/flask/checks/__init__.py index c73483b..77c8ce7 100644 --- a/src/dockerflow/flask/checks/__init__.py +++ b/src/dockerflow/flask/checks/__init__.py @@ -4,6 +4,8 @@ """ This module contains a few built-in checks for the Flask integration. """ +from sqlalchemy import text + from ... import health from ...checks import ( # noqa CRITICAL, @@ -47,7 +49,7 @@ def check_database_connected(db): errors = [] try: with db.engine.connect() as connection: - connection.execute("SELECT 1;") + connection.execute(text("SELECT 1;")) except DBAPIError as e: msg = "DB-API error: {!s}".format(e) errors.append(Error(msg, id=health.ERROR_DB_API_EXCEPTION)) diff --git a/src/dockerflow/sanic/checks.py b/src/dockerflow/sanic/checks.py index 50c8925..e47ef4e 100644 --- a/src/dockerflow/sanic/checks.py +++ b/src/dockerflow/sanic/checks.py @@ -56,10 +56,8 @@ async def check_redis_connected(redis): """ import aioredis - if aioredis.__version__.startswith("1."): - RedisConnectionError = aioredis.ConnectionClosedError - else: - RedisConnectionError = aioredis.ConnectionError + + RedisConnectionError = aioredis.ConnectionError errors = [] diff --git a/tests/django/test_django.py b/tests/django/test_django.py index 748aed4..cfa83b1 100644 --- a/tests/django/test_django.py +++ b/tests/django/test_django.py @@ -18,10 +18,8 @@ from dockerflow.django import checks from dockerflow.django.middleware import DockerflowMiddleware -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: # pragma: no cover - MiddlewareMixin = object + +from django.utils.deprecation import MiddlewareMixin @pytest.fixture diff --git a/tests/flask/test_flask.py b/tests/flask/test_flask.py index e28c852..d41fc14 100644 --- a/tests/flask/test_flask.py +++ b/tests/flask/test_flask.py @@ -29,7 +29,8 @@ def load_user(user_id): return MockUser(user_id) -def create_app(): +@pytest.fixture +def app(): app = Flask("dockerflow") app.secret_key = "super sekrit" login_manager = LoginManager(app) @@ -37,9 +38,9 @@ def create_app(): return app -@pytest.fixture -def app(): - return create_app() +@pytest.fixture() +def client(app): + return app.test_client() @pytest.fixture @@ -72,28 +73,23 @@ def test_instantiating(app): assert "dockerflow.heartbeat" in app.view_functions -def test_version_exists(dockerflow, mocker, app, version_content): +def test_version_exists(dockerflow, mocker, version_content, client): mocker.patch.object(dockerflow, "_version_callback", return_value=version_content) - response = app.test_client().get("/__version__") + response = client.get("/__version__") assert response.status_code == 200 assert json.loads(response.data.decode()) == version_content -def test_version_path(mocker, version_content): - app = Flask("dockerflow") - app.secret_key = "super sekrit" - login_manager = LoginManager(app) - login_manager.user_loader(load_user) +def test_version_path(mocker, app, client, version_content): custom_version_path = "/something/extra/ordinary" dockerflow = Dockerflow(app, version_path=custom_version_path) version_callback = mocker.patch.object( dockerflow, "_version_callback", return_value=version_content ) - with app.test_client() as test_client: - response = test_client.get("/__version__") - assert response.status_code == 200 - assert json.loads(response.data.decode()) == version_content - version_callback.assert_called_with(custom_version_path) + response = client.get("/__version__") + assert response.status_code == 200 + assert json.loads(response.data.decode()) == version_content + version_callback.assert_called_with(custom_version_path) def test_version_missing(dockerflow, mocker, app): @@ -144,10 +140,11 @@ def warning_check2(): def test_lbheartbeat_makes_no_db_queries(dockerflow, app): - assert len(get_debug_queries()) == 0 - response = app.test_client().get("/__lbheartbeat__") - assert response.status_code == 200 - assert len(get_debug_queries()) == 0 + with app.app_context(): + assert len(get_debug_queries()) == 0 + response = app.test_client().get("/__lbheartbeat__") + assert response.status_code == 200 + assert len(get_debug_queries()) == 0 def test_full_redis_check(mocker): @@ -177,37 +174,26 @@ def test_full_redis_check_error(mocker): assert json.loads(response.data.decode())["status"] == "error" -def test_full_db_check(mocker): - app = Flask("db-check") - app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" - app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False - db = SQLAlchemy(app) +def test_full_db_check(mocker, app, db, client): dockerflow = Dockerflow(app, db=db) assert "check_database_connected" in dockerflow.checks - response = app.test_client().get("/__heartbeat__") + response = client.get("/__heartbeat__") assert response.status_code == 200 assert json.loads(response.data.decode())["status"] == "ok" -def test_full_db_check_error(mocker): - app = Flask("db-check") - app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" - app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False - db = SQLAlchemy(app) - - engine_connect = mocker.patch.object(db.engine, "connect") - engine_connect.side_effect = SQLAlchemyError - dockerflow = Dockerflow(app, db=db) - assert "check_database_connected" in dockerflow.checks - - with app.test_client() as test_client: - response = test_client.get("/__heartbeat__") +def test_full_db_check_error(mocker, app, db, client): + with app.app_context(): + mocker.patch.object(db.engine, "connect", side_effect=SQLAlchemyError) + dockerflow = Dockerflow(app, db=db) + assert "check_database_connected" in dockerflow.checks + response = client.get("/__heartbeat__") assert response.status_code == 500 assert json.loads(response.data.decode())["status"] == "error" -def assert_log_record(request, record, errno=0, level=logging.INFO): +def assert_log_record(record, errno=0, level=logging.INFO): assert record.levelno == level assert record.errno == errno assert record.agent == "dockerflow/tests" @@ -221,17 +207,18 @@ def assert_log_record(request, record, errno=0, level=logging.INFO): headers = {"User-Agent": "dockerflow/tests", "Accept-Language": "tlh"} -def test_request_summary(caplog, dockerflow, app): - with app.test_client() as test_client: - test_client.get("/", headers=headers) +def test_request_summary(caplog, app, dockerflow, client): + caplog.set_level(logging.INFO) + with app.test_request_context("/"): + client.get("/", headers=headers) assert getattr(g, "_request_id") is not None assert getattr(g, "request_id") is not None assert isinstance(getattr(g, "_start_timestamp"), float) assert len(caplog.records) == 1 - for record in caplog.records: - assert_log_record(request, record) - assert getattr(request, "uid", None) is None + record = caplog.records[0] + assert_log_record(record) + assert getattr(request, "uid", None) is None def test_preserves_existing_request_id(dockerflow, app): @@ -256,17 +243,19 @@ def assert_user(app, caplog, user, callback): response = Response("") response = app.process_response(response) assert len(caplog.records) == 1 - for record in caplog.records: - assert_log_record(request, record) - assert record.uid == callback(user) + record = caplog.records[0] + assert_log_record(record) + assert record.uid == callback(user) -def test_request_summary_user_success(caplog, dockerflow, mocker, app): +def test_request_summary_user_success(caplog, dockerflow, app): + caplog.set_level(logging.INFO) user = MockUser(100) assert_user(app, caplog, user, lambda user: user.get_id()) def test_request_summary_user_is_authenticated_missing(caplog, dockerflow, app): + caplog.set_level(logging.INFO) class MissingIsAuthenticatedUser(object): id = 0 is_active = True @@ -278,6 +267,7 @@ def get_id(self): def test_request_summary_user_is_authenticated_callable(caplog, dockerflow, app): + caplog.set_level(logging.INFO) class CallableIsAuthenticatedUser(object): id = 0 is_active = True @@ -292,6 +282,7 @@ def is_authenticated(self): def test_request_summary_user_flask_login_missing(caplog, dockerflow, app, monkeypatch): + caplog.set_level(logging.INFO) monkeypatch.setattr("dockerflow.flask.app.has_flask_login", False) user = MockUser(100) assert_user(app, caplog, user, lambda user: "") @@ -314,6 +305,7 @@ def test_request_summary_exception(caplog, app): def test_request_summary_failed_request(caplog, dockerflow, app): + caplog.set_level(logging.INFO) @app.before_request def hostile_callback(): # simulating resetting request changes @@ -327,25 +319,26 @@ def hostile_callback(): assert getattr(record, "t", None) is None -def test_db_check_sqlalchemy_error(mocker, db): - engine_connect = mocker.patch.object(db.engine, "connect") - engine_connect.side_effect = SQLAlchemyError - errors = checks.check_database_connected(db) +def test_db_check_sqlalchemy_error(app, mocker, db): + with app.app_context(): + mocker.patch.object(db.engine, "connect", side_effect=SQLAlchemyError) + errors = checks.check_database_connected(db) assert len(errors) == 1 assert errors[0].id == health.ERROR_SQLALCHEMY_EXCEPTION -def test_db_check_dbapi_error(mocker, db): - exception = DBAPIError.instance("", [], Exception(), Exception) - engine_connect = mocker.patch.object(db.engine, "connect") - engine_connect.side_effect = exception - errors = checks.check_database_connected(db) +def test_db_check_dbapi_error(app, mocker, db): + with app.app_context(): + exception = DBAPIError.instance("", [], Exception(), Exception) + mocker.patch.object(db.engine, "connect", side_effect=exception) + errors = checks.check_database_connected(db) assert len(errors) == 1 assert errors[0].id == health.ERROR_DB_API_EXCEPTION -def test_db_check_success(db): - errors = checks.check_database_connected(db) +def test_db_check_success(app, db): + with app.app_context(): + errors = checks.check_database_connected(db) assert errors == [] @@ -374,16 +367,16 @@ def test_check_message(): [SQLAlchemyError(), DBAPIError.instance("", [], Exception(), Exception)], ) def test_check_migrations_applied_cannot_check_migrations( - exception, mocker, db, migrate + exception, mocker, app, db, migrate ): - engine_connect = mocker.patch.object(db.engine, "connect") - engine_connect.side_effect = exception - errors = checks.check_migrations_applied(migrate) + with app.app_context(): + mocker.patch.object(db.engine, "connect", side_effect=exception) + errors = checks.check_migrations_applied(migrate) assert len(errors) == 1 assert errors[0].id == health.INFO_CANT_CHECK_MIGRATIONS -def test_check_migrations_applied_success(mocker, db, migrate): +def test_check_migrations_applied_success(mocker, app, db, migrate): get_heads = mocker.patch( "alembic.script.ScriptDirectory.get_heads", return_value=("17164a7d1c2e",) ) @@ -391,13 +384,14 @@ def test_check_migrations_applied_success(mocker, db, migrate): "alembic.migration.MigrationContext.get_current_heads", return_value=("17164a7d1c2e",), ) - errors = checks.check_migrations_applied(migrate) + with app.app_context(): + errors = checks.check_migrations_applied(migrate) assert get_heads.called assert get_current_heads.called assert len(errors) == 0 -def test_check_migrations_applied_unapplied_migrations(mocker, db, migrate): +def test_check_migrations_applied_unapplied_migrations(mocker, app, db, migrate): get_heads = mocker.patch( "alembic.script.ScriptDirectory.get_heads", return_value=("7f447c94347a",) ) @@ -405,7 +399,8 @@ def test_check_migrations_applied_unapplied_migrations(mocker, db, migrate): "alembic.migration.MigrationContext.get_current_heads", return_value=("73d96d3120ff",), ) - errors = checks.check_migrations_applied(migrate) + with app.app_context(): + errors = checks.check_migrations_applied(migrate) assert get_heads.called assert get_current_heads.called assert len(errors) == 1 From 6a1d49881a74b64f5b00a6fd53c9c27dcee0bd3e Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 11:33:39 -0400 Subject: [PATCH 06/17] Replace "aioredis" with "redis" --- tests/requirements/sanic.txt | 2 +- tests/sanic/test_sanic.py | 19 ++++--------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/tests/requirements/sanic.txt b/tests/requirements/sanic.txt index 20ffec4..c18b730 100644 --- a/tests/requirements/sanic.txt +++ b/tests/requirements/sanic.txt @@ -1,7 +1,7 @@ # these are constrained by the files in tests/constraints/*.txt # to support a triple stack Django/Flask/Sanic aiohttp -aioredis +redis Sanic sanic_redis sanic-testing diff --git a/tests/sanic/test_sanic.py b/tests/sanic/test_sanic.py index b3c4be5..9373675 100644 --- a/tests/sanic/test_sanic.py +++ b/tests/sanic/test_sanic.py @@ -5,10 +5,10 @@ import logging import uuid -import aioredis import pytest import sanic import sanic_redis.core +from redis import asyncio as aioredis from sanic import Sanic, response from sanic_redis import SanicRedis @@ -43,10 +43,7 @@ async def wait_closed(self): async def ping(self): if self.error == "connection": - if aioredis.__version__.startswith("1."): - RedisConnectionError = aioredis.ConnectionClosedError - else: - RedisConnectionError = aioredis.ConnectionError + RedisConnectionError = aioredis.ConnectionError raise RedisConnectionError("fake") elif self.error == "redis": raise aioredis.RedisError("fake") @@ -62,8 +59,6 @@ def close(self): async def fake_redis(*args, **kw): - if aioredis.__version__.startswith("1."): - return FakeRedis1(*args, **kw) return FakeRedis(*args, **kw) @@ -191,10 +186,7 @@ async def warning_check2(): def test_redis_check(dockerflow_redis, mocker, test_client): assert "check_redis_connected" in dockerflow_redis.checks - if aioredis.__version__.startswith("1."): - mocker.patch.object(sanic_redis.core, "create_redis_pool", fake_redis) - else: - mocker.patch.object(sanic_redis.core, "from_url", fake_redis) + mocker.patch.object(sanic_redis.core, "from_url", fake_redis) _, response = test_client.get("/__heartbeat__") assert response.status == 200 assert response.json["status"] == "ok" @@ -214,10 +206,7 @@ def test_redis_check(dockerflow_redis, mocker, test_client): def test_redis_check_error(dockerflow_redis, mocker, test_client, error, messages): assert "check_redis_connected" in dockerflow_redis.checks fake_redis_error = functools.partial(fake_redis, error=error) - if aioredis.__version__.startswith("1."): - mocker.patch.object(sanic_redis.core, "create_redis_pool", fake_redis_error) - else: - mocker.patch.object(sanic_redis.core, "from_url", fake_redis_error) + mocker.patch.object(sanic_redis.core, "from_url", fake_redis_error) _, response = test_client.get("/__heartbeat__") assert response.status == 500 assert response.json["status"] == "error" From e696d2a5e5fc1a74543a567fbb80332786edfeba Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 11:33:50 -0400 Subject: [PATCH 07/17] Formatting fixes --- src/dockerflow/sanic/app.py | 1 - src/dockerflow/sanic/checks.py | 3 +-- tests/django/test_django.py | 4 +--- tests/flask/test_flask.py | 3 +++ 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/dockerflow/sanic/app.py b/src/dockerflow/sanic/app.py index 2ef5c02..5457c0c 100644 --- a/src/dockerflow/sanic/app.py +++ b/src/dockerflow/sanic/app.py @@ -84,7 +84,6 @@ def __init__( *args, **kwargs, ): - # The Dockerflow specific logger to be used by internals of this # extension. self.logger = logging.getLogger("dockerflow.sanic") diff --git a/src/dockerflow/sanic/checks.py b/src/dockerflow/sanic/checks.py index e47ef4e..cc31547 100644 --- a/src/dockerflow/sanic/checks.py +++ b/src/dockerflow/sanic/checks.py @@ -54,8 +54,7 @@ async def check_redis_connected(redis): dockerflow = Dockerflow(app, redis=redis) """ - import aioredis - + from redis import asyncio as aioredis RedisConnectionError = aioredis.ConnectionError diff --git a/tests/django/test_django.py b/tests/django/test_django.py index cfa83b1..6270f3e 100644 --- a/tests/django/test_django.py +++ b/tests/django/test_django.py @@ -13,15 +13,13 @@ from django.db.utils import OperationalError, ProgrammingError from django.http import HttpResponse from django.test.utils import CaptureQueriesContext +from django.utils.deprecation import MiddlewareMixin from dockerflow import health from dockerflow.django import checks from dockerflow.django.middleware import DockerflowMiddleware -from django.utils.deprecation import MiddlewareMixin - - @pytest.fixture def reset_checks(): if django_version[0] < 2: diff --git a/tests/flask/test_flask.py b/tests/flask/test_flask.py index d41fc14..7b345a9 100644 --- a/tests/flask/test_flask.py +++ b/tests/flask/test_flask.py @@ -256,6 +256,7 @@ def test_request_summary_user_success(caplog, dockerflow, app): def test_request_summary_user_is_authenticated_missing(caplog, dockerflow, app): caplog.set_level(logging.INFO) + class MissingIsAuthenticatedUser(object): id = 0 is_active = True @@ -268,6 +269,7 @@ def get_id(self): def test_request_summary_user_is_authenticated_callable(caplog, dockerflow, app): caplog.set_level(logging.INFO) + class CallableIsAuthenticatedUser(object): id = 0 is_active = True @@ -306,6 +308,7 @@ def test_request_summary_exception(caplog, app): def test_request_summary_failed_request(caplog, dockerflow, app): caplog.set_level(logging.INFO) + @app.before_request def hostile_callback(): # simulating resetting request changes From c5917aad9d9281dc3e2861ad2acff2437d0896d7 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 12:06:09 -0400 Subject: [PATCH 08/17] Remove redis dependency from sanic deps, since it's a default --- tests/requirements/sanic.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/requirements/sanic.txt b/tests/requirements/sanic.txt index c18b730..0800e11 100644 --- a/tests/requirements/sanic.txt +++ b/tests/requirements/sanic.txt @@ -1,7 +1,6 @@ # these are constrained by the files in tests/constraints/*.txt # to support a triple stack Django/Flask/Sanic aiohttp -redis Sanic sanic_redis sanic-testing From 2edc48f5e0daa9d42813b7e5a87480859218667f Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 12:20:15 -0400 Subject: [PATCH 09/17] Remove checks for old sanic versions --- tests/sanic/test_sanic.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/tests/sanic/test_sanic.py b/tests/sanic/test_sanic.py index 9373675..b76d31c 100644 --- a/tests/sanic/test_sanic.py +++ b/tests/sanic/test_sanic.py @@ -11,15 +11,11 @@ from redis import asyncio as aioredis from sanic import Sanic, response from sanic_redis import SanicRedis +from sanic_testing.testing import SanicTestClient from dockerflow import health from dockerflow.sanic import Dockerflow, checks -if sanic.__version__.startswith("20."): - from sanic.testing import SanicTestClient -else: - from sanic_testing.testing import SanicTestClient - class FakeRedis: def __init__(self, *args, error=None, **kw): @@ -91,17 +87,6 @@ def test_client(app): return SanicTestClient(app) -@pytest.mark.skipif(not sanic.__version__.startswith("20."), reason="requires sanic 20") -def test_instantiating_sanic_20(app): - dockerflow = Dockerflow() - assert "dockerflow.heartbeat" not in app.router.routes_names - dockerflow.init_app(app) - assert "dockerflow.heartbeat" in app.router.routes_names - - -@pytest.mark.skipif( - sanic.__version__.startswith("20."), reason="requires sanic 21 or later" -) def test_instantiating(app): Dockerflow() assert ("__heartbeat__",) not in app.router.routes_all From d691ee8f278e7d810859c19906bda2c7672d628e Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 12:29:14 -0400 Subject: [PATCH 10/17] Add @grahamalama to authors Also, alphabetize --- AUTHORS.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 13aa359..f9056fd 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,8 +1,9 @@ - Peter Bengtsson (@peterbe) +- Graham Beckley (@grahamalama) - Mike Cooper (@mythmon) - Will Kahn-Greene (@willkg) - Michael Kelly (@Osmose) - Jannis Leidel (@jezdez) -- Les Orchard (@lmorchard) - Mathieu Leplatre (@leplatrem) +- Les Orchard (@lmorchard) - Mathieu Pillard (@diox) From 0b06e10eeabf5b8e2f058c868ba36bebc1bff156 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 13:54:39 -0400 Subject: [PATCH 11/17] Update changelog --- docs/changelog.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8984677..7febb27 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,14 +1,14 @@ Changelog --------- -2022.8.0 (2022-08-18) +2023.8.0 ~~~~~~~~~~~~~~~~~~~~~ -- Add support for Sanic 21 and 22, with `aioredis` 2.x +- Drop support for Sanic 20 -- Add support for Django 4.1 +- Drop support for Flask 0.12, 1.0, 1.1, and 2.0 -- Add support for Flask 2.2 +- Add support for Python 3.11 2022.7.0 (2022-07-12) ~~~~~~~~~~~~~~~~~~~~~ From b6cd09e6f7a0b9046318e3e2ad8f878ad0f0d272 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 16:37:59 -0400 Subject: [PATCH 12/17] Put changelog entry for 2022.8.0 back --- docs/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7febb27..93a9ddb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,15 @@ Changelog - Add support for Python 3.11 +2022.8.0 (2022-08-18) +~~~~~~~~~~~~~~~~~~~~~ + +- Add support for Sanic 21 and 22, with `aioredis` 2.x + +- Add support for Django 4.1 + +- Add support for Flask 2.2 + 2022.7.0 (2022-07-12) ~~~~~~~~~~~~~~~~~~~~~ From 5d663ec69a6f83e5cfe8e139ffe0156de7083239 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 16:54:10 -0400 Subject: [PATCH 13/17] Remove unneeded sanic import --- tests/sanic/test_sanic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sanic/test_sanic.py b/tests/sanic/test_sanic.py index b76d31c..249dea1 100644 --- a/tests/sanic/test_sanic.py +++ b/tests/sanic/test_sanic.py @@ -6,7 +6,6 @@ import uuid import pytest -import sanic import sanic_redis.core from redis import asyncio as aioredis from sanic import Sanic, response From 603a9724843a9d4521d274b8f9ee60266e4f2107 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 18:49:22 -0400 Subject: [PATCH 14/17] Add back support for Flask 2.0 --- tests/constraints/flask-2.0.txt | 4 ++++ tox.ini | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 tests/constraints/flask-2.0.txt diff --git a/tests/constraints/flask-2.0.txt b/tests/constraints/flask-2.0.txt new file mode 100644 index 0000000..7997c7e --- /dev/null +++ b/tests/constraints/flask-2.0.txt @@ -0,0 +1,4 @@ +Flask<2.1 +Werkzeug<2.1.0 +SQLAlchemy<=1.4 +Flask-SQLAlchemy<3.0 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 0d6c414..eac15bb 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ envlist = py38-docs py{37,38,39,310,311}-dj32 py{38,39,310,311}-dj{40,41} - py{37,38,39,310,311}-fl{21,22} + py{37,38,39,310,311}-fl{20,21,22} py{37,38,39,310,311}-s{21,22} [gh-actions] @@ -26,11 +26,12 @@ setenv = deps = -rtests/requirements/default.txt dj{32,40,41}: -rtests/requirements/django.txt - fl{21,22}: -rtests/requirements/flask.txt + fl{20,21,22}: -rtests/requirements/flask.txt s{21,22}: -rtests/requirements/sanic.txt dj32: -ctests/constraints/django-3.2.txt dj40: -ctests/constraints/django-4.0.txt dj41: -ctests/constraints/django-4.1.txt + fl20: -ctests/constraints/flask-2.0.txt fl21: -ctests/constraints/flask-2.1.txt fl22: -ctests/constraints/flask-2.2.txt s21: -ctests/constraints/sanic-21.txt From b850e8202bf7a7ff15ff63b4b2f6199caedec3c2 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 19:26:41 -0400 Subject: [PATCH 15/17] Make Sanic redis changes - Make error imports cleaner - Remove unused fake implementation --- src/dockerflow/sanic/checks.py | 12 +++++------- tests/sanic/test_sanic.py | 12 +++--------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/dockerflow/sanic/checks.py b/src/dockerflow/sanic/checks.py index cc31547..4e3f21a 100644 --- a/src/dockerflow/sanic/checks.py +++ b/src/dockerflow/sanic/checks.py @@ -22,7 +22,7 @@ ) -async def check_redis_connected(redis): +async def check_redis_connected(redis_client): """ A built-in check to connect to Redis using the given client and see if it responds to the ``PING`` command. @@ -54,19 +54,17 @@ async def check_redis_connected(redis): dockerflow = Dockerflow(app, redis=redis) """ - from redis import asyncio as aioredis - - RedisConnectionError = aioredis.ConnectionError + import redis errors = [] try: - with await redis.conn as r: + with await redis_client.conn as r: result = await r.ping() - except RedisConnectionError as e: + except redis.ConnectionError as e: msg = "Could not connect to redis: {!s}".format(e) errors.append(Error(msg, id=health.ERROR_CANNOT_CONNECT_REDIS)) - except aioredis.RedisError as e: + except redis.RedisError as e: errors.append( Error('Redis error: "{!s}"'.format(e), id=health.ERROR_REDIS_EXCEPTION) ) diff --git a/tests/sanic/test_sanic.py b/tests/sanic/test_sanic.py index 249dea1..745052f 100644 --- a/tests/sanic/test_sanic.py +++ b/tests/sanic/test_sanic.py @@ -6,8 +6,8 @@ import uuid import pytest +import redis import sanic_redis.core -from redis import asyncio as aioredis from sanic import Sanic, response from sanic_redis import SanicRedis from sanic_testing.testing import SanicTestClient @@ -38,21 +38,15 @@ async def wait_closed(self): async def ping(self): if self.error == "connection": - RedisConnectionError = aioredis.ConnectionError - raise RedisConnectionError("fake") + raise redis.ConnectionError("fake") elif self.error == "redis": - raise aioredis.RedisError("fake") + raise redis.RedisError("fake") elif self.error == "malformed": return b"PING" else: return b"PONG" -class FakeRedis1(FakeRedis): - def close(self): - pass - - async def fake_redis(*args, **kw): return FakeRedis(*args, **kw) From 3dccd82419b75150a7e62dfb172fa602fac6b7c7 Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 19:46:25 -0400 Subject: [PATCH 16/17] Add support for Django 4.2 --- tests/constraints/django-4.2.txt | 1 + tox.ini | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 tests/constraints/django-4.2.txt diff --git a/tests/constraints/django-4.2.txt b/tests/constraints/django-4.2.txt new file mode 100644 index 0000000..804f9e1 --- /dev/null +++ b/tests/constraints/django-4.2.txt @@ -0,0 +1 @@ +Django>=4.2,<4.3 diff --git a/tox.ini b/tox.ini index eac15bb..a1d99fe 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py38-lint py38-docs py{37,38,39,310,311}-dj32 - py{38,39,310,311}-dj{40,41} + py{38,39,310,311}-dj{40,41,42} py{37,38,39,310,311}-fl{20,21,22} py{37,38,39,310,311}-s{21,22} @@ -25,12 +25,13 @@ setenv = PYTHONPATH = {toxinidir} deps = -rtests/requirements/default.txt - dj{32,40,41}: -rtests/requirements/django.txt + dj{32,40,41,42}: -rtests/requirements/django.txt fl{20,21,22}: -rtests/requirements/flask.txt s{21,22}: -rtests/requirements/sanic.txt dj32: -ctests/constraints/django-3.2.txt dj40: -ctests/constraints/django-4.0.txt dj41: -ctests/constraints/django-4.1.txt + dj42: -ctests/constraints/django-4.2.txt fl20: -ctests/constraints/flask-2.0.txt fl21: -ctests/constraints/flask-2.1.txt fl22: -ctests/constraints/flask-2.2.txt @@ -38,7 +39,7 @@ deps = s22: -ctests/constraints/sanic-22.txt commands = python --version - dj{32,40,41}: pytest tests/core/ tests/django --nomigrations {posargs:} + dj{32,40,41,42}: pytest tests/core/ tests/django --nomigrations {posargs:} fl{012,10,11,20,21,22}: pytest tests/core/ tests/flask/ {posargs:} s{20,21,22}: pytest tests/core/ tests/sanic/ {posargs:} From 9631f045fa69cecd703efdbfd6b1430fbbfdefef Mon Sep 17 00:00:00 2001 From: Graham Beckley Date: Tue, 1 Aug 2023 19:50:34 -0400 Subject: [PATCH 17/17] Update changelog with new supported libraries --- docs/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 93a9ddb..9dcffa0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,9 +4,11 @@ Changelog 2023.8.0 ~~~~~~~~~~~~~~~~~~~~~ +- Add support for Django 4.2 + - Drop support for Sanic 20 -- Drop support for Flask 0.12, 1.0, 1.1, and 2.0 +- Drop support for Flask 0.12, 1.0, and 1.1 - Add support for Python 3.11