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/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 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/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..47a2fae34 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] @@ -35,13 +29,14 @@ deps = flake8 [testenv:docs] commands = sphinx-build -WE docs _docs deps = + Django~=5.2.0 sphinx sphinx-rtd-theme [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