WTForms 3.3.0b3__tar.gz → 3.3.0b4__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.0b3 → wtforms-3.3.0b4}/CHANGES.rst +28 -0
  2. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/PKG-INFO +3 -3
  3. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/fields.rst +26 -17
  4. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/forms.rst +2 -0
  5. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/pyproject.toml +4 -4
  6. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/__init__.py +1 -1
  7. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/datalist.py +18 -13
  8. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/fields/choices.py +79 -30
  9. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/fields/core.py +5 -2
  10. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/fields/datetime.py +22 -25
  11. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/fields/list.py +13 -1
  12. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/fields/numeric.py +18 -16
  13. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/form.py +19 -13
  14. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_date.py +13 -0
  15. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_datetime.py +13 -0
  16. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_datetimelocal.py +4 -4
  17. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_decimal.py +10 -0
  18. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_float.py +13 -0
  19. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_form.py +12 -11
  20. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_integer.py +13 -0
  21. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_list.py +77 -0
  22. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_radio.py +17 -0
  23. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_select.py +147 -7
  24. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_selectmultiple.py +29 -0
  25. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_time.py +13 -0
  26. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_week.py +13 -0
  27. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/test_datalist.py +56 -0
  28. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/test_form.py +22 -0
  29. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/.gitignore +0 -0
  30. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/LICENSE.rst +0 -0
  31. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/README.rst +0 -0
  32. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/Makefile +0 -0
  33. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/_static/logo_joined.svg +0 -0
  34. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/_static/logo_split.svg +0 -0
  35. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/_static/logo_square.svg +0 -0
  36. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/changes.rst +0 -0
  37. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/conf.py +0 -0
  38. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/contributing.rst +0 -0
  39. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/crash_course.rst +0 -0
  40. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/csrf.rst +0 -0
  41. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/ext.rst +0 -0
  42. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/faq.rst +0 -0
  43. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/i18n.rst +0 -0
  44. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/index.rst +0 -0
  45. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/license.rst +0 -0
  46. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/make.bat +0 -0
  47. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/meta.rst +0 -0
  48. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/specific_problems.rst +0 -0
  49. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/validators.rst +0 -0
  50. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/whats_new.rst +0 -0
  51. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/docs/widgets.rst +0 -0
  52. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/hatch_build.py +0 -0
  53. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/_compat.py +0 -0
  54. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/csrf/__init__.py +0 -0
  55. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/csrf/core.py +0 -0
  56. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/csrf/session.py +0 -0
  57. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/fields/__init__.py +0 -0
  58. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/fields/form.py +0 -0
  59. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/fields/simple.py +0 -0
  60. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/i18n.py +0 -0
  61. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/README.md +0 -0
  62. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ar/LC_MESSAGES/wtforms.mo +0 -0
  63. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ar/LC_MESSAGES/wtforms.po +0 -0
  64. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/bg/LC_MESSAGES/wtforms.mo +0 -0
  65. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/bg/LC_MESSAGES/wtforms.po +0 -0
  66. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ca/LC_MESSAGES/wtforms.mo +0 -0
  67. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ca/LC_MESSAGES/wtforms.po +0 -0
  68. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/cs_CZ/LC_MESSAGES/wtforms.mo +0 -0
  69. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/cs_CZ/LC_MESSAGES/wtforms.po +0 -0
  70. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/cy/LC_MESSAGES/wtforms.mo +0 -0
  71. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/cy/LC_MESSAGES/wtforms.po +0 -0
  72. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/de/LC_MESSAGES/wtforms.mo +0 -0
  73. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/de/LC_MESSAGES/wtforms.po +0 -0
  74. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/de_CH/LC_MESSAGES/wtforms.mo +0 -0
  75. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/de_CH/LC_MESSAGES/wtforms.po +0 -0
  76. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/el/LC_MESSAGES/wtforms.mo +0 -0
  77. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/el/LC_MESSAGES/wtforms.po +0 -0
  78. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/en/LC_MESSAGES/wtforms.mo +0 -0
  79. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/en/LC_MESSAGES/wtforms.po +0 -0
  80. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/es/LC_MESSAGES/wtforms.mo +0 -0
  81. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/es/LC_MESSAGES/wtforms.po +0 -0
  82. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/et/LC_MESSAGES/wtforms.mo +0 -0
  83. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/et/LC_MESSAGES/wtforms.po +0 -0
  84. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/fa/LC_MESSAGES/wtforms.mo +0 -0
  85. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/fa/LC_MESSAGES/wtforms.po +0 -0
  86. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/fi/LC_MESSAGES/wtforms.mo +0 -0
  87. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/fi/LC_MESSAGES/wtforms.po +0 -0
  88. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/fr/LC_MESSAGES/wtforms.mo +0 -0
  89. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/fr/LC_MESSAGES/wtforms.po +0 -0
  90. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/he/LC_MESSAGES/wtforms.mo +0 -0
  91. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/he/LC_MESSAGES/wtforms.po +0 -0
  92. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/hu/LC_MESSAGES/wtforms.mo +0 -0
  93. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/hu/LC_MESSAGES/wtforms.po +0 -0
  94. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/it/LC_MESSAGES/wtforms.mo +0 -0
  95. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/it/LC_MESSAGES/wtforms.po +0 -0
  96. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ja/LC_MESSAGES/wtforms.mo +0 -0
  97. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ja/LC_MESSAGES/wtforms.po +0 -0
  98. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/kk/LC_MESSAGES/wtforms.mo +0 -0
  99. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/kk/LC_MESSAGES/wtforms.po +0 -0
  100. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ko/LC_MESSAGES/wtforms.mo +0 -0
  101. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ko/LC_MESSAGES/wtforms.po +0 -0
  102. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/nb/LC_MESSAGES/wtforms.mo +0 -0
  103. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/nb/LC_MESSAGES/wtforms.po +0 -0
  104. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/nl/LC_MESSAGES/wtforms.mo +0 -0
  105. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/nl/LC_MESSAGES/wtforms.po +0 -0
  106. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/pl/LC_MESSAGES/wtforms.mo +0 -0
  107. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/pl/LC_MESSAGES/wtforms.po +0 -0
  108. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/pt/LC_MESSAGES/wtforms.mo +0 -0
  109. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/pt/LC_MESSAGES/wtforms.po +0 -0
  110. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ro/LC_MESSAGES/wtforms.mo +0 -0
  111. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ro/LC_MESSAGES/wtforms.po +0 -0
  112. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ru/LC_MESSAGES/wtforms.mo +0 -0
  113. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ru/LC_MESSAGES/wtforms.po +0 -0
  114. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/sk/LC_MESSAGES/wtforms.mo +0 -0
  115. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/sk/LC_MESSAGES/wtforms.po +0 -0
  116. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/sr/LC_MESSAGES/wtforms.mo +0 -0
  117. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/sr/LC_MESSAGES/wtforms.po +0 -0
  118. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/sv/LC_MESSAGES/wtforms.mo +0 -0
  119. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/sv/LC_MESSAGES/wtforms.po +0 -0
  120. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ta/LC_MESSAGES/wtforms.mo +0 -0
  121. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/ta/LC_MESSAGES/wtforms.po +0 -0
  122. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/tr/LC_MESSAGES/wtforms.mo +0 -0
  123. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/tr/LC_MESSAGES/wtforms.po +0 -0
  124. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/uk/LC_MESSAGES/wtforms.mo +0 -0
  125. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/uk/LC_MESSAGES/wtforms.po +0 -0
  126. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/wtforms.pot +0 -0
  127. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/zh/LC_MESSAGES/wtforms.mo +0 -0
  128. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/zh/LC_MESSAGES/wtforms.po +0 -0
  129. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/zh_TW/LC_MESSAGES/wtforms.mo +0 -0
  130. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/locale/zh_TW/LC_MESSAGES/wtforms.po +0 -0
  131. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/meta.py +0 -0
  132. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/utils.py +0 -0
  133. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/validators.py +0 -0
  134. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/widgets/__init__.py +0 -0
  135. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/src/wtforms/widgets/core.py +0 -0
  136. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/__init__.py +0 -0
  137. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/common.py +0 -0
  138. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/conftest.py +0 -0
  139. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/__init__.py +0 -0
  140. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_boolean.py +0 -0
  141. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_button.py +0 -0
  142. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_color.py +0 -0
  143. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_custom.py +0 -0
  144. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_defaults.py +0 -0
  145. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_field.py +0 -0
  146. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_file.py +0 -0
  147. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_filters.py +0 -0
  148. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_flags.py +0 -0
  149. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_hidden.py +0 -0
  150. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_html5.py +0 -0
  151. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_label.py +0 -0
  152. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_month.py +0 -0
  153. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_password.py +0 -0
  154. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_string.py +0 -0
  155. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_submit.py +0 -0
  156. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/fields/test_textarea.py +0 -0
  157. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/test_csrf.py +0 -0
  158. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/test_i18n.py +0 -0
  159. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/test_locale_babel.py +0 -0
  160. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/test_webob.py +0 -0
  161. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/test_widgets.py +0 -0
  162. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/__init__.py +0 -0
  163. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_anyof.py +0 -0
  164. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_data_required.py +0 -0
  165. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_date_range.py +0 -0
  166. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_disabled.py +0 -0
  167. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_email.py +0 -0
  168. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_equal_to.py +0 -0
  169. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_input_required.py +0 -0
  170. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_ipaddress.py +0 -0
  171. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_lazy_proxy.py +0 -0
  172. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_length.py +0 -0
  173. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_mac_address.py +0 -0
  174. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_noneof.py +0 -0
  175. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_number_range.py +0 -0
  176. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_optional.py +0 -0
  177. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_readonly.py +0 -0
  178. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_regexp.py +0 -0
  179. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_url.py +0 -0
  180. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_uuid.py +0 -0
  181. {wtforms-3.3.0b3 → wtforms-3.3.0b4}/tests/validators/test_validators.py +0 -0
@@ -1,5 +1,33 @@
1
1
  .. currentmodule:: wtforms
2
2
 
3
+ Version 3.3.0b4
4
+ ---------------
5
+
6
+ Released 2026-10-23
7
+
8
+ - Drop support for Python 3.10. Add support for Python 3.15.
9
+ - :func:`fields.enum_choices` and :func:`~wtforms.datalist.enum_datalist`
10
+ label :class:`enum.IntEnum` members with their name on PyPy too, instead of
11
+ their value.
12
+ - :class:`~datalist.DataList` ``choices`` accepts the shorthand
13
+ ``{value: label}`` dict syntax. :issue:`886`
14
+ - Default ``invalid_value_message`` and ``invalid_choice_message`` are
15
+ translated when the error is raised, not when the field is built. Forms can
16
+ be built again outside of a translation context.
17
+ - ``choices`` callables on :class:`~fields.SelectField` and
18
+ :class:`~datalist.DataList` run on the first read and their result is
19
+ cached. Calling :meth:`~form.BaseForm.post_process` again refreshes them.
20
+ This also applies to entries added with
21
+ :meth:`~fields.FieldList.append_entry` and
22
+ :meth:`~fields.FieldList.insert_entry`.
23
+ - Passing a callable to ``SelectFieldBase._choices_from_input`` is
24
+ deprecated.
25
+ - Numeric, date, time and select fields report a validation error instead of
26
+ raising a :class:`TypeError` when the input is not a string.
27
+ - The labels of :class:`~fields.FieldList` entries point to the right field
28
+ after :meth:`~fields.FieldList.insert_entry` or
29
+ :meth:`~fields.FieldList.pop_entry` renumbers the entries.
30
+
3
31
  Version 3.3.0b3
4
32
  ---------------
5
33
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: WTForms
3
- Version: 3.3.0b3
3
+ Version: 3.3.0b4
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
@@ -44,7 +44,7 @@ Classifier: License :: OSI Approved :: BSD License
44
44
  Classifier: Operating System :: OS Independent
45
45
  Classifier: Programming Language :: Python
46
46
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
47
- Requires-Python: >=3.10
47
+ Requires-Python: >=3.11
48
48
  Requires-Dist: markupsafe>=1.1.1
49
49
  Provides-Extra: email
50
50
  Requires-Dist: email-validator>=2.0; extra == 'email'
@@ -67,6 +67,7 @@ The Field base class
67
67
  .. automethod:: process(formdata [, data])
68
68
  .. automethod:: process_data
69
69
  .. automethod:: process_formdata
70
+ .. automethod:: post_process
70
71
  .. attribute:: data
71
72
 
72
73
  Contains the resulting (sanitized) value of calling either of the
@@ -329,8 +330,8 @@ Choice Fields
329
330
  SelectChoice('text', 'Plain Text'),
330
331
  ])
331
332
 
332
- Note that the `choices` keyword is evaluated each time the form is
333
- instantiated, so callables passed as `choices` are re-evaluated per instance.
333
+ A callable passed as `choices` is resolved separately for each form
334
+ instance, the first time its choices are read.
334
335
  For dynamic options, either pass a callable to `choices` or assign
335
336
  `field.choices` after instantiation. Any submitted choices which are not in
336
337
  the given choices list will cause validation on the field to fail. If this
@@ -361,17 +362,22 @@ Choice Fields
361
362
  user = User.query.get(id)
362
363
  form = UserDetails(request.POST, obj=user)
363
364
 
364
- Note we passed a callable to `choices` rather than a static list; it is
365
- invoked each time the form is instantiated, producing fresh options per
366
- request. If the choices depend on data only available in the view, you can
367
- instead assign `form.group_id.choices` after instantiation. The `coerce`
368
- keyword arg to :class:`~wtforms.fields.SelectField` says that we use
369
- :func:`int()` to coerce form data. The default coerce is :func:`str()`.
365
+ Note we passed a callable to `choices` rather than a static list; it
366
+ produces fresh options per request. Its result is cached. Calling
367
+ :meth:`~wtforms.form.BaseForm.post_process` again refreshes it.
368
+
369
+ If the choices depend on data only available in the view, you can instead
370
+ assign `form.group_id.choices` after instantiation. A list assigned this
371
+ way replaces the callable.
372
+
373
+ The `coerce` keyword arg to :class:`~wtforms.fields.SelectField` says
374
+ that we use :func:`int()` to coerce form data. The default coerce is
375
+ :func:`str()`.
370
376
 
371
377
  The callable may optionally accept ``(form, field)`` as positional
372
- arguments. When this signature is used, the callable is invoked after the
373
- whole form has been processed, so it can read ``field.data`` and the data
374
- of any other field on the form::
378
+ arguments. The callable is invoked when the choices are read, normally
379
+ after the whole form has been processed, so it can read ``field.data`` and
380
+ the data of any other field on the form::
375
381
 
376
382
  def available_groups(form, field):
377
383
  return [SelectChoice(g.id, g.name) for g in form.tenant.data.groups]
@@ -610,12 +616,15 @@ Data Lists
610
616
 
611
617
  ``choices`` is either a list of :class:`~wtforms.DataListChoice`
612
618
  (or plain strings, in which case the string is used as both value
613
- and label), or a callable invoked at render time. The callable
614
- may take no argument (``fn()``) for a static list, or ``(field)``
615
- to adapt the suggestions to the current field value at each
616
- render — convenient for server-side filtering::
619
+ and label), a shorthand ``{value: label}`` dict, or a callable. The
620
+ callable runs when the datalist is rendered, after the whole form has been
621
+ processed. Its result is cached until the next
622
+ :meth:`~wtforms.form.BaseForm.post_process`. The callable may
623
+ take no argument (``fn()``) for a static list, or ``(form, field)``
624
+ to adapt the suggestions to the form data, for example the current
625
+ field value. This is convenient for server-side filtering::
617
626
 
618
- DataList(lambda field: search(field.data))
627
+ DataList(lambda form, field: search(field.data))
619
628
 
620
629
  **Inline DataList owned by a field**
621
630
 
@@ -642,7 +651,7 @@ Data Lists
642
651
  class F(Form):
643
652
  items = FieldList(
644
653
  StringField(
645
- datalist=DataList(lambda field: suggest(field.data))
654
+ datalist=DataList(lambda form, field: suggest(field.data))
646
655
  ),
647
656
  min_entries=3,
648
657
  )
@@ -294,6 +294,8 @@ fields.
294
294
  this to provide form data to the enclosed fields. Accessing the field's
295
295
  data before calling process is not recommended.
296
296
 
297
+ .. automethod:: post_process
298
+
297
299
  .. automethod:: validate
298
300
 
299
301
  .. automethod:: __iter__
@@ -13,7 +13,7 @@ classifiers = [
13
13
  "Programming Language :: Python",
14
14
  "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
15
15
  ]
16
- requires-python = ">=3.10"
16
+ requires-python = ">=3.11"
17
17
  dependencies = [
18
18
  "MarkupSafe>=1.1.1",
19
19
  ]
@@ -49,12 +49,12 @@ dev = [
49
49
  env_list = [
50
50
  "style",
51
51
  "minversions",
52
+ "py315",
52
53
  "py314",
53
54
  "py313",
54
55
  "py312",
55
56
  "py311",
56
- "py310",
57
- "pypy310",
57
+ "pypy311",
58
58
  "docs",
59
59
  ]
60
60
  skip_missing_interpreters = true
@@ -86,7 +86,7 @@ dependency_groups = ["docs"]
86
86
  commands = [["sphinx-build", "-W", "-b", "html", "-d", "{envtmpdir}/doctrees", "docs", "{envtmpdir}/html"]]
87
87
 
88
88
  [tool.tox.env.minversions]
89
- base_python = ["py3.10"]
89
+ base_python = ["py3.11"]
90
90
  uv_resolution = "lowest-direct"
91
91
  dependency_groups = ["dev"]
92
92
  commands = [
@@ -42,7 +42,7 @@ from wtforms.fields.simple import URLField
42
42
  from wtforms.form import Form
43
43
  from wtforms.validators import ValidationError
44
44
 
45
- __version__ = "3.3.0b3"
45
+ __version__ = "3.3.0b4"
46
46
 
47
47
  __all__ = [
48
48
  "validators",
@@ -3,8 +3,8 @@ from dataclasses import dataclass
3
3
  from dataclasses import field
4
4
 
5
5
  from wtforms import widgets
6
- from wtforms._compat import get_signature
7
6
  from wtforms.fields.choices import _enum_options
7
+ from wtforms.fields.choices import _invoke_choices_callable
8
8
  from wtforms.fields.choices import Choice
9
9
 
10
10
  __all__ = ("DataList", "DataListChoice", "enum_datalist")
@@ -70,7 +70,8 @@ def enum_datalist(enum_cls, *, by="value", label=None):
70
70
  Same semantics as :func:`~wtforms.fields.enum_choices`: ``by`` selects
71
71
  which member attribute becomes the ``<option>`` value (``"value"`` by
72
72
  default, ``"name"`` otherwise) and ``label`` defaults to ``str(member)``
73
- when the Enum defines its own ``__str__``, else ``member.name``.
73
+ when the Enum defines its own ``__str__`` or is a :class:`enum.StrEnum`,
74
+ else ``member.name``.
74
75
 
75
76
  A ``<datalist>`` only suggests values for a free-text field, so there is
76
77
  no coercion counterpart — the field stores whatever string is submitted.
@@ -91,6 +92,7 @@ class DataList:
91
92
  def __init__(self, choices=None, *, render_kw=None, widget=None):
92
93
  self._raw_choices = choices
93
94
  self._choices = None if callable(choices) else choices
95
+ self._field = None
94
96
  self.render_kw = render_kw or {}
95
97
  if widget is not None:
96
98
  self.widget = widget
@@ -100,27 +102,30 @@ class DataList:
100
102
  clone = DataList.__new__(DataList)
101
103
  clone._raw_choices = self._raw_choices
102
104
  clone._choices = None if callable(self._raw_choices) else self._raw_choices
105
+ clone._field = None
103
106
  clone.render_kw = self.render_kw
104
107
  clone.widget = self.widget
105
108
  clone.id = id
106
109
  return clone
107
110
 
108
- def _resolve(self, field):
109
- raw = self._raw_choices
110
- if not callable(raw):
111
- return
112
- try:
113
- sig = get_signature(raw)
114
- sig.bind(field._form, field)
115
- except TypeError:
116
- self._choices = raw()
117
- return
118
- self._choices = raw(field._form, field)
111
+ def _invalidate(self, field):
112
+ """Mark the choices for a lazy refresh on the next :meth:`iter_choices`."""
113
+ if callable(self._raw_choices):
114
+ self._field = field
119
115
 
120
116
  def iter_choices(self, field=None):
117
+ if (bound := self._field) is not None:
118
+ self._field = None
119
+ self._choices = _invoke_choices_callable(
120
+ self._raw_choices, bound._form, bound
121
+ )
121
122
  raw = self._choices
122
123
  if raw is None:
123
124
  return []
125
+ if isinstance(raw, dict):
126
+ return [
127
+ DataListChoice(value=value, label=label) for value, label in raw.items()
128
+ ]
124
129
  return [DataListChoice.from_input(item) for item in raw]
125
130
 
126
131
  def __call__(self, field=None, **kwargs):
@@ -2,6 +2,7 @@ import warnings
2
2
  from dataclasses import dataclass
3
3
  from dataclasses import field
4
4
  from dataclasses import replace
5
+ from enum import StrEnum
5
6
  from itertools import groupby
6
7
  from operator import attrgetter
7
8
  from typing import NamedTuple
@@ -115,11 +116,35 @@ class SelectChoice:
115
116
  return cls(*input, optgroup=optgroup)
116
117
 
117
118
 
119
+ def _has_custom_str(enum_cls):
120
+ """Tell whether the enum or one of its user bases defines ``__str__``."""
121
+ stdlib = [
122
+ cls.__str__
123
+ for cls in enum_cls.__mro__
124
+ if cls.__module__ in ("enum", "builtins")
125
+ ]
126
+ return enum_cls.__str__ not in stdlib
127
+
128
+
129
+ def _invoke_choices_callable(cb, form, field):
130
+ """Call a ``choices`` callable with ``(form, field)``, or with no argument."""
131
+ try:
132
+ sig = get_signature(cb)
133
+ except (ValueError, TypeError):
134
+ return cb()
135
+ try:
136
+ sig.bind(form, field)
137
+ except TypeError:
138
+ return cb()
139
+ return cb(form, field)
140
+
141
+
118
142
  def _enum_options(enum_cls, by, label, factory):
119
143
  if by not in ("value", "name"):
120
144
  raise ValueError(f"by must be 'value' or 'name', got {by!r}")
121
145
  if label is None:
122
- label = str if "__str__" in enum_cls.__dict__ else lambda m: m.name
146
+ use_str = issubclass(enum_cls, StrEnum) or _has_custom_str(enum_cls)
147
+ label = str if use_str else lambda m: m.name
123
148
  return [factory(value=getattr(m, by), label=label(m)) for m in enum_cls]
124
149
 
125
150
 
@@ -132,8 +157,8 @@ def enum_choices(enum_cls, *, by="value", label=None):
132
157
  transport identifiers (non-string, non-unique or non-serialisable).
133
158
 
134
159
  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.
160
+ ``__str__`` or is a :class:`enum.StrEnum`, otherwise to ``member.name``.
161
+ Pass ``label=`` (a callable taking a member) to override.
137
162
 
138
163
  Pair with :func:`enum_coerce` using the same ``by`` to round-trip the
139
164
  submitted string back into a member.
@@ -264,6 +289,13 @@ class SelectFieldBase(Field):
264
289
  def _choices_from_input(self, choices):
265
290
  """Parse the user-supplied ``choices`` into a list of :class:`SelectChoice`."""
266
291
  if callable(choices):
292
+ warnings.warn(
293
+ "Passing a callable to _choices_from_input() is deprecated; "
294
+ "resolve the callable before the call. Will be removed in "
295
+ "WTForms 3.4.",
296
+ DeprecationWarning,
297
+ stacklevel=2,
298
+ )
267
299
  choices = self._invoke_choices_callback(choices)
268
300
 
269
301
  if choices is None:
@@ -332,15 +364,7 @@ class SelectFieldBase(Field):
332
364
  return
333
365
 
334
366
  def _invoke_choices_callback(self, cb):
335
- try:
336
- sig = get_signature(cb)
337
- except (ValueError, TypeError):
338
- return cb()
339
- try:
340
- sig.bind(self._form, self)
341
- except TypeError:
342
- return cb()
343
- return cb(self._form, self)
367
+ return _invoke_choices_callable(cb, self._form, self)
344
368
 
345
369
  class _Option(Field):
346
370
  def _value(self):
@@ -365,23 +389,40 @@ class SelectField(SelectFieldBase):
365
389
  self.coerce = coerce
366
390
  if callable(choices):
367
391
  self._choices_callable = choices
368
- self.choices = None
392
+ self._choices_stale = True
393
+ self._choices = None
369
394
  else:
370
395
  self._choices_callable = None
396
+ self._choices_stale = False
371
397
  if choices is None:
372
- self.choices = None
398
+ self._choices = None
373
399
  else:
374
400
  self._warn_legacy_choices(choices)
375
- self.choices = (
401
+ self._choices = (
376
402
  dict(choices) if isinstance(choices, dict) else list(choices)
377
403
  )
378
404
  self.validate_choice = validate_choice
379
- self.invalid_value_message = invalid_value_message or self.gettext(
380
- "Invalid Choice: could not coerce."
381
- )
382
- self.invalid_choice_message = invalid_choice_message or self.gettext(
383
- "Not a valid choice."
384
- )
405
+ self.invalid_value_message = invalid_value_message
406
+ self.invalid_choice_message = invalid_choice_message
407
+
408
+ @property
409
+ def choices(self):
410
+ """The field's choices, resolved from the callable on first read."""
411
+ if self._choices_stale:
412
+ self._choices_stale = False
413
+ self._choices = self._invoke_choices_callback(self._choices_callable)
414
+ return self._choices
415
+
416
+ @choices.setter
417
+ def choices(self, value):
418
+ if callable(value):
419
+ self._choices_callable = value
420
+ self._choices_stale = True
421
+ self._choices = None
422
+ else:
423
+ self._choices_callable = None
424
+ self._choices_stale = False
425
+ self._choices = value
385
426
 
386
427
  def iter_choices(self):
387
428
  choices = self._choices_from_input(self.choices) or []
@@ -418,7 +459,7 @@ class SelectField(SelectFieldBase):
418
459
  def post_process(self, formdata=None):
419
460
  super().post_process(formdata)
420
461
  if self._choices_callable is not None:
421
- self.choices = self._invoke_choices_callback(self._choices_callable)
462
+ self._choices_stale = True
422
463
 
423
464
  def process_data(self, value):
424
465
  try:
@@ -433,8 +474,11 @@ class SelectField(SelectFieldBase):
433
474
 
434
475
  try:
435
476
  self.data = self.coerce(valuelist[0])
436
- except ValueError as exc:
437
- raise ValueError(self.invalid_value_message) from exc
477
+ except (ValueError, TypeError) as exc:
478
+ raise ValueError(
479
+ self.invalid_value_message
480
+ or self.gettext("Invalid Choice: could not coerce.")
481
+ ) from exc
438
482
 
439
483
  def pre_validate(self, form):
440
484
  if self.process_errors:
@@ -447,7 +491,9 @@ class SelectField(SelectFieldBase):
447
491
  raise TypeError(self.gettext("Choices cannot be None."))
448
492
 
449
493
  if not any(choice.selected for choice in self._iter_choices_normalized()):
450
- raise ValidationError(self.invalid_choice_message)
494
+ raise ValidationError(
495
+ self.invalid_choice_message or self.gettext("Not a valid choice.")
496
+ )
451
497
 
452
498
 
453
499
  class SelectMultipleField(SelectField):
@@ -483,9 +529,7 @@ class SelectMultipleField(SelectField):
483
529
  validate_choice=validate_choice,
484
530
  **kwargs,
485
531
  )
486
- self.invalid_value_message = invalid_value_message or self.gettext(
487
- "Invalid choice(s): one or more data inputs could not be coerced."
488
- )
532
+ self.invalid_value_message = invalid_value_message
489
533
  self.invalid_choice_message = invalid_choice_message
490
534
 
491
535
  def iter_choices(self):
@@ -527,8 +571,13 @@ class SelectMultipleField(SelectField):
527
571
  def process_formdata(self, valuelist):
528
572
  try:
529
573
  self.data = list(self.coerce(x) for x in valuelist)
530
- except ValueError as exc:
531
- raise ValueError(self.invalid_value_message) from exc
574
+ except (ValueError, TypeError) as exc:
575
+ raise ValueError(
576
+ self.invalid_value_message
577
+ or self.gettext(
578
+ "Invalid choice(s): one or more data inputs could not be coerced."
579
+ )
580
+ ) from exc
532
581
 
533
582
  def pre_validate(self, form):
534
583
  if self.process_errors:
@@ -355,14 +355,17 @@ class Field:
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
- The base implementation resolves any inline :class:`~wtforms.DataList`
358
+ The base implementation refreshes any inline :class:`~wtforms.DataList`
359
359
  attached to the field.
360
360
 
361
+ It may run again after the form data changed. See
362
+ :meth:`~wtforms.form.BaseForm.post_process`.
363
+
361
364
  :param formdata: The resolved formdata the enclosing form was bound
362
365
  with, or ``None`` for non-formdata cycles.
363
366
  """
364
367
  if self._datalist is not None and not isinstance(self._datalist, str):
365
- self._datalist._resolve(self)
368
+ self._datalist._invalidate(self)
366
369
 
367
370
  def process_data(self, value):
368
371
  """
@@ -23,7 +23,7 @@ class DateTimeField(Field):
23
23
  format will be accepted, and the first format in the list will be used
24
24
  to produce HTML values.
25
25
 
26
- .. deprecated:: 3.2.3
26
+ .. deprecated:: 3.3.0
27
27
  ``DateTimeField`` renders ``<input type="datetime">``, which is
28
28
  obsolete. Use :class:`DateTimeLocalField` instead. ``DateTimeField``
29
29
  will be removed in WTForms 3.4.
@@ -50,9 +50,7 @@ class DateTimeField(Field):
50
50
  )
51
51
  self.format = format if isinstance(format, list) else [format]
52
52
  self.strptime_format = clean_datetime_format_for_strptime(self.format)
53
- self.invalid_value_message = invalid_value_message or self.gettext(
54
- "Not a valid datetime value."
55
- )
53
+ self.invalid_value_message = invalid_value_message
56
54
 
57
55
  def _value(self):
58
56
  if self.raw_data:
@@ -64,15 +62,17 @@ class DateTimeField(Field):
64
62
  if not valuelist:
65
63
  return
66
64
 
67
- date_str = " ".join(valuelist)
68
65
  for format in self.strptime_format:
69
66
  try:
67
+ date_str = " ".join(valuelist)
70
68
  self.data = datetime.datetime.strptime(date_str, format)
71
69
  return
72
- except ValueError:
70
+ except (ValueError, TypeError):
73
71
  self.data = None
74
72
 
75
- raise ValueError(self.invalid_value_message)
73
+ raise ValueError(
74
+ self.invalid_value_message or self.gettext("Not a valid datetime value.")
75
+ )
76
76
 
77
77
 
78
78
  class DateField(DateTimeField):
@@ -98,23 +98,22 @@ class DateField(DateTimeField):
98
98
  invalid_value_message=invalid_value_message,
99
99
  **kwargs,
100
100
  )
101
- self.invalid_value_message = invalid_value_message or self.gettext(
102
- "Not a valid date value."
103
- )
104
101
 
105
102
  def process_formdata(self, valuelist):
106
103
  if not valuelist:
107
104
  return
108
105
 
109
- date_str = " ".join(valuelist)
110
106
  for format in self.strptime_format:
111
107
  try:
108
+ date_str = " ".join(valuelist)
112
109
  self.data = datetime.datetime.strptime(date_str, format).date()
113
110
  return
114
- except ValueError:
111
+ except (ValueError, TypeError):
115
112
  self.data = None
116
113
 
117
- raise ValueError(self.invalid_value_message)
114
+ raise ValueError(
115
+ self.invalid_value_message or self.gettext("Not a valid date value.")
116
+ )
118
117
 
119
118
 
120
119
  class TimeField(DateTimeField):
@@ -140,23 +139,22 @@ class TimeField(DateTimeField):
140
139
  invalid_value_message=invalid_value_message,
141
140
  **kwargs,
142
141
  )
143
- self.invalid_value_message = invalid_value_message or self.gettext(
144
- "Not a valid time value."
145
- )
146
142
 
147
143
  def process_formdata(self, valuelist):
148
144
  if not valuelist:
149
145
  return
150
146
 
151
- time_str = " ".join(valuelist)
152
147
  for format in self.strptime_format:
153
148
  try:
149
+ time_str = " ".join(valuelist)
154
150
  self.data = datetime.datetime.strptime(time_str, format).time()
155
151
  return
156
- except ValueError:
152
+ except (ValueError, TypeError):
157
153
  self.data = None
158
154
 
159
- raise ValueError(self.invalid_value_message)
155
+ raise ValueError(
156
+ self.invalid_value_message or self.gettext("Not a valid time value.")
157
+ )
160
158
 
161
159
 
162
160
  class MonthField(DateField):
@@ -196,17 +194,14 @@ class WeekField(DateField):
196
194
  invalid_value_message=invalid_value_message,
197
195
  **kwargs,
198
196
  )
199
- self.invalid_value_message = invalid_value_message or self.gettext(
200
- "Not a valid week value."
201
- )
202
197
 
203
198
  def process_formdata(self, valuelist):
204
199
  if not valuelist:
205
200
  return
206
201
 
207
- time_str = " ".join(valuelist)
208
202
  for format in self.strptime_format:
209
203
  try:
204
+ time_str = " ".join(valuelist)
210
205
  if "%w" not in format:
211
206
  # The '%w' week starting day is needed. This defaults it to monday
212
207
  # like ISO 8601 indicates.
@@ -216,10 +211,12 @@ class WeekField(DateField):
216
211
  else:
217
212
  self.data = datetime.datetime.strptime(time_str, format).date()
218
213
  return
219
- except ValueError:
214
+ except (ValueError, TypeError):
220
215
  self.data = None
221
216
 
222
- raise ValueError(self.invalid_value_message)
217
+ raise ValueError(
218
+ self.invalid_value_message or self.gettext("Not a valid week value.")
219
+ )
223
220
 
224
221
 
225
222
  class DateTimeLocalField(DateTimeField):
@@ -205,12 +205,21 @@ class FieldList(Field):
205
205
  entry.name = new_name
206
206
  entry.short_name = str(new_index)
207
207
  entry.id = new_id
208
+ self._sync_label(entry, old_id)
208
209
 
209
210
  for descendant in self._iter_descendants(entry):
210
211
  if descendant.name and descendant.name.startswith(old_name):
211
212
  descendant.name = new_name + descendant.name[len(old_name) :]
212
213
  if descendant.id and descendant.id.startswith(old_id):
214
+ old_descendant_id = descendant.id
213
215
  descendant.id = new_id + descendant.id[len(old_id) :]
216
+ self._sync_label(descendant, old_descendant_id)
217
+
218
+ @staticmethod
219
+ def _sync_label(field, old_id):
220
+ label = getattr(field, "label", None)
221
+ if label is not None and label.field_id == old_id:
222
+ label.field_id = field.id
214
223
 
215
224
  def _iter_descendants(self, field):
216
225
  """Yield all fields below ``field`` recursively (form sub-fields and
@@ -231,7 +240,9 @@ class FieldList(Field):
231
240
  Entries added in this way will *not* receive formdata however, and can
232
241
  only receive object data.
233
242
  """
234
- return self._add_entry(data=data)
243
+ field = self._add_entry(data=data)
244
+ field.post_process()
245
+ return field
235
246
 
236
247
  def insert_entry(self, index, data=unset_value):
237
248
  """
@@ -247,6 +258,7 @@ class FieldList(Field):
247
258
  field = self._add_entry(data=data)
248
259
  self.entries.insert(index, self.entries.pop())
249
260
  self._compact_indices()
261
+ field.post_process()
250
262
  return field
251
263
 
252
264
  def pop_entry(self, index=-1):