From b1df78f045bd90ef7fedf7ee1118cadb699aaf2d Mon Sep 17 00:00:00 2001 From: Alexander Valenchits Date: Sun, 6 Sep 2026 18:45:11 +0000 Subject: [PATCH] feat: add Python 3.14 support - Add Python 3.14 classifier, bump version to 1.4.0 - Extend CI test matrix to 3.9-3.14 and add test-extras job (tortoise/peewee extras) running on 3.14 - Cap tortoise-orm<1.0 and peewee<4 in optional extras (breaking major releases) - Fix Tortoise adapter tests: register shared models via tests/tortoise_models.py instead of unregistered local classes - Fix Peewee delete_element test to expect DoesNotExist - Add RELEASE_1.4.0.md and update RELEASE_NOTES.md --- .github/workflows/test.yml | 24 +++++++++++- RELEASE_1.4.0.md | 49 ++++++++++++++++++++++++ RELEASE_NOTES.md | 13 +++++++ pyproject.toml | 9 +++-- tests/test_adapter_methods_coverage.py | 53 ++++++++++---------------- tests/test_exception_handling.py | 38 +++++++----------- tests/test_missing_coverage.py | 3 +- tests/tortoise_models.py | 28 ++++++++++++++ 8 files changed, 153 insertions(+), 64 deletions(-) create mode 100644 RELEASE_1.4.0.md create mode 100644 tests/tortoise_models.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c47ba3a..494656c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 @@ -61,3 +61,25 @@ jobs: path: htmlcov/ retention-days: 30 + # Exercise the ORM adapters (Tortoise/Peewee) on the newest supported + # Python. The main matrix only installs the [test] extra, so adapter + # tests are skipped there โ€” this job makes sure they actually run. + test-extras: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[test,tortoise,peewee]" + + - name: Run tests + run: | + pytest tests/ -v --cov=fastapi_viewsets --cov-report=term-missing --cov-fail-under=70 + diff --git a/RELEASE_1.4.0.md b/RELEASE_1.4.0.md new file mode 100644 index 0000000..ea602b3 --- /dev/null +++ b/RELEASE_1.4.0.md @@ -0,0 +1,49 @@ +# Release v1.4.0 + +## โœจ What's New + +Official support for **Python 3.14**, plus dependency bounds that keep the +optional ORM adapters on known-compatible major versions. + +## ๐Ÿ Python 3.14 Support + +- `pyproject.toml` now declares `Programming Language :: Python :: 3.14`, so + PyPI and the version badge reflect the new interpreter. +- The CI test matrix runs on Python 3.9 โ€“ 3.14. +- `black` `target-version` extended with `py314`. +- The full test suite (263 tests) passes on Python 3.14.3 with current + dependency versions: FastAPI 0.141, Pydantic 2.12, SQLAlchemy 2.0.52. + +## ๐Ÿ“ฆ Dependency Bounds + +- `tortoise` extra: `tortoise-orm>=0.20.0,<1.0` โ€” Tortoise ORM 1.x introduces + breaking API changes (`TortoiseContext`, removal of implicit global state) + that are not compatible with this adapter yet. +- `peewee` extra: `peewee>=3.17.0,<4` โ€” Peewee 4.x changes exception + behaviour of `ModelSelect.get()` (raw `IndexError` instead of + `DoesNotExist`), which breaks adapters and tests. +- Core dependencies are unchanged (`fastapi>=0.110`, `pydantic>=2.5,<3`, + `SQLAlchemy>=1.4.36`) โ€” current releases of all of them already support + Python 3.14. + +## ๐Ÿงช Testing + +- Fixed previously broken Tortoise adapter tests: models are now properly + registered through `models=["tests.tortoise_models"]` instead of relying + on unregistered locally-defined models, and schemas are generated with + `Tortoise.generate_schemas(safe=True)`. +- Fixed the Peewee `delete_element` test to expect `DoesNotExist` after + deletion instead of a `None` return from `get_by_id()`. +- New CI job `test-extras` runs the suite with the `tortoise` and `peewee` + extras installed on Python 3.14, so adapter tests are no longer skipped + in CI. + +## ๐Ÿ“ฆ Installation + +```bash +pip install fastapi-viewsets +``` + +--- + +**Full Changelog**: https://github.com/svalench/fastapi_viewsets/compare/v1.3.0...v1.4.0 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0e1c741..a4ee345 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,18 @@ # Release Notes +## Version 1.4.0 + +### ๐Ÿ Python 3.14 + +- Official Python 3.14 support: new PyPI classifier, CI matrix extended to + 3.9 โ€“ 3.14, and the full test suite verified on 3.14.3. +- Dependency bounds for optional adapters: `tortoise-orm>=0.20,<1.0` and + `peewee>=3.17,<4` (Tortoise 1.x and Peewee 4.x contain breaking changes). +- Fixed Tortoise/Peewee adapter tests that were silently skipped or broken + in CI, and added a `test-extras` CI job that exercises them on 3.14. + +Details: [RELEASE_1.4.0.md](RELEASE_1.4.0.md). + ## Version 1.3.0 ### โœจ Highlights diff --git a/pyproject.toml b/pyproject.toml index c6fe18d..96fdeda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "fastapi_viewsets" -version = "1.3.0" +version = "1.4.0" description = "DRF-style viewsets for FastAPI with SQLAlchemy/Tortoise/Peewee adapters and Pydantic v2 support." readme = "README.md" license = { text = "MIT" } @@ -18,6 +18,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Framework :: FastAPI", @@ -34,8 +35,8 @@ dependencies = [ [project.optional-dependencies] sqlalchemy = ["SQLAlchemy>=1.4.36"] -tortoise = ["tortoise-orm>=0.20.0", "asyncpg>=0.28.0"] -peewee = ["peewee>=3.17.0"] +tortoise = ["tortoise-orm>=0.20.0,<1.0", "asyncpg>=0.28.0"] +peewee = ["peewee>=3.17.0,<4"] test = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", @@ -60,7 +61,7 @@ exclude = ["tests*"] [tool.black] line-length = 100 -target-version = ["py39", "py310", "py311", "py312", "py313"] +target-version = ["py39", "py310", "py311", "py312", "py313", "py314"] [tool.ruff] line-length = 100 diff --git a/tests/test_adapter_methods_coverage.py b/tests/test_adapter_methods_coverage.py index 63e27ae..25ed3aa 100644 --- a/tests/test_adapter_methods_coverage.py +++ b/tests/test_adapter_methods_coverage.py @@ -186,21 +186,18 @@ async def test_tortoise_adapter_get_list_queryset_with_offset(self): """Test Tortoise adapter get_list_queryset_async with offset.""" try: from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter - from tortoise.models import Model - from tortoise import fields - + from tests.tortoise_models import SimpleTortoiseModel as TestModel + adapter = TortoiseAdapter( - database_url="sqlite:///test_tortoise_offset.db", - models=[], + database_url="sqlite://:memory:", + models=["tests.tortoise_models"], app_label="test" ) - - class TestModel(Model): - id = fields.IntField(pk=True) - name = fields.CharField(max_length=50) - - # Initialize + + # Initialize and create the table + from tortoise import Tortoise await adapter._ensure_initialized() + await Tortoise.generate_schemas(safe=True) # Create test data await TestModel.create(name="test1") @@ -228,20 +225,14 @@ async def test_tortoise_adapter_create_element_validation(self): """Test Tortoise adapter create_element_async validation.""" try: from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter - from tortoise.models import Model - from tortoise import fields - + from tests.tortoise_models import SimpleTortoiseModel as TestModel + adapter = TortoiseAdapter( - database_url="sqlite:///test_tortoise_validation.db", - models=[], + database_url="sqlite://:memory:", + models=["tests.tortoise_models"], app_label="test" ) - - class TestModel(Model): - id = fields.IntField(pk=True) - name = fields.CharField(max_length=50, required=True) - value = fields.IntField(null=True) - + await adapter._ensure_initialized() # Test missing required field @@ -266,21 +257,17 @@ async def test_tortoise_adapter_update_element_put(self): """Test Tortoise adapter update_element_async with PUT.""" try: from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter - from tortoise.models import Model - from tortoise import fields - + from tests.tortoise_models import SimpleTortoiseModel as TestModel + adapter = TortoiseAdapter( - database_url="sqlite:///test_tortoise_put.db", - models=[], + database_url="sqlite://:memory:", + models=["tests.tortoise_models"], app_label="test" ) - - class TestModel(Model): - id = fields.IntField(pk=True) - name = fields.CharField(max_length=50) - value = fields.IntField(null=True) - + + from tortoise import Tortoise await adapter._ensure_initialized() + await Tortoise.generate_schemas(safe=True) # Create test data obj = await TestModel.create(name="old", value=10) diff --git a/tests/test_exception_handling.py b/tests/test_exception_handling.py index 4e20ec5..0a123f2 100644 --- a/tests/test_exception_handling.py +++ b/tests/test_exception_handling.py @@ -280,21 +280,17 @@ async def test_tortoise_adapter_create_element_integrity_error(self): """Test Tortoise adapter create_element_async handles IntegrityError.""" try: from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter - from tortoise.models import Model - from tortoise import fields - from tortoise.exceptions import IntegrityError as TortoiseIntegrityError + from tests.tortoise_models import UniqueNameTortoiseModel as TestModel adapter = TortoiseAdapter( - database_url="sqlite:///test_tortoise_integrity.db", - models=[], + database_url="sqlite://:memory:", + models=["tests.tortoise_models"], app_label="test" ) - class TestModel(Model): - id = fields.IntField(pk=True) - name = fields.CharField(max_length=50, unique=True) - + from tortoise import Tortoise await adapter._ensure_initialized() + await Tortoise.generate_schemas(safe=True) # Create first record await TestModel.create(name="test1") @@ -322,19 +318,14 @@ async def test_tortoise_adapter_create_element_generic_error(self): """Test Tortoise adapter create_element_async handles generic Exception.""" try: from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter - from tortoise.models import Model - from tortoise import fields + from tests.tortoise_models import SimpleTortoiseModel as TestModel adapter = TortoiseAdapter( - database_url="sqlite:///test_tortoise_generic.db", - models=[], + database_url="sqlite://:memory:", + models=["tests.tortoise_models"], app_label="test" ) - class TestModel(Model): - id = fields.IntField(pk=True) - name = fields.CharField(max_length=50) - await adapter._ensure_initialized() # Mock create to raise generic exception @@ -361,20 +352,17 @@ async def test_tortoise_adapter_update_element_empty_data(self): """Test Tortoise adapter update_element_async with empty data.""" try: from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter - from tortoise.models import Model - from tortoise import fields + from tests.tortoise_models import SimpleTortoiseModel as TestModel adapter = TortoiseAdapter( - database_url="sqlite:///test_tortoise_empty.db", - models=[], + database_url="sqlite://:memory:", + models=["tests.tortoise_models"], app_label="test" ) - class TestModel(Model): - id = fields.IntField(pk=True) - name = fields.CharField(max_length=50) - + from tortoise import Tortoise await adapter._ensure_initialized() + await Tortoise.generate_schemas(safe=True) # Create test data obj = await TestModel.create(name="test") diff --git a/tests/test_missing_coverage.py b/tests/test_missing_coverage.py index 44f8ff3..6c59f2a 100644 --- a/tests/test_missing_coverage.py +++ b/tests/test_missing_coverage.py @@ -316,7 +316,8 @@ class TestModel(Base): assert result is True # Verify it's deleted - assert TestModel.get_by_id(obj_id) is None + with pytest.raises(TestModel.DoesNotExist): + TestModel.get_by_id(obj_id) # Test with non-existent id with pytest.raises(HTTPException) as exc_info: diff --git a/tests/tortoise_models.py b/tests/tortoise_models.py new file mode 100644 index 0000000..c6f1e7f --- /dev/null +++ b/tests/tortoise_models.py @@ -0,0 +1,28 @@ +"""Shared Tortoise ORM models for tests. + +These models live at module level (as required by Tortoise) and are +referenced by tests via ``models=["tests.tortoise_models"]`` so that +``Tortoise.init()`` can discover and register them properly. + +The module is only imported from tests that already know tortoise-orm +is installed; importing it without tortoise-orm raises ``ImportError``, +which those tests translate into a skip. +""" + +from tortoise import fields +from tortoise.models import Model + + +class SimpleTortoiseModel(Model): + """Model with a required ``name`` and an optional ``value``.""" + + id = fields.IntField(pk=True) + name = fields.CharField(max_length=50) + value = fields.IntField(null=True) + + +class UniqueNameTortoiseModel(Model): + """Model with a unique constraint on ``name`` (for IntegrityError tests).""" + + id = fields.IntField(pk=True) + name = fields.CharField(max_length=50, unique=True)