WTForms 3.3.0b2__tar.gz → 3.3.0b3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/CHANGES.rst +11 -0
  2. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/PKG-INFO +1 -1
  3. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/fields.rst +40 -19
  4. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/__init__.py +3 -1
  5. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/datalist.py +16 -11
  6. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/fields/__init__.py +4 -0
  7. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/fields/choices.py +56 -30
  8. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/fields/core.py +4 -1
  9. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/fields/form.py +2 -2
  10. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/fields/list.py +2 -2
  11. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/form.py +10 -5
  12. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_form.py +6 -6
  13. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_select.py +129 -37
  14. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_selectmultiple.py +43 -6
  15. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/test_datalist.py +20 -0
  16. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/.gitignore +0 -0
  17. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/LICENSE.rst +0 -0
  18. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/README.rst +0 -0
  19. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/Makefile +0 -0
  20. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/_static/logo_joined.svg +0 -0
  21. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/_static/logo_split.svg +0 -0
  22. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/_static/logo_square.svg +0 -0
  23. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/changes.rst +0 -0
  24. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/conf.py +0 -0
  25. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/contributing.rst +0 -0
  26. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/crash_course.rst +0 -0
  27. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/csrf.rst +0 -0
  28. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/ext.rst +0 -0
  29. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/faq.rst +0 -0
  30. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/forms.rst +0 -0
  31. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/i18n.rst +0 -0
  32. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/index.rst +0 -0
  33. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/license.rst +0 -0
  34. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/make.bat +0 -0
  35. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/meta.rst +0 -0
  36. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/specific_problems.rst +0 -0
  37. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/validators.rst +0 -0
  38. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/whats_new.rst +0 -0
  39. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/docs/widgets.rst +0 -0
  40. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/hatch_build.py +0 -0
  41. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/pyproject.toml +0 -0
  42. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/_compat.py +0 -0
  43. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/csrf/__init__.py +0 -0
  44. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/csrf/core.py +0 -0
  45. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/csrf/session.py +0 -0
  46. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/fields/datetime.py +0 -0
  47. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/fields/numeric.py +0 -0
  48. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/fields/simple.py +0 -0
  49. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/i18n.py +0 -0
  50. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/README.md +0 -0
  51. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ar/LC_MESSAGES/wtforms.mo +0 -0
  52. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ar/LC_MESSAGES/wtforms.po +0 -0
  53. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/bg/LC_MESSAGES/wtforms.mo +0 -0
  54. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/bg/LC_MESSAGES/wtforms.po +0 -0
  55. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ca/LC_MESSAGES/wtforms.mo +0 -0
  56. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ca/LC_MESSAGES/wtforms.po +0 -0
  57. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/cs_CZ/LC_MESSAGES/wtforms.mo +0 -0
  58. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/cs_CZ/LC_MESSAGES/wtforms.po +0 -0
  59. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/cy/LC_MESSAGES/wtforms.mo +0 -0
  60. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/cy/LC_MESSAGES/wtforms.po +0 -0
  61. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/de/LC_MESSAGES/wtforms.mo +0 -0
  62. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/de/LC_MESSAGES/wtforms.po +0 -0
  63. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/de_CH/LC_MESSAGES/wtforms.mo +0 -0
  64. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/de_CH/LC_MESSAGES/wtforms.po +0 -0
  65. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/el/LC_MESSAGES/wtforms.mo +0 -0
  66. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/el/LC_MESSAGES/wtforms.po +0 -0
  67. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/en/LC_MESSAGES/wtforms.mo +0 -0
  68. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/en/LC_MESSAGES/wtforms.po +0 -0
  69. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/es/LC_MESSAGES/wtforms.mo +0 -0
  70. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/es/LC_MESSAGES/wtforms.po +0 -0
  71. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/et/LC_MESSAGES/wtforms.mo +0 -0
  72. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/et/LC_MESSAGES/wtforms.po +0 -0
  73. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/fa/LC_MESSAGES/wtforms.mo +0 -0
  74. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/fa/LC_MESSAGES/wtforms.po +0 -0
  75. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/fi/LC_MESSAGES/wtforms.mo +0 -0
  76. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/fi/LC_MESSAGES/wtforms.po +0 -0
  77. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/fr/LC_MESSAGES/wtforms.mo +0 -0
  78. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/fr/LC_MESSAGES/wtforms.po +0 -0
  79. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/he/LC_MESSAGES/wtforms.mo +0 -0
  80. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/he/LC_MESSAGES/wtforms.po +0 -0
  81. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/hu/LC_MESSAGES/wtforms.mo +0 -0
  82. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/hu/LC_MESSAGES/wtforms.po +0 -0
  83. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/it/LC_MESSAGES/wtforms.mo +0 -0
  84. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/it/LC_MESSAGES/wtforms.po +0 -0
  85. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ja/LC_MESSAGES/wtforms.mo +0 -0
  86. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ja/LC_MESSAGES/wtforms.po +0 -0
  87. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/kk/LC_MESSAGES/wtforms.mo +0 -0
  88. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/kk/LC_MESSAGES/wtforms.po +0 -0
  89. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ko/LC_MESSAGES/wtforms.mo +0 -0
  90. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ko/LC_MESSAGES/wtforms.po +0 -0
  91. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/nb/LC_MESSAGES/wtforms.mo +0 -0
  92. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/nb/LC_MESSAGES/wtforms.po +0 -0
  93. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/nl/LC_MESSAGES/wtforms.mo +0 -0
  94. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/nl/LC_MESSAGES/wtforms.po +0 -0
  95. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/pl/LC_MESSAGES/wtforms.mo +0 -0
  96. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/pl/LC_MESSAGES/wtforms.po +0 -0
  97. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/pt/LC_MESSAGES/wtforms.mo +0 -0
  98. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/pt/LC_MESSAGES/wtforms.po +0 -0
  99. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ro/LC_MESSAGES/wtforms.mo +0 -0
  100. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ro/LC_MESSAGES/wtforms.po +0 -0
  101. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ru/LC_MESSAGES/wtforms.mo +0 -0
  102. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ru/LC_MESSAGES/wtforms.po +0 -0
  103. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/sk/LC_MESSAGES/wtforms.mo +0 -0
  104. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/sk/LC_MESSAGES/wtforms.po +0 -0
  105. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/sr/LC_MESSAGES/wtforms.mo +0 -0
  106. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/sr/LC_MESSAGES/wtforms.po +0 -0
  107. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/sv/LC_MESSAGES/wtforms.mo +0 -0
  108. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/sv/LC_MESSAGES/wtforms.po +0 -0
  109. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ta/LC_MESSAGES/wtforms.mo +0 -0
  110. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/ta/LC_MESSAGES/wtforms.po +0 -0
  111. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/tr/LC_MESSAGES/wtforms.mo +0 -0
  112. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/tr/LC_MESSAGES/wtforms.po +0 -0
  113. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/uk/LC_MESSAGES/wtforms.mo +0 -0
  114. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/uk/LC_MESSAGES/wtforms.po +0 -0
  115. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/wtforms.pot +0 -0
  116. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/zh/LC_MESSAGES/wtforms.mo +0 -0
  117. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/zh/LC_MESSAGES/wtforms.po +0 -0
  118. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/zh_TW/LC_MESSAGES/wtforms.mo +0 -0
  119. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/locale/zh_TW/LC_MESSAGES/wtforms.po +0 -0
  120. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/meta.py +0 -0
  121. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/utils.py +0 -0
  122. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/validators.py +0 -0
  123. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/widgets/__init__.py +0 -0
  124. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/src/wtforms/widgets/core.py +0 -0
  125. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/__init__.py +0 -0
  126. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/common.py +0 -0
  127. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/conftest.py +0 -0
  128. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/__init__.py +0 -0
  129. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_boolean.py +0 -0
  130. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_button.py +0 -0
  131. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_color.py +0 -0
  132. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_custom.py +0 -0
  133. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_date.py +0 -0
  134. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_datetime.py +0 -0
  135. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_datetimelocal.py +0 -0
  136. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_decimal.py +0 -0
  137. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_defaults.py +0 -0
  138. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_field.py +0 -0
  139. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_file.py +0 -0
  140. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_filters.py +0 -0
  141. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_flags.py +0 -0
  142. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_float.py +0 -0
  143. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_hidden.py +0 -0
  144. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_html5.py +0 -0
  145. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_integer.py +0 -0
  146. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_label.py +0 -0
  147. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_list.py +0 -0
  148. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_month.py +0 -0
  149. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_password.py +0 -0
  150. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_radio.py +0 -0
  151. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_string.py +0 -0
  152. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_submit.py +0 -0
  153. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_textarea.py +0 -0
  154. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_time.py +0 -0
  155. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/fields/test_week.py +0 -0
  156. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/test_csrf.py +0 -0
  157. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/test_form.py +0 -0
  158. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/test_i18n.py +0 -0
  159. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/test_locale_babel.py +0 -0
  160. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/test_webob.py +0 -0
  161. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/test_widgets.py +0 -0
  162. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/__init__.py +0 -0
  163. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_anyof.py +0 -0
  164. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_data_required.py +0 -0
  165. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_date_range.py +0 -0
  166. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_disabled.py +0 -0
  167. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_email.py +0 -0
  168. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_equal_to.py +0 -0
  169. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_input_required.py +0 -0
  170. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_ipaddress.py +0 -0
  171. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_lazy_proxy.py +0 -0
  172. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_length.py +0 -0
  173. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_mac_address.py +0 -0
  174. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_noneof.py +0 -0
  175. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_number_range.py +0 -0
  176. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_optional.py +0 -0
  177. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_readonly.py +0 -0
  178. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_regexp.py +0 -0
  179. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_url.py +0 -0
  180. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_uuid.py +0 -0
  181. {wtforms-3.3.0b2 → wtforms-3.3.0b3}/tests/validators/test_validators.py +0 -0
@@ -1,5 +1,16 @@
1
1
  .. currentmodule:: wtforms
2
2
 
3
+ Version 3.3.0b3
4
+ ---------------
5
+
6
+ Released 2026-06-12
7
+
8
+ - Add :func:`fields.enum_choices`, :func:`fields.enum_coerce` and
9
+ :func:`~wtforms.datalist.enum_datalist` for Enum-backed choices,
10
+ replacing ``SelectChoice.from_enum`` and friends. :issue:`922`
11
+ - An Enum ``coerce`` is no longer wrapped, and options default to
12
+ ``member.value``. :issue:`922`
13
+
3
14
  Version 3.3.0b2
4
15
  ---------------
5
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: WTForms
3
- Version: 3.3.0b2
3
+ Version: 3.3.0b3
4
4
  Summary: Form validation and rendering for Python web development.
5
5
  Project-URL: Documentation, https://wtforms.readthedocs.io
6
6
  Project-URL: Changes, https://wtforms.readthedocs.io/changes
@@ -337,9 +337,7 @@ Choice Fields
337
337
  option cannot be applied to your problem you may wish to skip choice
338
338
  validation (see below).
339
339
 
340
- **Select fields with ``<optgroup>``**::
341
-
342
- Use :class:`SelectChoice` to assign an option to an ``<optgroup>``.
340
+ **Select fields with optgroup**::
343
341
 
344
342
  class PastebinEntry(Form):
345
343
  language = SelectField('Programming Language', choices=[
@@ -349,6 +347,8 @@ Choice Fields
349
347
  SelectChoice('text', 'Plain Text'),
350
348
  ])
351
349
 
350
+ Use :class:`SelectChoice` to assign an option to an ``<optgroup>``.
351
+
352
352
  **Select fields with dynamic choice values**::
353
353
 
354
354
  def available_groups():
@@ -400,28 +400,44 @@ Choice Fields
400
400
  **Select fields backed by an Enum**::
401
401
 
402
402
  from enum import Enum
403
+ from wtforms.fields import enum_choices, enum_coerce
403
404
 
404
405
  class Color(Enum):
405
- RED = 1
406
- GREEN = 2
407
- BLUE = 3
406
+ RED = "red"
407
+ GREEN = "green"
408
+ BLUE = "blue"
408
409
 
409
410
  class PaintForm(Form):
410
- color = SelectField(choices=SelectChoice.from_enum(Color), coerce=Color)
411
+ color = SelectField(
412
+ choices=enum_choices(Color),
413
+ coerce=enum_coerce(Color),
414
+ )
415
+
416
+ :func:`~wtforms.fields.enum_choices` builds the option list from
417
+ the Enum members; by default the HTML ``value`` of each option is the
418
+ member's ``value`` (``"red"``, ``"green"``, ...).
419
+ :func:`~wtforms.fields.enum_coerce` returns the matching ``coerce``
420
+ callable that resolves the submitted string back into a member, so
421
+ ``form.color.data`` is a ``Color`` member after submit. Pre-selecting
422
+ works the usual way, with an Enum member: ``PaintForm(color=Color.RED)``.
411
423
 
412
- :meth:`SelectChoice.from_enum` builds the option list from the Enum items;
413
- the HTML ``value`` of each option is the item's ``name``. Passing the
414
- Enum class itself as ``coerce`` installs the matching coercion, so
415
- ``form.color.data`` is a ``Color`` item after submit. Pre-selecting
416
- works the usual way, with an Enum item: ``PaintForm(color=Color.RED)``.
424
+ Use ``by="name"`` on **both** helpers when the Enum value is not a good
425
+ transport identifier — non-unique, non-serialisable, or when you simply
426
+ want ``member.name`` on the wire::
427
+
428
+ class PaintForm(Form):
429
+ color = SelectField(
430
+ choices=enum_choices(Color, by="name"),
431
+ coerce=enum_coerce(Color, by="name"),
432
+ )
417
433
 
418
- By default the option label is ``str(item)`` if the Enum defines its
434
+ By default the option label is ``str(member)`` if the Enum defines its
419
435
  own ``__str__`` (also the case for :class:`enum.StrEnum`), otherwise
420
- ``item.name``. To customise, pass a ``label`` callable taking an Enum
421
- item and returning the label string::
436
+ ``member.name``. To customise, pass a ``label`` callable taking an Enum
437
+ member and returning the label string::
422
438
 
423
- SelectChoice.from_enum(Color, label=lambda item: item.name.title())
424
- # → [SelectChoice('RED', 'Red'), SelectChoice('GREEN', 'Green'), SelectChoice('BLUE', 'Blue')]
439
+ enum_choices(Color, label=lambda member: member.name.title())
440
+ # → [SelectChoice('red', 'Red'), SelectChoice('green', 'Green'), SelectChoice('blue', 'Blue')]
425
441
 
426
442
  **Skipping choice validation**::
427
443
 
@@ -454,6 +470,10 @@ Choice Fields
454
470
  which are not in the given choices list will cause validation on the field
455
471
  to fail.
456
472
 
473
+ .. autofunction:: wtforms.fields.enum_choices
474
+
475
+ .. autofunction:: wtforms.fields.enum_coerce
476
+
457
477
  Submit fields
458
478
  -------------
459
479
 
@@ -580,8 +600,6 @@ Data Lists
580
600
 
581
601
  .. currentmodule:: wtforms
582
602
 
583
- .. autoclass:: DataListChoice
584
-
585
603
  .. class:: DataList(choices=None, *, render_kw=None, widget=None)
586
604
 
587
605
  A :mdn-tag:`datalist` of suggestions. Unlike
@@ -658,6 +676,9 @@ Data Lists
658
676
  :class:`~wtforms.fields.TextAreaField` is silently ignored by the
659
677
  browser.
660
678
 
679
+ .. autoclass:: DataListChoice
680
+
681
+ .. autofunction:: wtforms.datalist.enum_datalist
661
682
 
662
683
  .. currentmodule:: wtforms.fields
663
684
 
@@ -2,6 +2,7 @@ from wtforms import validators
2
2
  from wtforms import widgets
3
3
  from wtforms.datalist import DataList
4
4
  from wtforms.datalist import DataListChoice
5
+ from wtforms.datalist import enum_datalist
5
6
  from wtforms.fields.choices import Choice
6
7
  from wtforms.fields.choices import RadioField
7
8
  from wtforms.fields.choices import SelectChoice
@@ -41,7 +42,7 @@ from wtforms.fields.simple import URLField
41
42
  from wtforms.form import Form
42
43
  from wtforms.validators import ValidationError
43
44
 
44
- __version__ = "3.3.0b2"
45
+ __version__ = "3.3.0b3"
45
46
 
46
47
  __all__ = [
47
48
  "validators",
@@ -86,4 +87,5 @@ __all__ = [
86
87
  "Choice",
87
88
  "SelectChoice",
88
89
  "DataListChoice",
90
+ "enum_datalist",
89
91
  ]
@@ -4,9 +4,10 @@ from dataclasses import field
4
4
 
5
5
  from wtforms import widgets
6
6
  from wtforms._compat import get_signature
7
+ from wtforms.fields.choices import _enum_options
7
8
  from wtforms.fields.choices import Choice
8
9
 
9
- __all__ = ("DataList", "DataListChoice")
10
+ __all__ = ("DataList", "DataListChoice", "enum_datalist")
10
11
 
11
12
 
12
13
  @dataclass
@@ -35,16 +36,6 @@ class DataListChoice:
35
36
  def __iter__(self):
36
37
  return iter((self.value, self.label, self.render_kw))
37
38
 
38
- @classmethod
39
- def from_enum(cls, enum_cls, *, label=None):
40
- """Build a list of choices from an :class:`enum.Enum` class.
41
-
42
- See :meth:`SelectChoice.from_enum` for details.
43
- """
44
- if label is None:
45
- label = str if "__str__" in enum_cls.__dict__ else lambda m: m.name
46
- return [cls(value=m.name, label=label(m)) for m in enum_cls]
47
-
48
39
  @classmethod
49
40
  def from_input(cls, input):
50
41
  """Coerce a value passed by the user into a :class:`DataListChoice`."""
@@ -73,6 +64,20 @@ class DataListChoice:
73
64
  return cls(*input)
74
65
 
75
66
 
67
+ def enum_datalist(enum_cls, *, by="value", label=None):
68
+ """Build a list of :class:`DataListChoice` from an :class:`enum.Enum` class.
69
+
70
+ Same semantics as :func:`~wtforms.fields.enum_choices`: ``by`` selects
71
+ which member attribute becomes the ``<option>`` value (``"value"`` by
72
+ default, ``"name"`` otherwise) and ``label`` defaults to ``str(member)``
73
+ when the Enum defines its own ``__str__``, else ``member.name``.
74
+
75
+ A ``<datalist>`` only suggests values for a free-text field, so there is
76
+ no coercion counterpart — the field stores whatever string is submitted.
77
+ """
78
+ return _enum_options(enum_cls, by, label, DataListChoice)
79
+
80
+
76
81
  class DataList:
77
82
  """A ``<datalist>`` of suggestions attached to a single field.
78
83
 
@@ -1,4 +1,6 @@
1
1
  from wtforms.fields.choices import Choice
2
+ from wtforms.fields.choices import enum_choices
3
+ from wtforms.fields.choices import enum_coerce
2
4
  from wtforms.fields.choices import RadioField
3
5
  from wtforms.fields.choices import SelectChoice
4
6
  from wtforms.fields.choices import SelectField
@@ -46,6 +48,8 @@ __all__ = [
46
48
  "SelectMultipleField",
47
49
  "SelectFieldBase",
48
50
  "RadioField",
51
+ "enum_choices",
52
+ "enum_coerce",
49
53
  "DateTimeField",
50
54
  "DateField",
51
55
  "TimeField",
@@ -2,7 +2,6 @@ import warnings
2
2
  from dataclasses import dataclass
3
3
  from dataclasses import field
4
4
  from dataclasses import replace
5
- from enum import Enum
6
5
  from itertools import groupby
7
6
  from operator import attrgetter
8
7
  from typing import NamedTuple
@@ -21,18 +20,6 @@ __all__ = (
21
20
  )
22
21
 
23
22
 
24
- def _enum_coerce(enum_cls):
25
- def coerce(v):
26
- if isinstance(v, enum_cls):
27
- return v
28
- try:
29
- return enum_cls[v]
30
- except KeyError as e:
31
- raise ValueError(str(e)) from e
32
-
33
- return coerce
34
-
35
-
36
23
  class Choice(NamedTuple):
37
24
  """
38
25
  A rendered option yielded by
@@ -90,19 +77,6 @@ class SelectChoice:
90
77
  def __iter__(self):
91
78
  return iter((self.value, self.label, self.render_kw, self.optgroup))
92
79
 
93
- @classmethod
94
- def from_enum(cls, enum_cls, *, label=None):
95
- """Build a list of choices from an :class:`enum.Enum` class.
96
-
97
- The HTML value of each option is the item ``name``. The label
98
- defaults to ``str(item)`` when the Enum defines its own
99
- ``__str__``, otherwise to ``item.name``. Pass ``label=`` (a
100
- callable taking an item) to override.
101
- """
102
- if label is None:
103
- label = str if "__str__" in enum_cls.__dict__ else lambda m: m.name
104
- return [cls(value=m.name, label=label(m)) for m in enum_cls]
105
-
106
80
  @classmethod
107
81
  def from_input(cls, input, optgroup=None):
108
82
  """Coerce a value passed by the user via ``choices=...`` into a
@@ -141,6 +115,60 @@ class SelectChoice:
141
115
  return cls(*input, optgroup=optgroup)
142
116
 
143
117
 
118
+ def _enum_options(enum_cls, by, label, factory):
119
+ if by not in ("value", "name"):
120
+ raise ValueError(f"by must be 'value' or 'name', got {by!r}")
121
+ if label is None:
122
+ label = str if "__str__" in enum_cls.__dict__ else lambda m: m.name
123
+ return [factory(value=getattr(m, by), label=label(m)) for m in enum_cls]
124
+
125
+
126
+ def enum_choices(enum_cls, *, by="value", label=None):
127
+ """Build a list of :class:`SelectChoice` from an :class:`enum.Enum` class.
128
+
129
+ ``by`` selects which member attribute becomes the HTML ``value=``:
130
+ ``"value"`` (the default) emits ``member.value``, ``"name"`` emits
131
+ ``member.name``. Use ``"name"`` for enums whose values are not good
132
+ transport identifiers (non-string, non-unique or non-serialisable).
133
+
134
+ The label defaults to ``str(member)`` when the Enum defines its own
135
+ ``__str__``, otherwise to ``member.name``. Pass ``label=`` (a callable
136
+ taking a member) to override.
137
+
138
+ Pair with :func:`enum_coerce` using the same ``by`` to round-trip the
139
+ submitted string back into a member.
140
+ """
141
+ return _enum_options(enum_cls, by, label, SelectChoice)
142
+
143
+
144
+ def enum_coerce(enum_cls, *, by="value"):
145
+ """Return a ``coerce`` callable mapping a submitted string back to an
146
+ :class:`enum.Enum` member.
147
+
148
+ ``by`` must match the one passed to :func:`enum_choices`: ``"value"``
149
+ (the default) resolves ``str(member.value)`` — so ``IntEnum`` values
150
+ round-trip too — and ``"name"`` resolves ``member.name``. Already-coerced
151
+ members pass through unchanged.
152
+ """
153
+ if by not in ("value", "name"):
154
+ raise ValueError(f"by must be 'value' or 'name', got {by!r}")
155
+
156
+ def coerce(v):
157
+ if isinstance(v, enum_cls):
158
+ return v
159
+
160
+ try:
161
+ if by == "value":
162
+ by_value = {str(m.value): m for m in enum_cls}
163
+ return by_value[str(v)]
164
+ else:
165
+ return enum_cls[v]
166
+ except KeyError as e:
167
+ raise ValueError(str(e)) from e
168
+
169
+ return coerce
170
+
171
+
144
172
  def _normalize_iter_choice(choice):
145
173
  """Coerce a value yielded by :meth:`SelectFieldBase.iter_choices` or
146
174
  :meth:`SelectFieldBase.iter_groups` into a :class:`Choice`.
@@ -334,8 +362,6 @@ class SelectField(SelectFieldBase):
334
362
  **kwargs,
335
363
  ):
336
364
  super().__init__(label, validators, **kwargs)
337
- if isinstance(coerce, type) and issubclass(coerce, Enum):
338
- coerce = _enum_coerce(coerce)
339
365
  self.coerce = coerce
340
366
  if callable(choices):
341
367
  self._choices_callable = choices
@@ -389,8 +415,8 @@ class SelectField(SelectFieldBase):
389
415
  ],
390
416
  )
391
417
 
392
- def post_process(self):
393
- super().post_process()
418
+ def post_process(self, formdata=None):
419
+ super().post_process(formdata)
394
420
  if self._choices_callable is not None:
395
421
  self.choices = self._invoke_choices_callback(self._choices_callable)
396
422
 
@@ -350,13 +350,16 @@ class Field:
350
350
  except ValueError as e:
351
351
  self.process_errors.append(e.args[0])
352
352
 
353
- def post_process(self):
353
+ def post_process(self, formdata=None):
354
354
  """Hook called after every field in the enclosing form has been processed.
355
355
 
356
356
  Override this when a field needs to read other fields' processed data,
357
357
  for example to resolve dynamic choices that depend on the form state.
358
358
  The base implementation resolves any inline :class:`~wtforms.DataList`
359
359
  attached to the field.
360
+
361
+ :param formdata: The resolved formdata the enclosing form was bound
362
+ with, or ``None`` for non-formdata cycles.
360
363
  """
361
364
  if self._datalist is not None and not isinstance(self._datalist, str):
362
365
  self._datalist._resolve(self)
@@ -70,8 +70,8 @@ class FormField(Field):
70
70
  meta={"_parent_form": self._form},
71
71
  )
72
72
 
73
- def post_process(self):
74
- self.form.post_process()
73
+ def post_process(self, formdata=None):
74
+ self.form.post_process(formdata)
75
75
 
76
76
  def validate(self, form, extra_validators=()):
77
77
  if extra_validators:
@@ -114,9 +114,9 @@ class FieldList(Field):
114
114
  if k.isdigit():
115
115
  yield int(k)
116
116
 
117
- def post_process(self):
117
+ def post_process(self, formdata=None):
118
118
  for entry in self.entries:
119
- entry.post_process()
119
+ entry.post_process(formdata)
120
120
 
121
121
  def validate(self, form, extra_validators=()):
122
122
  """
@@ -131,15 +131,20 @@ class BaseForm:
131
131
  field.process(formdata, data, extra_filters=field_extra_filters)
132
132
 
133
133
  if self._parent_form is None:
134
- self.post_process()
134
+ self.post_process(formdata)
135
135
 
136
- def post_process(self):
136
+ def post_process(self, formdata=None):
137
137
  """Hook called at the end of :meth:`process` on the root form.
138
138
 
139
139
  Runs the :meth:`~fields.Field.post_process` hook on every field, after
140
140
  all fields have been processed. Override this on a form subclass to add
141
- cross-field finalization logic; call ``super().post_process()`` to keep
142
- per-field hooks running.
141
+ cross-field finalization logic; call ``super().post_process(formdata)``
142
+ to keep per-field hooks running.
143
+
144
+ :param formdata: The resolved formdata the form was bound with
145
+ (already passed through :meth:`~meta.DefaultMeta.wrap_formdata`),
146
+ or ``None`` for non-formdata cycles. Forwarded to every nested
147
+ ``post_process``.
143
148
 
144
149
  ``post_process`` is only triggered automatically on the root form. Forms
145
150
  nested inside a :class:`~fields.FormField` (or via :class:`~fields.FieldList`
@@ -148,7 +153,7 @@ class BaseForm:
148
153
  ``post_process`` runs exactly once per processing cycle.
149
154
  """
150
155
  for field in self._fields.values():
151
- field.post_process()
156
+ field.post_process(formdata)
152
157
 
153
158
  def validate(self, extra_validators=None):
154
159
  """
@@ -118,15 +118,15 @@ def test_post_process_propagates_through_form_field():
118
118
  class Inner(Form):
119
119
  x = StringField()
120
120
 
121
- def post_process(self):
122
- super().post_process()
121
+ def post_process(self, formdata=None):
122
+ super().post_process(formdata)
123
123
  captured.append(("inner", self.x.data))
124
124
 
125
125
  class Outer(Form):
126
126
  block = FormField(Inner)
127
127
 
128
- def post_process(self):
129
- super().post_process()
128
+ def post_process(self, formdata=None):
129
+ super().post_process(formdata)
130
130
  captured.append(("outer", self.block.form.x.data))
131
131
 
132
132
  Outer(DummyPostData({"block-x": "v"}))
@@ -182,9 +182,9 @@ def test_post_process_mutation_propagates_top_down():
182
182
  tenant = StringField()
183
183
  block = FormField(Inner)
184
184
 
185
- def post_process(self):
185
+ def post_process(self, formdata=None):
186
186
  self.tenant.data = (self.tenant.data or "").upper()
187
- super().post_process()
187
+ super().post_process(formdata)
188
188
 
189
189
  Outer(DummyPostData({"tenant": "acme", "block-item": "a"}))
190
190
  assert captured == ["ACME"]