Opened 4 years ago
Last modified 12 days ago
#34156 assigned Bug
TypedChoiceField.to_python() always returns a string
| Reported by: | Yoshio Hasegawa | Owned by: | Django Sprints |
|---|---|---|---|
| Component: | Forms | Version: | 4.1 |
| Severity: | Normal | Keywords: | Form, TypedChoiceField, IntegerChoices, Coercion |
| Cc: | Triage Stage: | Accepted | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Coercion always fails when using IntegerChoices with a TypedChoiceField in a Django form.
When a value is cleaned in a TypedChoiceField, the inherited ChoiceField's to_python() method will return the value casted as a string. This means that when coercion with an IntegerChoices object is attempted, it will always fail as IntegerChoices will expect an int type when instantiated.
Example
class SomeIntegerChoice(models.IntegerChoices): VAL_2350 = (2350, "value 2350") VAL_4100 = (4100, "value 4100") VAL_8760 = (8760, "value 8760")
form.fields["Integer_choice"] = forms.TypedChoiceField( required=required, choices=SomeIntegerChoice.choices, coerce=SomeIntegerChoice, )
This field will never pass validation since ChoiceField (inherited by TypedChoiceField) has a to_python() method that casts the provided value as a string:
# class ChoiceField: # ... def to_python(self, value): """Return a string.""" if value in self.empty_values: return "" return str(value)
To explain further... ChoiceField.to_python() will be called when TypedChoiceField attempts to clean() a value. After the value is cleaned, it is coerced using the provided class definition via the coerce property.
Here is example code from Django to show how this happens:
# class TypedChoiceField: # ... def _coerce(self, value): """ Validate that the value can be coerced to the right type (if not empty). """ if value == self.empty_value or value in self.empty_values: return self.empty_value try: value = self.coerce(value) except (ValueError, TypeError, ValidationError): raise ValidationError( self.error_messages["invalid_choice"], code="invalid_choice", params={"value": value}, ) return value def clean(self, value): value = super().clean(value) return self._coerce(value)
Change History (4)
follow-up: 2 comment:1 by , 4 years ago
| Resolution: | → invalid |
|---|---|
| Status: | new → closed |
comment:2 by , 2 weeks ago
| Resolution: | invalid |
|---|---|
| Status: | closed → new |
| Summary: | TypedChoiceField not compatible with IntegerChoices → TypedChoiceField.to_python() always returns a string |
Replying to Mariusz Felisiak:
As far as I'm aware, it's an issue in your code. For
IntegerChoicesyou should passcoerce=intand everything works fine.
Mariusz is right, coerce=int is enough to make the error go away, but the OP might have wanted to be able to use enum values in their code, e.g.:
>>> TypedChoiceField(choices=Suit, coerce=Suit).clean(4) Suit.CLUB
Also, although it's handy that int can handle strings just as well as ints, decimal.Decimal would surely produce interesting differences:
>>> decimal.Decimal(4.0)
Decimal('4')
>>> decimal.Decimal(str(4.0)) # to_python does str()
Decimal('4.0')
I'm suggesting we take another look, because the state of play after #21397 is not quite ideal -- it leaves to_python() to return str, whereas it's documented to return the proper python type. Breaking this expectation led to 500s in the admin changelist in #36865/4cecf3039586ea738afafb9a28c946bff42c37c1 (crash report in #37263).
Tests pass with the following starting suggestion (maybe also worth looking into self.empty_values):
-
django/forms/fields.py
diff --git a/django/forms/fields.py b/django/forms/fields.py index ab3f6876df..4e5735db16 100644
a b class TypedChoiceField(ChoiceField): 938 938 ) 939 939 return value 940 940 941 def clean(self, value): 942 value = super().clean(value) 941 def to_python(self, value): 943 942 return self._coerce(value) 944 943 945 944 … … class TypedMultipleChoiceField(MultipleChoiceField): 1015 1014 ) 1016 1015 return new_value 1017 1016 1018 def clean(self, value): 1019 value = super().clean(value) 1017 def to_python(self, value): 1020 1018 return self._coerce(value) 1021 1019 1022 1020 def validate(self, value): -
tests/forms_tests/field_tests/test_typedchoicefield.py
diff --git a/tests/forms_tests/field_tests/test_typedchoicefield.py b/tests/forms_tests/field_tests/test_typedchoicefield.py index fe5cbe12ee..0391513c89 100644
a b class TypedChoiceFieldTest(SimpleTestCase): 89 89 f = TypedChoiceField( 90 90 choices=[(1, "1"), (2, "2")], coerce=coerce_func, required=True 91 91 ) 92 self.assertEqual(decimal.Decimal("1.2"), f. clean("2"))92 self.assertEqual(decimal.Decimal("1.2"), f.to_python("2")) 93 93 with self.assertRaisesMessage(ValidationError, "'This field is required.'"): 94 94 f.clean("") 95 msg = "'Select a valid choice. 3 is not one of the available choices.'"95 msg = "'Select a valid choice. 1.3 is not one of the available choices.'" 96 96 with self.assertRaisesMessage(ValidationError, msg): 97 97 f.clean("3") -
tests/forms_tests/field_tests/test_typedmultiplechoicefield.py
diff --git a/tests/forms_tests/field_tests/test_typedmultiplechoicefield.py b/tests/forms_tests/field_tests/test_typedmultiplechoicefield.py index e874e192d8..a634be564f 100644
a b class TypedMultipleChoiceFieldTest(SimpleTestCase): 80 80 f = TypedMultipleChoiceField( 81 81 choices=[(1, "1"), (2, "2")], coerce=coerce_func, required=True 82 82 ) 83 self.assertEqual([decimal.Decimal("1.2")], f. clean(["2"]))83 self.assertEqual([decimal.Decimal("1.2")], f.to_python(["2"])) 84 84 with self.assertRaisesMessage(ValidationError, "'This field is required.'"): 85 85 f.clean([]) 86 msg = "'Select a valid choice. 3 is not one of the available choices.'"86 msg = "'Select a valid choice. 1.3 is not one of the available choices.'" 87 87 with self.assertRaisesMessage(ValidationError, msg): 88 88 f.clean(["3"])
Since the solve is the same as the original request here, I figured to reopen.
comment:3 by , 2 weeks ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:4 by , 12 days ago
| Triage Stage: | Unreviewed → Accepted |
|---|
As far as I'm aware, it's an issue in your code. For
IntegerChoicesyou should passcoerce=intand everything works fine.