From f66a81dc71b1e9c44ced7441a7e22634a199fd74 Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Tue, 14 Apr 2026 18:39:24 +0200 Subject: [PATCH 1/5] Update testing dependencies --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 41bdb86..a73bff5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ where = src # changes how error messages look in a minor version update. testing = pytest ~= 9.0.3 - pytest-cov ~= 7.0.0 + pytest-cov ~= 7.1.0 coverage ~= 7.13.5 flake8 ~= 7.3.0 - mypy ~= 1.19.1 + mypy ~= 1.20.1 From b66170083facdcc6bf10c698caec725f56b31569 Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Tue, 14 Apr 2026 19:11:28 +0200 Subject: [PATCH 2/5] Modernize tox config; fix SQLAlchemy 1.4 testing --- .coveragerc | 3 --- Makefile | 5 +++++ constraints.sqlalchemy1.4.txt | 1 - constraints.sqlalchemy2.0.txt | 1 - pyproject.toml | 2 +- setup.cfg | 2 ++ tox.ini | 28 +++++++++++++++++----------- 7 files changed, 25 insertions(+), 17 deletions(-) delete mode 100644 constraints.sqlalchemy1.4.txt delete mode 100644 constraints.sqlalchemy2.0.txt diff --git a/.coveragerc b/.coveragerc index 72faee7..0683905 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,6 +20,3 @@ exclude_also = directory = reports/coverage_html/ skip_covered = False show_contexts = True - -[xml] -output = reports/coverage.xml diff --git a/Makefile b/Makefile index f449ac6..bebe5f4 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,11 @@ build: tox: tox run +# Run tox suite with the latest installed Python version and SQLAlchemy 2.0 +.PHONY: tox-latest +tox-latest: + tox run -e clean,py-sqlalchemy2.0,report,flake8,mypy + # Run tox in venv (needs to be installed with `make venv` first) .PHONY: venv-tox venv-tox: diff --git a/constraints.sqlalchemy1.4.txt b/constraints.sqlalchemy1.4.txt deleted file mode 100644 index cfd4f95..0000000 --- a/constraints.sqlalchemy1.4.txt +++ /dev/null @@ -1 +0,0 @@ -SQLAlchemy~=1.4 diff --git a/constraints.sqlalchemy2.0.txt b/constraints.sqlalchemy2.0.txt deleted file mode 100644 index a5ba6db..0000000 --- a/constraints.sqlalchemy2.0.txt +++ /dev/null @@ -1 +0,0 @@ -SQLAlchemy~=2.0 diff --git a/pyproject.toml b/pyproject.toml index b67f19c..366ac41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/validataclass_search_queries/_version.py" -version_scheme = "post-release" +version_scheme = "no-guess-dev" [tool.mypy] files = ["src/", "tests/"] diff --git a/setup.cfg b/setup.cfg index a73bff5..1779dbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,8 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 + Programming Language :: Python :: 3.14 Programming Language :: Python :: Implementation :: CPython Topic :: Software Development :: Libraries :: Python Modules Topic :: Utilities diff --git a/tox.ini b/tox.ini index e31e24c..9fcec55 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -minversion = 4.5.1 -envlist = clean,py{310,311,312,313,314}-sqlalchemy{1.4,2.0},report,flake8,mypy +requires = tox >= 4.32 +envlist = clean, py3{10-14}-sqlalchemy{1.4,2.0}, report, flake8, mypy skip_missing_interpreters = true [flake8] @@ -9,13 +9,26 @@ exclude = _version.py ignore = [testenv] +# Build and install a wheel (speeds up tox runs) +package = wheel +wheel_build_env = .pkg + +# Install all testing requirements by default(pytest, mypy etc.) extras = testing + +# Default command: Run pytest commands = python -m pytest --cov --cov-append {posargs} +# Conditionally install SQLAlchemy version depending on the sqlalchemy factor +constrain_package_deps = true +deps = + sqlalchemy1.4: sqlalchemy >= 1.4, < 2.0 + sqlalchemy2.0: sqlalchemy >= 2.0, < 2.1 + [testenv:flake8] commands = flake8 src/ tests/ -[testenv:mypy,py{310,311,312,313,314}-mypy] +[testenv:mypy, py3{10-14}-mypy] commands = mypy {posargs} [testenv:mypy-debug] @@ -25,16 +38,9 @@ commands = mypy --show-traceback --no-incremental {posargs} [testenv:clean] commands = coverage erase -[testenv:report,py{310,311,312,313,314}-report] +[testenv:report, py3{10-14}-report] commands = coverage html - coverage xml # TODO: As soon as we've reached 100% test coverage, enforce this test coverage. # coverage report --fail-under=100 coverage report - -[testenv:sqlalchemy1.4] -set_env = PIP_CONSTRAINT=constraints.sqlalchemy1.4.txt - -[testenv:sqlalchemy2.0] -set_env = PIP_CONSTRAINT=constraints.sqlalchemy2.0.txt From 95fe3d095c23ed3d5d6f17583176bfd258afe235 Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Tue, 14 Apr 2026 20:10:36 +0200 Subject: [PATCH 3/5] Fix type annotations for SQLAlchemy 1.4 compatibility --- .../pagination/abstract_pagination_mixin.py | 2 +- .../pagination/cursor_pagination_mixin.py | 2 +- .../pagination/offset_pagination_mixin.py | 2 +- .../repositories/search_query_repository_mixin.py | 10 +++++----- .../sorting/abstract_sorting_mixin.py | 2 +- .../sorting/sorting_mixin.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/validataclass_search_queries/pagination/abstract_pagination_mixin.py b/src/validataclass_search_queries/pagination/abstract_pagination_mixin.py index 3d0d9ce..a131304 100644 --- a/src/validataclass_search_queries/pagination/abstract_pagination_mixin.py +++ b/src/validataclass_search_queries/pagination/abstract_pagination_mixin.py @@ -27,7 +27,7 @@ class AbstractPaginationMixin(ABC): limit: int | None @abstractmethod - def apply_pagination_to_query(self, query: Query[T], model_cls: Any) -> Query[T]: + def apply_pagination_to_query(self, query: 'Query[T]', model_cls: Any) -> 'Query[T]': """ Applies the pagination parameters to an SQLAlchemy query and returns the new query. diff --git a/src/validataclass_search_queries/pagination/cursor_pagination_mixin.py b/src/validataclass_search_queries/pagination/cursor_pagination_mixin.py index 3846e78..d3b7dc8 100644 --- a/src/validataclass_search_queries/pagination/cursor_pagination_mixin.py +++ b/src/validataclass_search_queries/pagination/cursor_pagination_mixin.py @@ -134,7 +134,7 @@ def get_cursor_column(self, model_cls: Any) -> ColumnElement[Any]: return cast(ColumnElement[Any], getattr(model_cls, self.get_cursor_column_name())) @override - def apply_pagination_to_query(self, query: Query[T], model_cls: Any) -> Query[T]: + def apply_pagination_to_query(self, query: 'Query[T]', model_cls: Any) -> 'Query[T]': """ Applies the pagination parameters to an SQLAlchemy query and returns the new query. diff --git a/src/validataclass_search_queries/pagination/offset_pagination_mixin.py b/src/validataclass_search_queries/pagination/offset_pagination_mixin.py index 258f24d..3e2d52f 100644 --- a/src/validataclass_search_queries/pagination/offset_pagination_mixin.py +++ b/src/validataclass_search_queries/pagination/offset_pagination_mixin.py @@ -99,7 +99,7 @@ def __init_subclass__(cls, **kwargs: Any): super().__init_subclass__(**kwargs) @override - def apply_pagination_to_query(self, query: Query[T], model_cls: Any) -> Query[T]: + def apply_pagination_to_query(self, query: 'Query[T]', model_cls: Any) -> 'Query[T]': """ Applies the pagination parameters to an SQLAlchemy query and returns the new query. diff --git a/src/validataclass_search_queries/repositories/search_query_repository_mixin.py b/src/validataclass_search_queries/repositories/search_query_repository_mixin.py index 261f2fb..997720b 100644 --- a/src/validataclass_search_queries/repositories/search_query_repository_mixin.py +++ b/src/validataclass_search_queries/repositories/search_query_repository_mixin.py @@ -147,7 +147,7 @@ def model_cls(self) -> type[T_Model]: """ raise NotImplementedError - def _search_and_paginate(self, query: Query[Any], search_query: BaseSearchQuery | None) -> PaginatedResult[T_Model]: + def _search_and_paginate(self, query: 'Query[Any]', search_query: BaseSearchQuery | None) -> PaginatedResult[T_Model]: """ Apply filters, sorting and pagination to a database query, based on search parameters (usually parsed from HTTP query parameters), then execute the query and return a paginated list of results. @@ -158,7 +158,7 @@ def _search_and_paginate(self, query: Query[Any], search_query: BaseSearchQuery query = self._order_by_search_query(query, search_query) return self._paginate_result(query, search_query) - def _filter_by_search_query(self, query: Query[T_Query], search_query: BaseSearchQuery | None) -> Query[T_Query]: + def _filter_by_search_query(self, query: 'Query[T_Query]', search_query: BaseSearchQuery | None) -> 'Query[T_Query]': """ Apply filters to a database query, based on search parameters (usually parsed from HTTP query parameters). This does not include sorting or pagination! @@ -174,7 +174,7 @@ def _filter_by_search_query(self, query: Query[T_Query], search_query: BaseSearc return query - def _apply_bound_search_filter(self, query: Query[T_Query], bound_filter: BoundSearchFilter) -> Query[T_Query]: + def _apply_bound_search_filter(self, query: 'Query[T_Query]', bound_filter: BoundSearchFilter) -> 'Query[T_Query]': """ Apply a single search filter from a `BoundSearchFilter` to a database query with `query.filter(...)`. Called by `_filter_by_search_query()` for every set search filter. @@ -185,7 +185,7 @@ def _apply_bound_search_filter(self, query: Query[T_Query], bound_filter: BoundS col = getattr(self.model_cls, bound_filter.column_name) return query.filter(bound_filter.get_sqlalchemy_filter(col)) - def _order_by_search_query(self, query: Query[T_Query], search_query: BaseSearchQuery | None) -> Query[T_Query]: + def _order_by_search_query(self, query: 'Query[T_Query]', search_query: BaseSearchQuery | None) -> 'Query[T_Query]': """ Apply sorting (`query.order_by(...)`) to a database query based on sorting parameters from a search query. @@ -197,7 +197,7 @@ def _order_by_search_query(self, query: Query[T_Query], search_query: BaseSearch return query - def _paginate_result(self, query: Query[Any], search_query: BaseSearchQuery | None) -> PaginatedResult[T_Model]: + def _paginate_result(self, query: 'Query[Any]', search_query: BaseSearchQuery | None) -> PaginatedResult[T_Model]: """ Apply pagination to a database query based on search parameters, execute the query and return a paginated list of results. diff --git a/src/validataclass_search_queries/sorting/abstract_sorting_mixin.py b/src/validataclass_search_queries/sorting/abstract_sorting_mixin.py index 5b35be7..bce6ece 100644 --- a/src/validataclass_search_queries/sorting/abstract_sorting_mixin.py +++ b/src/validataclass_search_queries/sorting/abstract_sorting_mixin.py @@ -48,7 +48,7 @@ def apply_sorting_direction(self, column: ColumnElement[T]) -> ColumnElement[T]: raise NotImplementedError @abstractmethod - def apply_sorting_to_query(self, query: Query[T], model_cls: Any) -> Query[T]: + def apply_sorting_to_query(self, query: 'Query[T]', model_cls: Any) -> 'Query[T]': """ Applies the sorting parameters to an SQLAlchemy query (`query.order_by()`) and returns the new query. diff --git a/src/validataclass_search_queries/sorting/sorting_mixin.py b/src/validataclass_search_queries/sorting/sorting_mixin.py index 2743e60..556735b 100644 --- a/src/validataclass_search_queries/sorting/sorting_mixin.py +++ b/src/validataclass_search_queries/sorting/sorting_mixin.py @@ -81,7 +81,7 @@ def apply_sorting_direction(self, column: ColumnElement[T]) -> ColumnElement[T]: return column.desc() if self.sorting_direction is SortingDirection.DESC else column.asc() @override - def apply_sorting_to_query(self, query: Query[T], model_cls: Any) -> Query[T]: + def apply_sorting_to_query(self, query: 'Query[T]', model_cls: Any) -> 'Query[T]': """ Applies the sorting parameters to an SQLAlchemy query (`query.order_by()`) and returns the new query. From 14d7c2009fba1739e5285a9532fa6a7f837351c2 Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Tue, 14 Apr 2026 20:13:50 +0200 Subject: [PATCH 4/5] GitHub Actions: Update actions to latest versions; fix release workflow --- .github/workflows/release.yml | 20 ++++++++++---------- .github/workflows/tests.yml | 15 ++++++--------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20921f9..bd2d13d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,13 +13,13 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # We use Python 3.10 here because it's the minimum Python version supported by this library. - name: Setup Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: 3.10 + python-version: '3.10' - name: Install dependencies run: pip install --upgrade pip build @@ -28,7 +28,7 @@ jobs: run: python -m build - name: Upload build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: dist_packages path: dist/ @@ -40,15 +40,15 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: 3.10 + python-version: '3.10' - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist_packages path: dist/ @@ -66,7 +66,7 @@ jobs: steps: - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist_packages path: dist/ @@ -78,7 +78,7 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@v1.14.0 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cdf2900..2ffeb1f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,16 +2,13 @@ name: Unit tests -# TODO: Remove dev-mypy after merging it into main. on: push: branches: - main - - dev-mypy pull_request: branches: - main - - dev-mypy jobs: test: @@ -32,10 +29,10 @@ jobs: - '2.0' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -47,7 +44,7 @@ jobs: run: tox run -e clean,py-sqlalchemy${{ matrix.sqlalchemy-version }},report,flake8,mypy -- --junit-xml=reports/pytest_${{ matrix.python-version }}_sqlalchemy${{ matrix.sqlalchemy-version }}.xml - name: Upload test result artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: success() || failure() with: name: pytest-results-${{ matrix.python-version }}-sqlalchemy${{ matrix.sqlalchemy-version }} @@ -64,17 +61,17 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Download test result artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: reports/ pattern: pytest-results-* merge-multiple: true - name: Publish unit test reports - uses: dorny/test-reporter@v2.1.1 + uses: dorny/test-reporter@v3.0.0 if: success() || failure() with: name: Pytest Report From 555887147a687e317fdc85bde43c7c9624da8b9f Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Tue, 14 Apr 2026 20:16:48 +0200 Subject: [PATCH 5/5] Reformat: Split too long lines --- .../search_query_repository_mixin.py | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/validataclass_search_queries/repositories/search_query_repository_mixin.py b/src/validataclass_search_queries/repositories/search_query_repository_mixin.py index 997720b..a96a0b5 100644 --- a/src/validataclass_search_queries/repositories/search_query_repository_mixin.py +++ b/src/validataclass_search_queries/repositories/search_query_repository_mixin.py @@ -147,7 +147,11 @@ def model_cls(self) -> type[T_Model]: """ raise NotImplementedError - def _search_and_paginate(self, query: 'Query[Any]', search_query: BaseSearchQuery | None) -> PaginatedResult[T_Model]: + def _search_and_paginate( + self, + query: 'Query[Any]', + search_query: BaseSearchQuery | None, + ) -> PaginatedResult[T_Model]: """ Apply filters, sorting and pagination to a database query, based on search parameters (usually parsed from HTTP query parameters), then execute the query and return a paginated list of results. @@ -158,7 +162,11 @@ def _search_and_paginate(self, query: 'Query[Any]', search_query: BaseSearchQuer query = self._order_by_search_query(query, search_query) return self._paginate_result(query, search_query) - def _filter_by_search_query(self, query: 'Query[T_Query]', search_query: BaseSearchQuery | None) -> 'Query[T_Query]': + def _filter_by_search_query( + self, + query: 'Query[T_Query]', + search_query: BaseSearchQuery | None, + ) -> 'Query[T_Query]': """ Apply filters to a database query, based on search parameters (usually parsed from HTTP query parameters). This does not include sorting or pagination! @@ -174,7 +182,11 @@ def _filter_by_search_query(self, query: 'Query[T_Query]', search_query: BaseSea return query - def _apply_bound_search_filter(self, query: 'Query[T_Query]', bound_filter: BoundSearchFilter) -> 'Query[T_Query]': + def _apply_bound_search_filter( + self, + query: 'Query[T_Query]', + bound_filter: BoundSearchFilter, + ) -> 'Query[T_Query]': """ Apply a single search filter from a `BoundSearchFilter` to a database query with `query.filter(...)`. Called by `_filter_by_search_query()` for every set search filter. @@ -185,7 +197,11 @@ def _apply_bound_search_filter(self, query: 'Query[T_Query]', bound_filter: Boun col = getattr(self.model_cls, bound_filter.column_name) return query.filter(bound_filter.get_sqlalchemy_filter(col)) - def _order_by_search_query(self, query: 'Query[T_Query]', search_query: BaseSearchQuery | None) -> 'Query[T_Query]': + def _order_by_search_query( + self, + query: 'Query[T_Query]', + search_query: BaseSearchQuery | None, + ) -> 'Query[T_Query]': """ Apply sorting (`query.order_by(...)`) to a database query based on sorting parameters from a search query. @@ -197,7 +213,11 @@ def _order_by_search_query(self, query: 'Query[T_Query]', search_query: BaseSear return query - def _paginate_result(self, query: 'Query[Any]', search_query: BaseSearchQuery | None) -> PaginatedResult[T_Model]: + def _paginate_result( + self, + query: 'Query[Any]', + search_query: BaseSearchQuery | None, + ) -> PaginatedResult[T_Model]: """ Apply pagination to a database query based on search parameters, execute the query and return a paginated list of results.