test_choices.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. from unittest import mock
  2. from django.db.models import TextChoices
  3. from django.test import SimpleTestCase
  4. from django.utils.choices import CallableChoiceIterator, normalize_choices
  5. from django.utils.translation import gettext_lazy as _
  6. class NormalizeFieldChoicesTests(SimpleTestCase):
  7. expected = [
  8. ("C", _("Club")),
  9. ("D", _("Diamond")),
  10. ("H", _("Heart")),
  11. ("S", _("Spade")),
  12. ]
  13. expected_nested = [
  14. ("Audio", [("vinyl", _("Vinyl")), ("cd", _("CD"))]),
  15. ("Video", [("vhs", _("VHS Tape")), ("dvd", _("DVD"))]),
  16. ("unknown", _("Unknown")),
  17. ]
  18. invalid = [
  19. 1j,
  20. 123,
  21. 123.45,
  22. "invalid",
  23. b"invalid",
  24. _("invalid"),
  25. object(),
  26. None,
  27. True,
  28. False,
  29. ]
  30. invalid_iterable = [
  31. # Special cases of a string-likes which would unpack incorrectly.
  32. ["ab"],
  33. [b"ab"],
  34. [_("ab")],
  35. # Non-iterable items or iterable items with incorrect number of
  36. # elements that cannot be unpacked.
  37. [123],
  38. [("value",)],
  39. [("value", "label", "other")],
  40. ]
  41. invalid_nested = [
  42. # Nested choices can only be two-levels deep, so return callables,
  43. # mappings, iterables, etc. at deeper levels unmodified.
  44. [("Group", [("Value", lambda: "Label")])],
  45. [("Group", [("Value", {"Label 1?": "Label 2?"})])],
  46. [("Group", [("Value", [("Label 1?", "Label 2?")])])],
  47. ]
  48. def test_empty(self):
  49. def generator():
  50. yield from ()
  51. for choices in ({}, [], (), set(), frozenset(), generator()):
  52. with self.subTest(choices=choices):
  53. self.assertEqual(normalize_choices(choices), [])
  54. def test_choices(self):
  55. class Medal(TextChoices):
  56. GOLD = "GOLD", _("Gold")
  57. SILVER = "SILVER", _("Silver")
  58. BRONZE = "BRONZE", _("Bronze")
  59. expected = [
  60. ("GOLD", _("Gold")),
  61. ("SILVER", _("Silver")),
  62. ("BRONZE", _("Bronze")),
  63. ]
  64. self.assertEqual(normalize_choices(Medal), expected)
  65. def test_callable(self):
  66. def get_choices():
  67. return {
  68. "C": _("Club"),
  69. "D": _("Diamond"),
  70. "H": _("Heart"),
  71. "S": _("Spade"),
  72. }
  73. get_choices_spy = mock.Mock(wraps=get_choices)
  74. output = normalize_choices(get_choices_spy)
  75. get_choices_spy.assert_not_called()
  76. self.assertIsInstance(output, CallableChoiceIterator)
  77. self.assertEqual(list(output), self.expected)
  78. get_choices_spy.assert_called_once()
  79. def test_mapping(self):
  80. choices = {
  81. "C": _("Club"),
  82. "D": _("Diamond"),
  83. "H": _("Heart"),
  84. "S": _("Spade"),
  85. }
  86. self.assertEqual(normalize_choices(choices), self.expected)
  87. def test_iterable(self):
  88. choices = [
  89. ("C", _("Club")),
  90. ("D", _("Diamond")),
  91. ("H", _("Heart")),
  92. ("S", _("Spade")),
  93. ]
  94. self.assertEqual(normalize_choices(choices), self.expected)
  95. def test_iterator(self):
  96. def generator():
  97. yield "C", _("Club")
  98. yield "D", _("Diamond")
  99. yield "H", _("Heart")
  100. yield "S", _("Spade")
  101. choices = generator()
  102. self.assertEqual(normalize_choices(choices), self.expected)
  103. def test_nested_callable(self):
  104. def get_audio_choices():
  105. return [("vinyl", _("Vinyl")), ("cd", _("CD"))]
  106. def get_video_choices():
  107. return [("vhs", _("VHS Tape")), ("dvd", _("DVD"))]
  108. def get_media_choices():
  109. return [
  110. ("Audio", get_audio_choices),
  111. ("Video", get_video_choices),
  112. ("unknown", _("Unknown")),
  113. ]
  114. get_media_choices_spy = mock.Mock(wraps=get_media_choices)
  115. output = normalize_choices(get_media_choices_spy)
  116. get_media_choices_spy.assert_not_called()
  117. self.assertIsInstance(output, CallableChoiceIterator)
  118. self.assertEqual(list(output), self.expected_nested)
  119. get_media_choices_spy.assert_called_once()
  120. def test_nested_mapping(self):
  121. choices = {
  122. "Audio": {"vinyl": _("Vinyl"), "cd": _("CD")},
  123. "Video": {"vhs": _("VHS Tape"), "dvd": _("DVD")},
  124. "unknown": _("Unknown"),
  125. }
  126. self.assertEqual(normalize_choices(choices), self.expected_nested)
  127. def test_nested_iterable(self):
  128. choices = [
  129. ("Audio", [("vinyl", _("Vinyl")), ("cd", _("CD"))]),
  130. ("Video", [("vhs", _("VHS Tape")), ("dvd", _("DVD"))]),
  131. ("unknown", _("Unknown")),
  132. ]
  133. self.assertEqual(normalize_choices(choices), self.expected_nested)
  134. def test_nested_iterator(self):
  135. def generate_audio_choices():
  136. yield "vinyl", _("Vinyl")
  137. yield "cd", _("CD")
  138. def generate_video_choices():
  139. yield "vhs", _("VHS Tape")
  140. yield "dvd", _("DVD")
  141. def generate_media_choices():
  142. yield "Audio", generate_audio_choices()
  143. yield "Video", generate_video_choices()
  144. yield "unknown", _("Unknown")
  145. choices = generate_media_choices()
  146. self.assertEqual(normalize_choices(choices), self.expected_nested)
  147. def test_callable_non_canonical(self):
  148. # Canonical form is list of 2-tuple, but nested lists should work.
  149. def get_choices():
  150. return [
  151. ["C", _("Club")],
  152. ["D", _("Diamond")],
  153. ["H", _("Heart")],
  154. ["S", _("Spade")],
  155. ]
  156. get_choices_spy = mock.Mock(wraps=get_choices)
  157. output = normalize_choices(get_choices_spy)
  158. get_choices_spy.assert_not_called()
  159. self.assertIsInstance(output, CallableChoiceIterator)
  160. self.assertEqual(list(output), self.expected)
  161. get_choices_spy.assert_called_once()
  162. def test_iterable_non_canonical(self):
  163. # Canonical form is list of 2-tuple, but nested lists should work.
  164. choices = [
  165. ["C", _("Club")],
  166. ["D", _("Diamond")],
  167. ["H", _("Heart")],
  168. ["S", _("Spade")],
  169. ]
  170. self.assertEqual(normalize_choices(choices), self.expected)
  171. def test_iterator_non_canonical(self):
  172. # Canonical form is list of 2-tuple, but nested lists should work.
  173. def generator():
  174. yield ["C", _("Club")]
  175. yield ["D", _("Diamond")]
  176. yield ["H", _("Heart")]
  177. yield ["S", _("Spade")]
  178. choices = generator()
  179. self.assertEqual(normalize_choices(choices), self.expected)
  180. def test_nested_callable_non_canonical(self):
  181. # Canonical form is list of 2-tuple, but nested lists should work.
  182. def get_audio_choices():
  183. return [["vinyl", _("Vinyl")], ["cd", _("CD")]]
  184. def get_video_choices():
  185. return [["vhs", _("VHS Tape")], ["dvd", _("DVD")]]
  186. def get_media_choices():
  187. return [
  188. ["Audio", get_audio_choices],
  189. ["Video", get_video_choices],
  190. ["unknown", _("Unknown")],
  191. ]
  192. get_media_choices_spy = mock.Mock(wraps=get_media_choices)
  193. output = normalize_choices(get_media_choices_spy)
  194. get_media_choices_spy.assert_not_called()
  195. self.assertIsInstance(output, CallableChoiceIterator)
  196. self.assertEqual(list(output), self.expected_nested)
  197. get_media_choices_spy.assert_called_once()
  198. def test_nested_iterable_non_canonical(self):
  199. # Canonical form is list of 2-tuple, but nested lists should work.
  200. choices = [
  201. ["Audio", [["vinyl", _("Vinyl")], ["cd", _("CD")]]],
  202. ["Video", [["vhs", _("VHS Tape")], ["dvd", _("DVD")]]],
  203. ["unknown", _("Unknown")],
  204. ]
  205. self.assertEqual(normalize_choices(choices), self.expected_nested)
  206. def test_nested_iterator_non_canonical(self):
  207. # Canonical form is list of 2-tuple, but nested lists should work.
  208. def generator():
  209. yield ["Audio", [["vinyl", _("Vinyl")], ["cd", _("CD")]]]
  210. yield ["Video", [["vhs", _("VHS Tape")], ["dvd", _("DVD")]]]
  211. yield ["unknown", _("Unknown")]
  212. choices = generator()
  213. self.assertEqual(normalize_choices(choices), self.expected_nested)
  214. def test_nested_mixed_mapping_and_iterable(self):
  215. # Although not documented, as it's better to stick to either mappings
  216. # or iterables, nesting of mappings within iterables and vice versa
  217. # works and is likely to occur in the wild. This is supported by the
  218. # recursive call to `normalize_choices()` which will normalize nested
  219. # choices.
  220. choices = {
  221. "Audio": [("vinyl", _("Vinyl")), ("cd", _("CD"))],
  222. "Video": [("vhs", _("VHS Tape")), ("dvd", _("DVD"))],
  223. "unknown": _("Unknown"),
  224. }
  225. self.assertEqual(normalize_choices(choices), self.expected_nested)
  226. choices = [
  227. ("Audio", {"vinyl": _("Vinyl"), "cd": _("CD")}),
  228. ("Video", {"vhs": _("VHS Tape"), "dvd": _("DVD")}),
  229. ("unknown", _("Unknown")),
  230. ]
  231. self.assertEqual(normalize_choices(choices), self.expected_nested)
  232. def test_iterable_set(self):
  233. # Although not documented, as sets are unordered which results in
  234. # randomised order in form fields, passing a set of 2-tuples works.
  235. # Consistent ordering of choices on model fields in migrations is
  236. # enforced by the migrations serializer.
  237. choices = {
  238. ("C", _("Club")),
  239. ("D", _("Diamond")),
  240. ("H", _("Heart")),
  241. ("S", _("Spade")),
  242. }
  243. self.assertEqual(sorted(normalize_choices(choices)), sorted(self.expected))
  244. def test_unsupported_values_returned_unmodified(self):
  245. # Unsupported values must be returned unmodified for model system check
  246. # to work correctly.
  247. for value in self.invalid + self.invalid_iterable + self.invalid_nested:
  248. with self.subTest(value=value):
  249. self.assertEqual(normalize_choices(value), value)
  250. def test_unsupported_values_from_callable_returned_unmodified(self):
  251. for value in self.invalid_iterable + self.invalid_nested:
  252. with self.subTest(value=value):
  253. self.assertEqual(list(normalize_choices(lambda: value)), value)
  254. def test_unsupported_values_from_iterator_returned_unmodified(self):
  255. for value in self.invalid_nested:
  256. with self.subTest(value=value):
  257. self.assertEqual(
  258. list(normalize_choices((lambda: (yield from value))())),
  259. value,
  260. )