diff --git a/AUTHORS.rst b/AUTHORS.rst index 83a7ca9e7d..1854320f57 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -23,6 +23,7 @@ their individual contributions. * `Andrea Reina `_ * `Andrew Sansom `_ * `Anne Archibald `_ +* `Arjoonn Sharma `_ * `Ben Anhalt `_ * `Ben Peterson `_ (killthrush@hotmail.com) * `Benjamin Lee `_ (benjamindlee@me.com) diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..0787682e7c --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,6 @@ +RELEASE_TYPE: patch + +This patch fixes file field handling for django forms when using +:func:`~hypothesis.extra.django.from_form` (:issue:`4093`). + +Thanks to Arjoonn Sharma for this fix! diff --git a/hypothesis-python/src/hypothesis/extra/django/_impl.py b/hypothesis-python/src/hypothesis/extra/django/_impl.py index 09a1c2707c..a21ead029d 100644 --- a/hypothesis-python/src/hypothesis/extra/django/_impl.py +++ b/hypothesis-python/src/hypothesis/extra/django/_impl.py @@ -179,7 +179,10 @@ def from_form( unbound_form = form(**form_kwargs) fields_by_name = {} + file_fields_by_name = set() for name, field in unbound_form.fields.items(): + if isinstance(field, df.FileField): + file_fields_by_name.add(name) if isinstance(field, df.MultiValueField): # PS: So this is a little strange, but MultiValueFields must # have their form data encoded in a particular way for the @@ -201,11 +204,20 @@ def from_form( for name, field in sorted(fields_by_name.items()): if name not in field_strategies and not field.disabled: field_strategies[name] = from_field(field) + # File strategies need to be passed separately into the form constructor + # and so we create a separate dict for them. These fields are not supposed + # to be sent via the data constructor + # ref: https://docs.djangoproject.com/en/5.1/topics/http/file-uploads/#id4 + file_strategies = {} + for name in list(field_strategies): + if name in file_fields_by_name: + file_strategies[name] = field_strategies.pop(name) return _forms_impl( st.builds( partial(form, **form_kwargs), # type: ignore data=st.fixed_dictionaries(field_strategies), # type: ignore + files=st.fixed_dictionaries(file_strategies), # type: ignore ) ) diff --git a/hypothesis-python/tests/django/toystore/forms.py b/hypothesis-python/tests/django/toystore/forms.py index dfd482bbad..7dde8e5ccc 100644 --- a/hypothesis-python/tests/django/toystore/forms.py +++ b/hypothesis-python/tests/django/toystore/forms.py @@ -46,6 +46,12 @@ class Meta: fields = "__all__" +class FileFieldsForm(ReprModelForm): + class Meta: + model = FileFields + fields = "__all__" + + class CustomerForm(ReprModelForm): class Meta: model = Customer diff --git a/hypothesis-python/tests/django/toystore/models.py b/hypothesis-python/tests/django/toystore/models.py index b945f87243..49ba9367f2 100644 --- a/hypothesis-python/tests/django/toystore/models.py +++ b/hypothesis-python/tests/django/toystore/models.py @@ -99,6 +99,10 @@ class CustomishDefault(models.Model): customish = CustomishField(default="b") +class FileFields(models.Model): + file1 = FileField() + + class MandatoryComputed(models.Model): name = models.CharField(max_length=100, unique=True) company = models.ForeignKey(Company, null=False, on_delete=models.CASCADE) diff --git a/hypothesis-python/tests/django/toystore/test_given_forms.py b/hypothesis-python/tests/django/toystore/test_given_forms.py index be8fa470de..703f88c655 100644 --- a/hypothesis-python/tests/django/toystore/test_given_forms.py +++ b/hypothesis-python/tests/django/toystore/test_given_forms.py @@ -41,6 +41,7 @@ UsernameForm, UUIDFieldForm, WithValidatorsForm, + FileFieldsForm, ) from tests.django.toystore.models import Company @@ -130,6 +131,10 @@ def test_tight_validators_form(self, x): self.assertTrue(1 <= x.data["_float_one_to_five"] <= 5) self.assertTrue(5 <= len(x.data["_string_five_to_ten"]) <= 10) + @given(from_form(FileFieldsForm)) + def test_file_fields_form(self, x): + self.assertTrue(x.data["file1"]) + @given(from_form(UsernameForm)) def test_username_form(self, username_form): self.assertTrue(username_form.is_valid())