diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 544424e61b..cf4f47a0a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,9 +73,8 @@ jobs: - check-py39-pytest46 - check-py39-pytest54 - check-pytest62 + - check-django50 - check-django42 - - check-django41 - - check-django32 - check-pandas22 - check-pandas21 - check-pandas20 diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..15582e3079 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,5 @@ +RELEASE_TYPE: minor + +This release improves support for Django 5.0, and drops support for end-of-life Django versions (< 4.2). + +Thanks to Joshua Munn for this contribution. diff --git a/hypothesis-python/src/hypothesis/extra/django/_fields.py b/hypothesis-python/src/hypothesis/extra/django/_fields.py index 181c8869f9..29f6dcf00a 100644 --- a/hypothesis-python/src/hypothesis/extra/django/_fields.py +++ b/hypothesis-python/src/hypothesis/extra/django/_fields.py @@ -57,7 +57,9 @@ def inner(field): def timezones(): # From Django 4.0, the default is to use zoneinfo instead of pytz. assert getattr(django.conf.settings, "USE_TZ", False) - if getattr(django.conf.settings, "USE_DEPRECATED_PYTZ", True): + if django.VERSION < (5, 0, 0) and getattr( + django.conf.settings, "USE_DEPRECATED_PYTZ", True + ): from hypothesis.extra.pytz import timezones else: from hypothesis.strategies import timezones diff --git a/hypothesis-python/src/hypothesis/extra/django/_impl.py b/hypothesis-python/src/hypothesis/extra/django/_impl.py index 5a7ab8f0e3..d4bcefb0c1 100644 --- a/hypothesis-python/src/hypothesis/extra/django/_impl.py +++ b/hypothesis-python/src/hypothesis/extra/django/_impl.py @@ -105,6 +105,7 @@ def from_model( name not in field_strategies and not field.auto_created and not isinstance(field, dm.AutoField) + and not isinstance(field, getattr(dm, "GeneratedField", ())) and field.default is dm.fields.NOT_PROVIDED ): field_strategies[name] = from_field(field) diff --git a/hypothesis-python/tests/django/manage.py b/hypothesis-python/tests/django/manage.py index be364ea9a3..6d1de7eebd 100755 --- a/hypothesis-python/tests/django/manage.py +++ b/hypothesis-python/tests/django/manage.py @@ -38,6 +38,12 @@ except ImportError: RemovedInDjango50Warning = () + try: + from django.utils.deprecation import RemovedInDjango60Warning + except ImportError: + RemovedInDjango60Warning = () + with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RemovedInDjango50Warning) + warnings.simplefilter("ignore", category=RemovedInDjango60Warning) execute_from_command_line(sys.argv) diff --git a/hypothesis-python/tests/django/toys/settings.py b/hypothesis-python/tests/django/toys/settings.py index 6f753e9eca..6f8ae97fb5 100644 --- a/hypothesis-python/tests/django/toys/settings.py +++ b/hypothesis-python/tests/django/toys/settings.py @@ -19,6 +19,8 @@ import os +import django + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -85,7 +87,8 @@ USE_I18N = True -USE_L10N = True +if django.VERSION < (5, 0, 0): + USE_L10N = True USE_TZ = os.environ.get("HYPOTHESIS_DJANGO_USETZ", "TRUE") == "TRUE" @@ -121,3 +124,7 @@ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] + +# Transitional setting until 6.0. See +# https://docs.djangoproject.com/en/5.0/ref/forms/fields/#django.forms.URLField.assume_scheme +FORMS_URLFIELD_ASSUME_HTTPS = True diff --git a/hypothesis-python/tests/django/toystore/models.py b/hypothesis-python/tests/django/toystore/models.py index 80810fc8c1..b945f87243 100644 --- a/hypothesis-python/tests/django/toystore/models.py +++ b/hypothesis-python/tests/django/toystore/models.py @@ -8,7 +8,9 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. +import django from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator from django.db import models @@ -149,3 +151,23 @@ class CompanyExtension(models.Model): class UserSpecifiedAutoId(models.Model): my_id = models.AutoField(primary_key=True) + + +if django.VERSION >= (5, 0, 0): + import math + + class Pizza(models.Model): + AREA = math.pi * models.F("radius") ** 2 + + radius = models.IntegerField(validators=[MinValueValidator(1)]) + slices = models.PositiveIntegerField(validators=[MinValueValidator(2)]) + total_area = models.GeneratedField( + expression=AREA, + output_field=models.FloatField(), + db_persist=True, + ) + slice_area = models.GeneratedField( + expression=AREA / models.F("slices"), + output_field=models.FloatField(), + db_persist=False, + ) diff --git a/hypothesis-python/tests/django/toystore/test_given_models.py b/hypothesis-python/tests/django/toystore/test_given_models.py index ff291cd63e..8e393ca25c 100644 --- a/hypothesis-python/tests/django/toystore/test_given_models.py +++ b/hypothesis-python/tests/django/toystore/test_given_models.py @@ -11,6 +11,7 @@ import datetime as dt from uuid import UUID +import django from django.conf import settings as django_settings from django.contrib.auth.models import User @@ -210,3 +211,19 @@ class TestUserSpecifiedAutoId(TestCase): def test_user_specified_auto_id(self, user_specified_auto_id): self.assertIsInstance(user_specified_auto_id, UserSpecifiedAutoId) self.assertIsNotNone(user_specified_auto_id.pk) + + +if django.VERSION >= (5, 0, 0): + from tests.django.toystore.models import Pizza + + class TestModelWithGeneratedField(TestCase): + @given(from_model(Pizza)) + def test_create_pizza(self, pizza): + """ + Strategies are not inferred for GeneratedField. + """ + + pizza.full_clean() + # Check the expected types of the generated fields. + self.assertIsInstance(pizza.slice_area, float) + self.assertIsInstance(pizza.total_area, float) diff --git a/hypothesis-python/tox.ini b/hypothesis-python/tox.ini index 6042594930..46cf4633cd 100644 --- a/hypothesis-python/tox.ini +++ b/hypothesis-python/tox.ini @@ -156,22 +156,18 @@ commands = niche: bash scripts/other-tests.sh custom: python -bb -X dev -m pytest {posargs} -[testenv:django32] -commands = - pip install .[pytz] - pip install django~=3.2.15 - python -bb -X dev -m tests.django.manage test tests.django {posargs} - -[testenv:django41] +[testenv:django42] +setenv= + PYTHONWARNDEFAULTENCODING=1 commands = - pip install django~=4.1.0 + pip install django~=4.2.0 python -bb -X dev -m tests.django.manage test tests.django {posargs} -[testenv:django42] +[testenv:django50] setenv= PYTHONWARNDEFAULTENCODING=1 commands = - pip install django~=4.2.0 + pip install django~=5.0.0 python -bb -X dev -m tests.django.manage test tests.django {posargs} [testenv:py{37,38,39}-nose] diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py index 95402bfadb..5a3b593c8e 100644 --- a/tooling/src/hypothesistooling/__main__.py +++ b/tooling/src/hypothesistooling/__main__.py @@ -512,7 +512,7 @@ def standard_tox_task(name, py=ci_version): standard_tox_task("py39-pytest54", py="3.9") standard_tox_task("pytest62") -for n in [32, 41, 42]: +for n in [42, 50]: standard_tox_task(f"django{n}") for n in [13, 14, 15, 20, 21, 22]: