summaryrefslogtreecommitdiff
path: root/tests/utils_tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests/utils_tests')
-rw-r--r--tests/utils_tests/test_choices.py305
1 files changed, 305 insertions, 0 deletions
diff --git a/tests/utils_tests/test_choices.py b/tests/utils_tests/test_choices.py
new file mode 100644
index 0000000000..d96c3d49c4
--- /dev/null
+++ b/tests/utils_tests/test_choices.py
@@ -0,0 +1,305 @@
+from unittest import mock
+
+from django.db.models import TextChoices
+from django.test import SimpleTestCase
+from django.utils.choices import CallableChoiceIterator, normalize_choices
+from django.utils.translation import gettext_lazy as _
+
+
+class NormalizeFieldChoicesTests(SimpleTestCase):
+ expected = [
+ ("C", _("Club")),
+ ("D", _("Diamond")),
+ ("H", _("Heart")),
+ ("S", _("Spade")),
+ ]
+ expected_nested = [
+ ("Audio", [("vinyl", _("Vinyl")), ("cd", _("CD"))]),
+ ("Video", [("vhs", _("VHS Tape")), ("dvd", _("DVD"))]),
+ ("unknown", _("Unknown")),
+ ]
+ invalid = [
+ 1j,
+ 123,
+ 123.45,
+ "invalid",
+ b"invalid",
+ _("invalid"),
+ object(),
+ None,
+ True,
+ False,
+ ]
+ invalid_iterable = [
+ # Special cases of a string-likes which would unpack incorrectly.
+ ["ab"],
+ [b"ab"],
+ [_("ab")],
+ # Non-iterable items or iterable items with incorrect number of
+ # elements that cannot be unpacked.
+ [123],
+ [("value",)],
+ [("value", "label", "other")],
+ ]
+ invalid_nested = [
+ # Nested choices can only be two-levels deep, so return callables,
+ # mappings, iterables, etc. at deeper levels unmodified.
+ [("Group", [("Value", lambda: "Label")])],
+ [("Group", [("Value", {"Label 1?": "Label 2?"})])],
+ [("Group", [("Value", [("Label 1?", "Label 2?")])])],
+ ]
+
+ def test_empty(self):
+ def generator():
+ yield from ()
+
+ for choices in ({}, [], (), set(), frozenset(), generator()):
+ with self.subTest(choices=choices):
+ self.assertEqual(normalize_choices(choices), [])
+
+ def test_choices(self):
+ class Medal(TextChoices):
+ GOLD = "GOLD", _("Gold")
+ SILVER = "SILVER", _("Silver")
+ BRONZE = "BRONZE", _("Bronze")
+
+ expected = [
+ ("GOLD", _("Gold")),
+ ("SILVER", _("Silver")),
+ ("BRONZE", _("Bronze")),
+ ]
+ self.assertEqual(normalize_choices(Medal), expected)
+
+ def test_callable(self):
+ def get_choices():
+ return {
+ "C": _("Club"),
+ "D": _("Diamond"),
+ "H": _("Heart"),
+ "S": _("Spade"),
+ }
+
+ get_choices_spy = mock.Mock(wraps=get_choices)
+ output = normalize_choices(get_choices_spy)
+
+ get_choices_spy.assert_not_called()
+ self.assertIsInstance(output, CallableChoiceIterator)
+ self.assertEqual(list(output), self.expected)
+ get_choices_spy.assert_called_once()
+
+ def test_mapping(self):
+ choices = {
+ "C": _("Club"),
+ "D": _("Diamond"),
+ "H": _("Heart"),
+ "S": _("Spade"),
+ }
+ self.assertEqual(normalize_choices(choices), self.expected)
+
+ def test_iterable(self):
+ choices = [
+ ("C", _("Club")),
+ ("D", _("Diamond")),
+ ("H", _("Heart")),
+ ("S", _("Spade")),
+ ]
+ self.assertEqual(normalize_choices(choices), self.expected)
+
+ def test_iterator(self):
+ def generator():
+ yield "C", _("Club")
+ yield "D", _("Diamond")
+ yield "H", _("Heart")
+ yield "S", _("Spade")
+
+ choices = generator()
+ self.assertEqual(normalize_choices(choices), self.expected)
+
+ def test_nested_callable(self):
+ def get_audio_choices():
+ return [("vinyl", _("Vinyl")), ("cd", _("CD"))]
+
+ def get_video_choices():
+ return [("vhs", _("VHS Tape")), ("dvd", _("DVD"))]
+
+ def get_media_choices():
+ return [
+ ("Audio", get_audio_choices),
+ ("Video", get_video_choices),
+ ("unknown", _("Unknown")),
+ ]
+
+ get_media_choices_spy = mock.Mock(wraps=get_media_choices)
+ output = normalize_choices(get_media_choices_spy)
+
+ get_media_choices_spy.assert_not_called()
+ self.assertIsInstance(output, CallableChoiceIterator)
+ self.assertEqual(list(output), self.expected_nested)
+ get_media_choices_spy.assert_called_once()
+
+ def test_nested_mapping(self):
+ choices = {
+ "Audio": {"vinyl": _("Vinyl"), "cd": _("CD")},
+ "Video": {"vhs": _("VHS Tape"), "dvd": _("DVD")},
+ "unknown": _("Unknown"),
+ }
+ self.assertEqual(normalize_choices(choices), self.expected_nested)
+
+ def test_nested_iterable(self):
+ choices = [
+ ("Audio", [("vinyl", _("Vinyl")), ("cd", _("CD"))]),
+ ("Video", [("vhs", _("VHS Tape")), ("dvd", _("DVD"))]),
+ ("unknown", _("Unknown")),
+ ]
+ self.assertEqual(normalize_choices(choices), self.expected_nested)
+
+ def test_nested_iterator(self):
+ def generate_audio_choices():
+ yield "vinyl", _("Vinyl")
+ yield "cd", _("CD")
+
+ def generate_video_choices():
+ yield "vhs", _("VHS Tape")
+ yield "dvd", _("DVD")
+
+ def generate_media_choices():
+ yield "Audio", generate_audio_choices()
+ yield "Video", generate_video_choices()
+ yield "unknown", _("Unknown")
+
+ choices = generate_media_choices()
+ self.assertEqual(normalize_choices(choices), self.expected_nested)
+
+ def test_callable_non_canonical(self):
+ # Canonical form is list of 2-tuple, but nested lists should work.
+ def get_choices():
+ return [
+ ["C", _("Club")],
+ ["D", _("Diamond")],
+ ["H", _("Heart")],
+ ["S", _("Spade")],
+ ]
+
+ get_choices_spy = mock.Mock(wraps=get_choices)
+ output = normalize_choices(get_choices_spy)
+
+ get_choices_spy.assert_not_called()
+ self.assertIsInstance(output, CallableChoiceIterator)
+ self.assertEqual(list(output), self.expected)
+ get_choices_spy.assert_called_once()
+
+ def test_iterable_non_canonical(self):
+ # Canonical form is list of 2-tuple, but nested lists should work.
+ choices = [
+ ["C", _("Club")],
+ ["D", _("Diamond")],
+ ["H", _("Heart")],
+ ["S", _("Spade")],
+ ]
+ self.assertEqual(normalize_choices(choices), self.expected)
+
+ def test_iterator_non_canonical(self):
+ # Canonical form is list of 2-tuple, but nested lists should work.
+ def generator():
+ yield ["C", _("Club")]
+ yield ["D", _("Diamond")]
+ yield ["H", _("Heart")]
+ yield ["S", _("Spade")]
+
+ choices = generator()
+ self.assertEqual(normalize_choices(choices), self.expected)
+
+ def test_nested_callable_non_canonical(self):
+ # Canonical form is list of 2-tuple, but nested lists should work.
+
+ def get_audio_choices():
+ return [["vinyl", _("Vinyl")], ["cd", _("CD")]]
+
+ def get_video_choices():
+ return [["vhs", _("VHS Tape")], ["dvd", _("DVD")]]
+
+ def get_media_choices():
+ return [
+ ["Audio", get_audio_choices],
+ ["Video", get_video_choices],
+ ["unknown", _("Unknown")],
+ ]
+
+ get_media_choices_spy = mock.Mock(wraps=get_media_choices)
+ output = normalize_choices(get_media_choices_spy)
+
+ get_media_choices_spy.assert_not_called()
+ self.assertIsInstance(output, CallableChoiceIterator)
+ self.assertEqual(list(output), self.expected_nested)
+ get_media_choices_spy.assert_called_once()
+
+ def test_nested_iterable_non_canonical(self):
+ # Canonical form is list of 2-tuple, but nested lists should work.
+ choices = [
+ ["Audio", [["vinyl", _("Vinyl")], ["cd", _("CD")]]],
+ ["Video", [["vhs", _("VHS Tape")], ["dvd", _("DVD")]]],
+ ["unknown", _("Unknown")],
+ ]
+ self.assertEqual(normalize_choices(choices), self.expected_nested)
+
+ def test_nested_iterator_non_canonical(self):
+ # Canonical form is list of 2-tuple, but nested lists should work.
+ def generator():
+ yield ["Audio", [["vinyl", _("Vinyl")], ["cd", _("CD")]]]
+ yield ["Video", [["vhs", _("VHS Tape")], ["dvd", _("DVD")]]]
+ yield ["unknown", _("Unknown")]
+
+ choices = generator()
+ self.assertEqual(normalize_choices(choices), self.expected_nested)
+
+ def test_nested_mixed_mapping_and_iterable(self):
+ # Although not documented, as it's better to stick to either mappings
+ # or iterables, nesting of mappings within iterables and vice versa
+ # works and is likely to occur in the wild. This is supported by the
+ # recursive call to `normalize_choices()` which will normalize nested
+ # choices.
+ choices = {
+ "Audio": [("vinyl", _("Vinyl")), ("cd", _("CD"))],
+ "Video": [("vhs", _("VHS Tape")), ("dvd", _("DVD"))],
+ "unknown": _("Unknown"),
+ }
+ self.assertEqual(normalize_choices(choices), self.expected_nested)
+ choices = [
+ ("Audio", {"vinyl": _("Vinyl"), "cd": _("CD")}),
+ ("Video", {"vhs": _("VHS Tape"), "dvd": _("DVD")}),
+ ("unknown", _("Unknown")),
+ ]
+ self.assertEqual(normalize_choices(choices), self.expected_nested)
+
+ def test_iterable_set(self):
+ # Although not documented, as sets are unordered which results in
+ # randomised order in form fields, passing a set of 2-tuples works.
+ # Consistent ordering of choices on model fields in migrations is
+ # enforced by the migrations serializer.
+ choices = {
+ ("C", _("Club")),
+ ("D", _("Diamond")),
+ ("H", _("Heart")),
+ ("S", _("Spade")),
+ }
+ self.assertEqual(sorted(normalize_choices(choices)), sorted(self.expected))
+
+ def test_unsupported_values_returned_unmodified(self):
+ # Unsupported values must be returned unmodified for model system check
+ # to work correctly.
+ for value in self.invalid + self.invalid_iterable + self.invalid_nested:
+ with self.subTest(value=value):
+ self.assertEqual(normalize_choices(value), value)
+
+ def test_unsupported_values_from_callable_returned_unmodified(self):
+ for value in self.invalid_iterable + self.invalid_nested:
+ with self.subTest(value=value):
+ self.assertEqual(list(normalize_choices(lambda: value)), value)
+
+ def test_unsupported_values_from_iterator_returned_unmodified(self):
+ for value in self.invalid_nested:
+ with self.subTest(value=value):
+ self.assertEqual(
+ list(normalize_choices((lambda: (yield from value))())),
+ value,
+ )