From 5bb368d75cfba7c4555acb2a89b94cb416ec082a Mon Sep 17 00:00:00 2001 From: Justin Krinke Date: Mon, 27 Jul 2026 15:09:35 -0400 Subject: [PATCH 1/3] Backport Django 5.0+ choices compatibility from upstream 24.3 Django 5.0 replaced ChoiceField._get_choices/_set_choices with a plain property, so the ChoiceIteratorMixin override raised AttributeError on any filter with choices. Port of upstream django-filter 23.5/24.3 fix: ChoiceIterator subclasses BaseChoiceIterator and normalizes choices on 5.0+, and the mixin overrides the choices property instead of the removed private pair. Pre-5.0 branches are kept, matching upstream. Co-Authored-By: Claude Fable 5 --- django_filters/fields.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/django_filters/fields.py b/django_filters/fields.py index 6293fb3a6..d6ac79cb4 100644 --- a/django_filters/fields.py +++ b/django_filters/fields.py @@ -17,6 +17,13 @@ RangeWidget, ) +try: + from django.utils.choices import BaseChoiceIterator, normalize_choices +except ImportError: + DJANGO_50 = False +else: + DJANGO_50 = True + class RangeField(forms.MultiValueField): widget = RangeWidget @@ -210,7 +217,7 @@ def clean(self, value): return value -class ChoiceIterator: +class ChoiceIterator(BaseChoiceIterator if DJANGO_50 else object): # Emulates the behavior of ModelChoiceIterator, but instead wraps # the field's _choices iterable. @@ -223,7 +230,10 @@ def __iter__(self): yield ("", self.field.empty_label) if self.field.null_label is not None: yield (self.field.null_value, self.field.null_label) - yield from self.choices + if DJANGO_50: + yield from normalize_choices(self.choices) + else: + yield from self.choices def __len__(self): add = 1 if self.field.empty_label is not None else 0 @@ -257,16 +267,21 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def _get_choices(self): - return super()._get_choices() - - def _set_choices(self, value): - super()._set_choices(value) - value = self.iterator(self, self._choices) - - self._choices = self.widget.choices = value - - choices = property(_get_choices, _set_choices) + @property + def choices(self): + return super().choices + + @choices.setter + def choices(self, value): + if DJANGO_50: + value = self.iterator(self, value) + # Simple `super()` syntax for calling a parent property setter is + # unsupported. See https://github.com/python/cpython/issues/59170 + super(ChoiceIteratorMixin, self.__class__).choices.__set__(self, value) + else: + super()._set_choices(value) + value = self.iterator(self, self._choices) + self._choices = self.widget.choices = value # Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label From ea41acb220d45193f9eaecac809ddb59d1d596e2 Mon Sep 17 00:00:00 2001 From: Justin Krinke Date: Thu, 30 Jul 2026 16:35:55 -0400 Subject: [PATCH 2/3] Modernize CI matrix and fix test suite for Django 3.2/4.2/5.2 Drop Python 3.7 and EOL Django 4.0/4.1 from the matrix; test Python 3.8-3.13 against the LTS lines (3.2, 4.2, 5.2). Replace the dead tox-py plugin with tox 4 factor selection, upgrade deprecated GitHub actions, remove the codecov v1 publish step, and fix the push trigger to match this repo's master branch. Pin DRF 3.14 for Django 3.2 envs since DRF does not constrain Django in its metadata. Test fixes: - Rename assertQuerysetEqual to assertQuerySetEqual (removed in Django 5.1), with an alias in tests/__init__.py for Django < 4.2. - Update range filter/widget tests to the _0/_1 suffixes this fork intentionally restored in 3610e9b; the tests still assumed upstream's _min/_max/_after/_before and had been failing on every version. 508 tests pass on py3.8/dj3.2, py3.11/dj4.2, and py3.13/dj5.2. Co-Authored-By: Claude Fable 5 --- .github/workflows/tests.yml | 44 +++--- setup.py | 9 +- tests/__init__.py | 9 ++ tests/test_filtering.py | 268 ++++++++++++++++++------------------ tests/test_widgets.py | 12 +- tox.ini | 24 ++-- 6 files changed, 180 insertions(+), 186 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58f3417aa..7c8a8f495 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,7 @@ name: Tests on: push: branches: - - main + - master pull_request: jobs: @@ -13,72 +13,62 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Ensure latest setuptools - run: | - python -m pip install --upgrade pip setuptools - name: Install dependencies run: | - python -m pip install coverage tox tox-py unittest-xml-reporting + python -m pip install --upgrade pip + python -m pip install coverage tox unittest-xml-reporting - name: Run tox run: | python -m pip --version python -m tox --version - python -m tox --py current + python -m tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) - name: Coverage reporting run: | coverage combine coverage report -m - coverage xml - coverage html - - name: Publish coverage results - uses: codecov/codecov-action@v1 isort: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Ensure latest setuptools - run: | - python -m pip install --upgrade pip setuptools + python-version: "3.12" - name: Install dependencies run: | + python -m pip install --upgrade pip python -m pip install tox - name: Run tox run: | python -m pip --version python -m tox --version - python -m tox -e isort,lint,docs + python -m tox run -e isort,lint,docs warnings: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Ensure latest setuptools - run: | - python -m pip install --upgrade pip setuptools + python-version: "3.13" - name: Install dependencies run: | + python -m pip install --upgrade pip python -m pip install tox - name: Run tox run: | python -m pip --version python -m tox --version - python -m tox -e warnings + python -m tox run -e warnings diff --git a/setup.py b/setup.py index e1d3ea0da..4314a936d 100644 --- a/setup.py +++ b/setup.py @@ -56,18 +56,19 @@ "Operating System :: OS Independent", "Framework :: Django", "Framework :: Django :: 3.2", - "Framework :: Django :: 4.0", - "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.2", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], zip_safe=False, - python_requires=">=3.7", + python_requires=">=3.8", install_requires=[ "Django>=3.2", ], diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb..c91711a51 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,9 @@ +import django.test + +# Django 4.2 renamed assertQuerysetEqual to assertQuerySetEqual, and Django +# 5.1 removed the old alias. Tests use the new name; alias it on Django < 4.2 +# so the full support matrix (3.2/4.2/5.2) runs the same test code. +if not hasattr(django.test.TransactionTestCase, "assertQuerySetEqual"): + django.test.TransactionTestCase.assertQuerySetEqual = ( + django.test.TransactionTestCase.assertQuerysetEqual + ) diff --git a/tests/test_filtering.py b/tests/test_filtering.py index 8b8f010a3..28ac413e9 100644 --- a/tests/test_filtering.py +++ b/tests/test_filtering.py @@ -63,11 +63,11 @@ class Meta: qs = Book.objects.all() f = F(queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, [b1.pk, b2.pk, b3.pk], lambda o: o.pk, ordered=False ) f = F({"title": "Snowcrash"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [b3.pk], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [b3.pk], lambda o: o.pk) class IntegerFilterTest(TestCase): @@ -87,13 +87,13 @@ class Meta: qs = BankAccount.objects.all() f = F(queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, [b1.pk, b2.pk, b3.pk], lambda o: o.pk, ordered=False ) f = F({"amount_saved": "10"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [b3.pk], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [b3.pk], lambda o: o.pk) f = F({"amount_saved": "0"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [b1.pk], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [b1.pk], lambda o: o.pk) class BooleanFilterTests(TestCase): @@ -111,13 +111,13 @@ class Meta: # '2' and '3' are how the field expects the data from the browser f = F({"is_active": "2"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["jacob"], lambda o: o.username, False) + self.assertQuerySetEqual(f.qs, ["jacob"], lambda o: o.username, False) f = F({"is_active": "3"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex", "aaron"], lambda o: o.username, False) + self.assertQuerySetEqual(f.qs, ["alex", "aaron"], lambda o: o.username, False) f = F({"is_active": "1"}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["alex", "aaron", "jacob"], lambda o: o.username, False ) @@ -143,17 +143,17 @@ class Meta: fields = ["status"] f = F() - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False ) f = F({"status": "1"}) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username, False) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username, False) f = F({"status": "2"}) - self.assertQuerysetEqual(f.qs, ["jacob", "aaron"], lambda o: o.username, False) + self.assertQuerySetEqual(f.qs, ["jacob", "aaron"], lambda o: o.username, False) f = F({"status": "0"}) - self.assertQuerysetEqual(f.qs, ["carl"], lambda o: o.username, False) + self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username, False) def test_filtering_on_explicitly_defined_field(self): """ @@ -170,17 +170,17 @@ class Meta: fields = ["status"] f = F() - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False ) f = F({"status": "1"}) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username, False) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username, False) f = F({"status": "2"}) - self.assertQuerysetEqual(f.qs, ["jacob", "aaron"], lambda o: o.username, False) + self.assertQuerySetEqual(f.qs, ["jacob", "aaron"], lambda o: o.username, False) f = F({"status": "0"}) - self.assertQuerysetEqual(f.qs, ["carl"], lambda o: o.username, False) + self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username, False) def test_filtering_on_empty_choice(self): class F(FilterSet): @@ -189,7 +189,7 @@ class Meta: fields = ["status"] f = F({"status": ""}) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False ) @@ -209,10 +209,10 @@ class Meta: # sanity check to make sure the filter is setup correctly f = F({"author": "1"}) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: str(o.author), False) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: str(o.author), False) f = F({"author": "null"}) - self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False) + self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) class MultipleChoiceFilterTests(TestCase): @@ -231,18 +231,18 @@ class Meta: qs = User.objects.all().order_by("username") f = F(queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["aaron", "jacob", "alex", "carl"], lambda o: o.username, False ) f = F({"status": ["0"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["carl"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username) f = F({"status": ["0", "1"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex", "carl"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex", "carl"], lambda o: o.username) f = F({"status": ["0", "1", "2"]}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["aaron", "alex", "carl", "jacob"], lambda o: o.username ) @@ -273,13 +273,13 @@ class Meta: # sanity check to make sure the filter is setup correctly f = F({"author": ["1"]}) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: str(o.author), False) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: str(o.author), False) f = F({"author": ["null"]}) - self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False) + self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) f = F({"author": ["1", "null"]}) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["alex", None], lambda o: o.author and str(o.author), False ) @@ -302,18 +302,18 @@ class Meta: qs = User.objects.all().order_by("username") f = F(queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["aa", "ja", "al", "ca"], lambda o: o.username[0:2], False ) f = F({"status": ["0"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["ca"], lambda o: o.username[0:2]) + self.assertQuerySetEqual(f.qs, ["ca"], lambda o: o.username[0:2]) f = F({"status": ["0", "1"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["al", "ca"], lambda o: o.username[0:2]) + self.assertQuerySetEqual(f.qs, ["al", "ca"], lambda o: o.username[0:2]) f = F({"status": ["0", "1", "2"]}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["aa", "al", "ca", "ja"], lambda o: o.username[0:2] ) @@ -337,7 +337,7 @@ class Meta: f = F({"date": check_date}, queryset=Comment.objects.all()) self.assertEqual(len(f.qs), 2) - self.assertQuerysetEqual(f.qs, [2, 4], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk, False) class TimeFilterTests(TestCase): @@ -360,7 +360,7 @@ class Meta: f = F({"time": check_time}, queryset=Comment.objects.all()) self.assertEqual(len(f.qs), 2) - self.assertQuerysetEqual(f.qs, [2, 4], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk, False) class DateTimeFilterTests(TestCase): @@ -386,14 +386,14 @@ class Meta: qs = Article.objects.all() f = F({"published": ten_min_ago}, queryset=qs) self.assertEqual(len(f.qs), 1) - self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) # this is how it would come through a browser f = F({"published": check_dt}, queryset=qs) self.assertEqual( len(f.qs), 1, "%s isn't matching %s when cleaned" % (check_dt, ten_min_ago) ) - self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) class DurationFilterTests(TestCase): @@ -440,19 +440,19 @@ class Meta: # Django style: 3 days, 10 hours, 22 minutes. f = F({"duration": "3 10:22:00"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1], lambda x: x) + self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # ISO 8601: 3 days, 10 hours, 22 minutes. f = F({"duration": "P3DT10H22M"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1], lambda x: x) + self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # Django style: 82 hours, 22 minutes. f = F({"duration": "82:22:00"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1], lambda x: x) + self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # ISO 8601: 82 hours, 22 minutes. f = F({"duration": "PT82H22M"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1], lambda x: x) + self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) def test_filtering_with_single_lookup_expr_dictionary(self): class F(FilterSet): @@ -463,18 +463,18 @@ class Meta: qs = SpacewalkRecord.objects.order_by("-duration") f = F({"duration__gt": "PT58H30M"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1, self.r2, self.r3], lambda x: x) + self.assertQuerySetEqual(f.qs, [self.r1, self.r2, self.r3], lambda x: x) f = F({"duration__gte": "PT58H30M"}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, [self.r1, self.r2, self.r3, self.r4], lambda x: x ) f = F({"duration__lt": "PT58H30M"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r5], lambda x: x) + self.assertQuerySetEqual(f.qs, [self.r5], lambda x: x) f = F({"duration__lte": "PT58H30M"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r4, self.r5], lambda x: x) + self.assertQuerySetEqual(f.qs, [self.r4, self.r5], lambda x: x) def test_filtering_with_multiple_lookup_exprs(self): class F(FilterSet): @@ -488,7 +488,7 @@ class Meta: qs = SpacewalkRecord.objects.order_by("duration") f = F({"min_duration": "PT55H", "max_duration": "PT60H"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r4, self.r3], lambda x: x) + self.assertQuerySetEqual(f.qs, [self.r4, self.r3], lambda x: x) class ModelChoiceFilterTests(TestCase): @@ -508,7 +508,7 @@ class Meta: qs = Comment.objects.all() f = F({"author": jacob.pk}, queryset=qs) - self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) @override_settings(FILTERS_NULL_CHOICE_LABEL="No Author") def test_filtering_null(self): @@ -523,7 +523,7 @@ class Meta: qs = Article.objects.all() f = F({"author": "null"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False) + self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) def test_callable_queryset(self): # Sanity check for callable queryset arguments. @@ -547,11 +547,11 @@ class Meta: request.user = jacob f = F(queryset=qs, request=request).filters["author"].field - self.assertQuerysetEqual(f.queryset, [1], lambda o: o.pk, False) + self.assertQuerySetEqual(f.queryset, [1], lambda o: o.pk, False) request.user = aaron f = F(queryset=qs, request=request).filters["author"].field - self.assertQuerysetEqual(f.queryset, [1, 2], lambda o: o.pk, False) + self.assertQuerySetEqual(f.queryset, [1, 2], lambda o: o.pk, False) class ModelMultipleChoiceFilterTests(TestCase): @@ -578,16 +578,16 @@ class Meta: qs = User.objects.all().order_by("username") f = F({"favorite_books": ["1"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["1", "3"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["2"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) f = F({"favorite_books": ["4"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.username) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username) @override_settings(FILTERS_NULL_CHOICE_LABEL="No Favorites") def test_filtering_null(self): @@ -599,7 +599,7 @@ class Meta: qs = User.objects.all() f = F({"favorite_books": ["null"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["jacob"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["jacob"], lambda o: o.username) def test_filtering_dictionary(self): class F(FilterSet): @@ -609,16 +609,16 @@ class Meta: qs = User.objects.all().order_by("username") f = F({"favorite_books": ["1"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["1", "3"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["2"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) f = F({"favorite_books": ["4"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.username) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username) def test_filtering_on_all_of_subset_of_choices(self): class F(FilterSet): @@ -641,7 +641,7 @@ def __init__(self, *args, **kwargs): f = F({"favorite_books": ["1", "2"]}, queryset=qs) # The results should only include matching users - not Jacob. - self.assertQuerysetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) def test_filtering_on_non_required_fields(self): # See issue #132 - filtering with all options on a non-required @@ -690,7 +690,7 @@ class Meta: fields = ["price"] f = F({"price": 10}, queryset=Book.objects.all()) - self.assertQuerysetEqual(f.qs, ["Ender's Game"], lambda o: o.title) + self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) class RangeFilterTests(TestCase): @@ -717,31 +717,31 @@ class Meta: qs = Book.objects.all().order_by("title") f = F(queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Free Book", "Rainbow Six", "Refund", "Snowcrash"], lambda o: o.title, ) - f = F({"price_min": "5", "price_max": "15"}, queryset=qs) - self.assertQuerysetEqual( + f = F({"price_0": "5", "price_1": "15"}, queryset=qs) + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) - f = F({"price_min": "11"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["Rainbow Six", "Snowcrash"], lambda o: o.title) - f = F({"price_max": "19"}, queryset=qs) - self.assertQuerysetEqual( + f = F({"price_0": "11"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["Rainbow Six", "Snowcrash"], lambda o: o.title) + f = F({"price_1": "19"}, queryset=qs) + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Free Book", "Rainbow Six", "Refund"], lambda o: o.title, ) - f = F({"price_min": "0", "price_max": "12"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["Ender's Game", "Free Book"], lambda o: o.title) - f = F({"price_min": "-11", "price_max": "0"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["Free Book", "Refund"], lambda o: o.title) - f = F({"price_min": "0", "price_max": "0"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["Free Book"], lambda o: o.title) + f = F({"price_0": "0", "price_1": "12"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["Ender's Game", "Free Book"], lambda o: o.title) + f = F({"price_0": "-11", "price_1": "0"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["Free Book", "Refund"], lambda o: o.title) + f = F({"price_0": "0", "price_1": "0"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["Free Book"], lambda o: o.title) class DateRangeFilterTests(TestCase): @@ -777,27 +777,27 @@ def relative_to(self, today): def test_filtering_for_year(self): f = self.CommentFilter({"date": "year"}) with self.relative_to(datetime.datetime(now().year, 4, 1)): - self.assertQuerysetEqual(f.qs, [1, 3, 4, 5, 6], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3, 4, 5, 6], lambda o: o.pk, False) def test_filtering_for_month(self): f = self.CommentFilter({"date": "month"}) with self.relative_to(datetime.datetime(now().year, 4, 21)): - self.assertQuerysetEqual(f.qs, [1, 3, 4, 5], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3, 4, 5], lambda o: o.pk, False) def test_filtering_for_week(self): f = self.CommentFilter({"date": "week"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): - self.assertQuerysetEqual(f.qs, [3, 4, 5], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [3, 4, 5], lambda o: o.pk, False) def test_filtering_for_yesterday(self): f = self.CommentFilter({"date": "yesterday"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): - self.assertQuerysetEqual(f.qs, [5], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [5], lambda o: o.pk, False) def test_filtering_for_today(self): f = self.CommentFilter({"date": "today"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): - self.assertQuerysetEqual(f.qs, [4], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [4], lambda o: o.pk, False) class DateFromToRangeFilterTests(TestCase): @@ -817,7 +817,7 @@ class Meta: fields = ["date"] results = F( - data={"published_after": "2016-01-02", "published_before": "2016-01-03"} + data={"published_0": "2016-01-02", "published_1": "2016-01-03"} ) self.assertEqual(len(results.qs), 3) @@ -844,7 +844,7 @@ class Meta: fields = ["published"] results = F( - data={"published_after": "2016-01-02", "published_before": "2016-01-03"} + data={"published_0": "2016-01-02", "published_1": "2016-01-03"} ) self.assertEqual(len(results.qs), 3) @@ -873,7 +873,7 @@ class Meta: fields = ["published"] results = F( - data={"published_after": "2017-10-15", "published_before": "2017-10-15"} + data={"published_0": "2017-10-15", "published_1": "2017-10-15"} ) self.assertEqual(len(results.qs), 2) @@ -902,7 +902,7 @@ class Meta: fields = ["published"] results = F( - data={"published_after": "2017-02-18", "published_before": "2017-02-18"} + data={"published_0": "2017-02-18", "published_1": "2017-02-18"} ) self.assertEqual(len(results.qs), 2) @@ -934,7 +934,7 @@ class Meta: fields = ["published"] results = F( - data={"published_after": "2017-3-26", "published_before": "2017-3-26"} + data={"published_0": "2017-3-26", "published_1": "2017-3-26"} ) self.assertEqual(len(results.qs), 3) @@ -966,7 +966,7 @@ class Meta: fields = ["published"] results = F( - data={"published_after": "2017-10-29", "published_before": "2017-10-29"} + data={"published_0": "2017-10-29", "published_1": "2017-10-29"} ) self.assertEqual(len(results.qs), 3) @@ -996,8 +996,8 @@ class Meta: results = F( data={ - "published_after": "2016-01-02 10:00", - "published_before": "2016-01-03 19:00", + "published_0": "2016-01-02 10:00", + "published_1": "2016-01-03 19:00", } ) self.assertEqual(len(results.qs), 2) @@ -1029,8 +1029,8 @@ class Meta: dt = datetime.datetime.now(tz=tz) results = F( data={ - "published_after": "2016-01-02T10:00:00.000000" + dt.strftime("%z"), - "published_before": "2016-01-03T19:00:00.000000" + dt.strftime("%z"), + "published_0": "2016-01-02T10:00:00.000000" + dt.strftime("%z"), + "published_1": "2016-01-03T19:00:00.000000" + dt.strftime("%z"), } ) self.assertEqual(len(results.qs), 2) @@ -1052,7 +1052,7 @@ class Meta: model = Comment fields = ["time"] - results = F(data={"time_after": "8:00", "time_before": "10:00"}) + results = F(data={"time_0": "8:00", "time_1": "10:00"}) self.assertEqual(len(results.qs), 2) @@ -1177,7 +1177,7 @@ class Meta: f = F({"account": 1}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [1], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_o2o_relation_dictionary(self): class F(FilterSet): @@ -1192,7 +1192,7 @@ class Meta: f = F({"account": 1}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [1], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_reverse_o2o_relation(self): class F(FilterSet): @@ -1205,7 +1205,7 @@ class Meta: f = F({"profile": 1}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [1], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_o2o_relation_attribute(self): class F(FilterSet): @@ -1218,7 +1218,7 @@ class Meta: f = F({"account__in_good_standing": "2"}) self.assertEqual(f.qs.count(), 2) - self.assertQuerysetEqual(f.qs, [2, 3], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [2, 3], lambda o: o.pk, False) def test_o2o_relation_attribute2(self): class F(FilterSet): @@ -1234,7 +1234,7 @@ class Meta: f = F({"account__in_good_standing": "2", "account__friendly": "2"}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) def test_reverse_o2o_relation_attribute(self): class F(FilterSet): @@ -1247,7 +1247,7 @@ class Meta: f = F({"profile__likes_coffee": "2"}) self.assertEqual(f.qs.count(), 2) - self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) def test_reverse_o2o_relation_attribute2(self): class F(FilterSet): @@ -1260,7 +1260,7 @@ class Meta: f = F({"profile__likes_coffee": "2", "profile__likes_tea": "2"}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [3], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [3], lambda o: o.pk) class FKRelationshipTests(TestCase): @@ -1281,7 +1281,7 @@ class Meta: f = F({"company": 1}) self.assertEqual(f.qs.count(), 2) - self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) def test_reverse_fk_relation(self): alex = User.objects.create(username="alex") @@ -1299,7 +1299,7 @@ class Meta: qs = User.objects.all() f = F({"comments": [2]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) class F(FilterSet): comments = AllValuesFilter() @@ -1309,7 +1309,7 @@ class Meta: fields = ["comments"] f = F({"comments": 2}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) def test_fk_relation_attribute(self): now_dt = now() @@ -1355,7 +1355,7 @@ class Meta: qs = User.objects.all() f = F({"comments__text": "comment 2"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) class F(FilterSet): comments__text = AllValuesFilter() @@ -1365,7 +1365,7 @@ class Meta: fields = ["comments__text"] f = F({"comments__text": "comment 2"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) @unittest.skip("todo - need correct models") def test_fk_relation_multiple_attributes(self): @@ -1408,16 +1408,16 @@ class Meta: qs = User.objects.all().order_by("username") f = F({"favorite_books": ["1"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["1", "3"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["2"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) f = F({"favorite_books": ["4"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.username) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username) def test_reverse_m2m_relation(self): class F(FilterSet): @@ -1427,7 +1427,7 @@ class Meta: qs = Book.objects.all().order_by("title") f = F({"lovers": [1]}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) @@ -1439,7 +1439,7 @@ class Meta: fields = ["lovers"] f = F({"lovers": 1}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) @@ -1451,10 +1451,10 @@ class Meta: qs = User.objects.all().order_by("username") f = F({"favorite_books__title": "Ender's Game"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books__title": "Rainbow Six"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) class F(FilterSet): favorite_books__title = MultipleChoiceFilter() @@ -1467,7 +1467,7 @@ class Meta: self.assertEqual(len(f.filters["favorite_books__title"].field.choices), 0) # f = F({'favorite_books__title': ['1', '3']}, # queryset=qs) - # self.assertQuerysetEqual( + # self.assertQuerySetEqual( # f.qs, ['aaron', 'alex'], lambda o: o.username) class F(FilterSet): @@ -1478,7 +1478,7 @@ class Meta: fields = ["favorite_books__title"] f = F({"favorite_books__title": "Snowcrash"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["aaron"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron"], lambda o: o.username) def test_reverse_m2m_relation_attribute(self): class F(FilterSet): @@ -1488,12 +1488,12 @@ class Meta: qs = Book.objects.all().order_by("title") f = F({"lovers__username": "alex"}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) f = F({"lovers__username": "jacob"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.title) + self.assertQuerySetEqual(f.qs, [], lambda o: o.title) class F(FilterSet): lovers__username = MultipleChoiceFilter() @@ -1506,7 +1506,7 @@ class Meta: self.assertEqual(len(f.filters["lovers__username"].field.choices), 0) # f = F({'lovers__username': ['1', '3']}, # queryset=qs) - # self.assertQuerysetEqual( + # self.assertQuerySetEqual( # f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title) class F(FilterSet): @@ -1517,7 +1517,7 @@ class Meta: fields = ["lovers__username"] f = F({"lovers__username": "alex"}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) @@ -1533,13 +1533,13 @@ class Meta: {"favorite_books__price": "1.00", "favorite_books__average_rating": 4.0}, queryset=qs, ) - self.assertQuerysetEqual(f.qs, ["aaron"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron"], lambda o: o.username) f = F( {"favorite_books__price": "3.00", "favorite_books__average_rating": 4.0}, queryset=qs, ) - self.assertQuerysetEqual(f.qs, [], lambda o: o.username) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username) @unittest.expectedFailure def test_reverse_m2m_relation_multiple_attributes(self): @@ -1550,12 +1550,12 @@ class Meta: qs = Book.objects.all().order_by("title") f = F({"lovers__status": 1, "lovers__username": "alex"}, queryset=qs) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) f = F({"lovers__status": 1, "lovers__username": "jacob"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.title) + self.assertQuerySetEqual(f.qs, [], lambda o: o.title) @unittest.skip("todo") def test_fk_relation_on_m2m_relation(self): @@ -1585,7 +1585,7 @@ class Meta: qs = Node.objects.all().order_by("pk") f = F({"adjacents": ["1"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, [2, 4], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk) class NonSymmetricalSelfReferentialRelationshipTests(TestCase): @@ -1607,7 +1607,7 @@ class Meta: qs = DirectedNode.objects.all().order_by("pk") f = F({"outbound_nodes": ["1"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, [4], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [4], lambda o: o.pk) def test_reverse_relation(self): class F(FilterSet): @@ -1617,7 +1617,7 @@ class Meta: qs = DirectedNode.objects.all().order_by("pk") f = F({"inbound_nodes": ["1"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) @override_settings(TIME_ZONE="UTC") @@ -1639,7 +1639,7 @@ class Meta: qs = Article.objects.all() f = F({"published__hour__gte": 17}, queryset=qs) self.assertEqual(len(f.qs), 1) - self.assertQuerysetEqual(f.qs, [a.pk], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [a.pk], lambda o: o.pk) class LookupChoiceFilterTests(TestCase): @@ -1668,12 +1668,12 @@ def test_filtering(self): F = self.BookFilter f = F({"price": "15", "price_lookup": "lt"}) - self.assertQuerysetEqual(f.qs, ["Ender's Game"], lambda o: o.title) + self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) f = F({"price": "15", "price_lookup": "lt"}) - self.assertQuerysetEqual(f.qs, ["Ender's Game"], lambda o: o.title) + self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) f = F({"price": "", "price_lookup": "lt"}) self.assertTrue(f.is_valid()) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six", "Snowcrash"], lambda o: o.title, @@ -1681,7 +1681,7 @@ def test_filtering(self): ) f = F({"price": "15"}) self.assertFalse(f.is_valid()) - self.assertQuerysetEqual( + self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six", "Snowcrash"], lambda o: o.title, @@ -1777,7 +1777,7 @@ def test_numeric_filtering(self): for params, expected in cases: with self.subTest(params=params, expected=expected): - self.assertQuerysetEqual( + self.assertQuerySetEqual( F(params, queryset=qs).qs, expected, attrgetter("pk") ) @@ -1798,7 +1798,7 @@ def test_string_filtering(self): for params, expected in cases: with self.subTest(params=params, expected=expected): - self.assertQuerysetEqual( + self.assertQuerySetEqual( F(params, queryset=qs).qs, expected, attrgetter("pk") ) @@ -1840,7 +1840,7 @@ def test_datetime_filtering(self): for params, expected in cases: with self.subTest(params=params, expected=expected): - self.assertQuerysetEqual( + self.assertQuerySetEqual( F(params, queryset=qs).qs, expected, attrgetter("pk") ) @@ -1861,7 +1861,7 @@ def test_related_filtering(self): for params, expected in cases: with self.subTest(params=params, expected=expected): - self.assertQuerysetEqual( + self.assertQuerySetEqual( F(params, queryset=qs).qs, expected, attrgetter("pk") ) @@ -1999,7 +1999,7 @@ class F(FilterSet): username = CharFilter() f = F({"username": "alex"}, queryset=User.objects.all()) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) def test_filtering_with_multiple_filters(self): class F(FilterSet): @@ -2010,10 +2010,10 @@ class Meta: qs = User.objects.all() f = F({"username": "alex", "status": "1"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["alex"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) f = F({"username": "alex", "status": "2"}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [], lambda o: o.pk) def test_filter_with_initial(self): # Initial values are a form presentation option - the FilterSet should @@ -2029,10 +2029,10 @@ class Meta: users = ["alex", "jacob", "aaron", "carl"] f = F(queryset=qs) - self.assertQuerysetEqual(f.qs.order_by("pk"), users, lambda o: o.username) + self.assertQuerySetEqual(f.qs.order_by("pk"), users, lambda o: o.username) f = F({"status": 0}, queryset=qs) - self.assertQuerysetEqual(f.qs, ["carl"], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username) def test_qs_count(self): class F(FilterSet): diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 9e3ba3bf2..57c257624 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -264,17 +264,17 @@ def test_widget(self): self.assertHTMLEqual( w.render("price", ""), """ - + - - """, + """, ) self.assertHTMLEqual( w.render("price", slice(5.99, 9.99)), """ - + - - """, + """, ) def test_widget_attributes(self): @@ -283,9 +283,9 @@ def test_widget_attributes(self): self.assertHTMLEqual( w.render("date", ""), """ - + - - """, + """, ) diff --git a/tox.ini b/tox.ini index 7d260f478..c1721ca78 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,21 @@ [tox] envlist = - {py37,py38,py39,py310}-django32, - {py38,py39}-{django40,django41,django42}, - {py310, py311}-{django41,django42,latest}, + {py38,py39,py310}-django32, + {py38,py39,py310,py311,py312}-django42, + {py310,py311,py312,py313}-django52, isort,lint,docs,warnings, -[latest] -deps = - https://github.com/django/django/archive/main.tar.gz - https://github.com/encode/django-rest-framework/archive/master.tar.gz - [testenv] commands = coverage run --parallel-mode --source django_filters ./runtests.py --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner {posargs} setenv = PYTHONDONTWRITEBYTECODE=1 deps = django32: django~=3.2.0 - django40: Django>=4.0,<4.1 - django41: Django>=4.1,<4.2 - django42: Django>=4.2a1,<5.0 - !latest: djangorestframework - latest: {[latest]deps} + django42: Django~=4.2.0 + django52: Django~=5.2.0 + django32: djangorestframework~=3.14.0 + !django32: djangorestframework -rrequirements/test-ci.txt [testenv:isort] @@ -40,8 +34,8 @@ deps = [testenv:warnings] ignore_outcome = True -unignore_outcomes = True commands = python -Werror ./runtests.py --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner {posargs} deps = - {[latest]deps} + Django~=5.2.0 + djangorestframework -rrequirements/test-ci.txt From c642f6e6137ffdb39d9d869a5b40764041fd0d27 Mon Sep 17 00:00:00 2001 From: Justin Krinke Date: Fri, 31 Jul 2026 09:29:48 -0400 Subject: [PATCH 3/3] Fix docs tox env: itercompat removal and rtd-theme deprecation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs env installs the package with an unpinned Django and now gets 6.x, where django.utils.itercompat was removed (deprecated in 5.1), so importing django_filters from docs/conf.py crashed sphinx. Inline is_iterable in django_filters.utils (Django's implementation) and stop importing itercompat — this also silences the 5.1+ deprecation warning for the monolith. Pin the docs env to Django 5.2 so it can't drift to an unsupported major again. Also drop the deprecated sphinx_rtd_theme.get_html_theme_path() call in docs/conf.py, which sphinx-build -W turns into a hard failure; the theme registers itself with Sphinx since 1.0. 508 tests still green on Django 3.2 and 5.2; flake8/isort clean; docs env builds. Co-Authored-By: Claude Fable 5 --- django_filters/filters.py | 3 +-- django_filters/utils.py | 10 ++++++++++ docs/conf.py | 5 ++--- tox.ini | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/django_filters/filters.py b/django_filters/filters.py index 3a494c15c..bc8b0584c 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -6,7 +6,6 @@ from django.db.models import Q from django.db.models.constants import LOOKUP_SEP from django.forms.utils import pretty_name -from django.utils.itercompat import is_iterable from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ @@ -27,7 +26,7 @@ RangeField, TimeRangeField, ) -from .utils import get_model_field, label_for_filter +from .utils import get_model_field, is_iterable, label_for_filter __all__ = [ "AllValuesFilter", diff --git a/django_filters/utils.py b/django_filters/utils.py index b032637e4..cf502b2b8 100644 --- a/django_filters/utils.py +++ b/django_filters/utils.py @@ -17,6 +17,16 @@ from .exceptions import FieldLookupError +def is_iterable(x): + # Replacement for django.utils.itercompat.is_iterable, deprecated in + # Django 5.1 and removed in 6.0. + try: + iter(x) + except TypeError: + return False + return True + + def deprecate(msg, level_modifier=0): warnings.warn(msg, MigrationNotice, stacklevel=3 + level_modifier) diff --git a/docs/conf.py b/docs/conf.py index 6d4f63235..4e49d96f5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -265,7 +265,6 @@ # only import and set the theme if we're building docs locally if not on_rtd: - import sphinx_rtd_theme - + # sphinx_rtd_theme >= 1.0 registers itself with Sphinx; + # get_html_theme_path() is deprecated and warns under -W. html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] diff --git a/tox.ini b/tox.ini index c1721ca78..47a2fae34 100644 --- a/tox.ini +++ b/tox.ini @@ -29,6 +29,7 @@ deps = flake8 [testenv:docs] commands = sphinx-build -WE docs _docs deps = + Django~=5.2.0 sphinx sphinx-rtd-theme