diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1f1859..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.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] # Service containers to run with `container-job` services: 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) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8984677..9dcffa0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ Changelog --------- +2023.8.0 +~~~~~~~~~~~~~~~~~~~~~ + +- Add support for Django 4.2 + +- Drop support for Sanic 20 + +- Drop support for Flask 0.12, 1.0, and 1.1 + +- Add support for Python 3.11 + 2022.8.0 (2022-08-18) ~~~~~~~~~~~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index bfc30ba..63c8fb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 88 -target-version = ['py37', 'py38', 'py39', 'py310'] +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] include = '\.pyi?$' exclude = ''' /( diff --git a/setup.py b/setup.py index e05b33f..d1d6ead 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,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/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/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 50c8925..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,22 +54,17 @@ async def check_redis_connected(redis): dockerflow = Dockerflow(app, redis=redis) """ - import aioredis - - if aioredis.__version__.startswith("1."): - RedisConnectionError = aioredis.ConnectionClosedError - else: - 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/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/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 index a6675ab..7997c7e 100644 --- a/tests/constraints/flask-2.0.txt +++ b/tests/constraints/flask-2.0.txt @@ -1,2 +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/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/django/test_django.py b/tests/django/test_django.py index 748aed4..6270f3e 100644 --- a/tests/django/test_django.py +++ b/tests/django/test_django.py @@ -13,16 +13,12 @@ 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 -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: # pragma: no cover - MiddlewareMixin = object - @pytest.fixture def reset_checks(): diff --git a/tests/flask/test_flask.py b/tests/flask/test_flask.py index e28c852..7b345a9 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,20 @@ 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 +268,8 @@ 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 +284,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 +307,8 @@ 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 +322,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 +370,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 +387,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 +402,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 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/tests/requirements/sanic.txt b/tests/requirements/sanic.txt index 20ffec4..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 -aioredis Sanic sanic_redis sanic-testing diff --git a/tests/sanic/test_sanic.py b/tests/sanic/test_sanic.py index b3c4be5..745052f 100644 --- a/tests/sanic/test_sanic.py +++ b/tests/sanic/test_sanic.py @@ -5,21 +5,16 @@ import logging import uuid -import aioredis import pytest -import sanic +import redis import sanic_redis.core 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): @@ -43,27 +38,16 @@ 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 - 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): - if aioredis.__version__.startswith("1."): - return FakeRedis1(*args, **kw) return FakeRedis(*args, **kw) @@ -96,17 +80,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 @@ -191,10 +164,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 +184,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" diff --git a/tox.ini b/tox.ini index b28cbcb..a1d99fe 100644 --- a/tox.ini +++ b/tox.ini @@ -4,11 +4,10 @@ minversion = 1.8 envlist = py38-lint py38-docs - py{37,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{37,38,39,310,311}-dj32 + 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} [gh-actions] python = @@ -16,6 +15,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [testenv] usedevelop = true @@ -25,25 +25,21 @@ setenv = PYTHONPATH = {toxinidir} 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 + 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 - fl012: -ctests/constraints/flask-0.12.txt - fl10: -ctests/constraints/flask-1.0.txt - fl11: -ctests/constraints/flask-1.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 - s20: -ctests/constraints/sanic-20.txt s21: -ctests/constraints/sanic-21.txt 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:}