Repository: wtforms/wtforms
Pull request #593 (2020-04-24 10:43:01)

Implemented MonthField
I could not manage to rebase #530, so here is a new PR.

davidism: Can you amend the first commit message so it contains [`Co-authored-by`](https://help.github.com/en/github/committing-changes-to-your-project/creating-a-commit-with-multiple-authors) information for the original user. You can get the `name <email>` from `git log` in the original PR.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 5a4f498..4c28ec3 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -26,6 +26,7 @@ Unreleased
 -   Choices shortcut for :class:`~fields.core.SelectMultipleField`.
     :issue:`603` :pr:`605`
 -   Forms can have form-level errors. :issue:`55` :pr:`595`
+-   Implemented :class:`~wtforms.fields.core.MonthField`. :pr:`530` :pr:`593`
 
 
 Version 2.3.1
diff --git a/docs/fields.rst b/docs/fields.rst
index 2d183d0..ed047ce 100644
--- a/docs/fields.rst
+++ b/docs/fields.rst
@@ -583,6 +583,8 @@ namespace and can be overridden or modified just like any other widget.
 
 .. autoclass:: DateField(default field arguments, format='%Y-%m-%d')
 
+.. autoclass:: MonthField(default field arguments, format='%Y-%m')
+
 .. autoclass:: TimeField(default field arguments, format='%H:%M')
 
 .. autoclass:: DateTimeLocalField(default field arguments, format='%Y-%m-%d %H:%M:%S')
diff --git a/docs/widgets.rst b/docs/widgets.rst
index 71db5da..81b96c0 100644
--- a/docs/widgets.rst
+++ b/docs/widgets.rst
@@ -1,6 +1,5 @@
 Widgets
 =======
-.. module:: wtforms.widgets
 
 Widgets are classes whose purpose are to render a field to its usable
 representation, usually XHTML.  When a field is called, the default behaviour
@@ -15,17 +14,36 @@ recognize as not needing to be auto-escaped.
 Built-in widgets
 ----------------
 
-.. autoclass:: ListWidget
-.. autoclass:: TableWidget
-.. autoclass:: Input
-.. autoclass:: TextInput()
-.. autoclass:: PasswordInput
-.. autoclass:: HiddenInput()
-.. autoclass:: CheckboxInput()
-.. autoclass:: FileInput()
-.. autoclass:: SubmitInput()
-.. autoclass:: TextArea
-.. autoclass:: Select
+.. autoclass:: wtforms.widgets.ListWidget
+.. autoclass:: wtforms.widgets.TableWidget
+.. autoclass:: wtforms.widgets.Input
+.. autoclass:: wtforms.widgets.TextInput()
+.. autoclass:: wtforms.widgets.PasswordInput
+.. autoclass:: wtforms.widgets.HiddenInput()
+.. autoclass:: wtforms.widgets.CheckboxInput()
+.. autoclass:: wtforms.widgets.FileInput()
+.. autoclass:: wtforms.widgets.SubmitInput()
+.. autoclass:: wtforms.widgets.TextArea
+.. autoclass:: wtforms.widgets.Select
+
+HTML5 widgets
+-------------
+
+.. module:: wtforms.widgets.html5
+
+.. autoclass:: wtforms.widgets.html5.ColorInput
+.. autoclass:: wtforms.widgets.html5.DateTimeInput
+.. autoclass:: wtforms.widgets.html5.DateTimeLocalInput
+.. autoclass:: wtforms.widgets.html5.DateInput
+.. autoclass:: wtforms.widgets.html5.EmailInput
+.. autoclass:: wtforms.widgets.html5.MonthInput
+.. autoclass:: wtforms.widgets.html5.NumberInput
+.. autoclass:: wtforms.widgets.html5.RangeInput
+.. autoclass:: wtforms.widgets.html5.SearchInput
+.. autoclass:: wtforms.widgets.html5.TelInput
+.. autoclass:: wtforms.widgets.html5.TimeInput
+.. autoclass:: wtforms.widgets.html5.URLInput
+.. autoclass:: wtforms.widgets.html5.WeekInput
 
 Widget-Building Utilities
 -------------------------
@@ -34,7 +52,7 @@ These utilities are used in WTForms widgets to help render HTML and also in
 order to work along with HTML templating frameworks. They can be imported for
 use in building custom widgets as well.
 
-.. autofunction:: html_params
+.. autofunction:: wtforms.widgets.html_params
 
 WTForms uses `MarkupSafe`_ to escape unsafe HTML characters before
 rendering. You can mark a string using :class:`markupsafe.Markup` to
diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py
index 8415ca9..96bc5d6 100644
--- a/src/wtforms/fields/core.py
+++ b/src/wtforms/fields/core.py
@@ -26,6 +26,7 @@ __all__ = (
     "SelectMultipleField",
     "StringField",
     "TimeField",
+    "MonthField",
 )
 
 
@@ -898,6 +899,16 @@ class TimeField(DateTimeField):
                 raise ValueError(self.gettext("Not a valid time value"))
 
 
+class MonthField(DateField):
+    """
+    Same as DateField, except represents a month, stores a `datetime.date`
+    with `day = 1`.
+    """
+
+    def __init__(self, label=None, validators=None, format="%Y-%m", **kwargs):
+        super().__init__(label, validators, format, **kwargs)
+
+
 class FormField(Field):
     """
     Encapsulate a form as a field in another form.
diff --git a/src/wtforms/fields/html5.py b/src/wtforms/fields/html5.py
index 177001f..26d7efa 100644
--- a/src/wtforms/fields/html5.py
+++ b/src/wtforms/fields/html5.py
@@ -73,6 +73,14 @@ class TimeField(core.TimeField):
     widget = widgets.TimeInput()
 
 
+class MonthField(core.MonthField):
+    """
+    Represents an ``<input type="month">``.
+    """
+
+    widget = widgets.MonthInput()
+
+
 class DateTimeLocalField(core.DateTimeField):
     """
     Represents an ``<input type="datetime-local">``.


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index cde7945..c78ed47 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -28,6 +28,7 @@ from wtforms.fields import HiddenField
 from wtforms.fields import IntegerField
 from wtforms.fields import IntegerRangeField
 from wtforms.fields import Label
+from wtforms.fields import MonthField
 from wtforms.fields import MultipleFileField
 from wtforms.fields import PasswordField
 from wtforms.fields import RadioField
@@ -811,6 +812,29 @@ class TestDateField:
         assert form.a.process_errors[0] == "Not a valid date value"
 
 
+class TestMonthField:
+    class F(Form):
+        a = MonthField()
+        b = MonthField(format="%m/%Y")
+
+    def test_basic(self):
+        d = date(2008, 5, 1)
+        form = self.F(DummyPostData(a=["2008-05"], b=["05/2008"]))
+
+        assert d == form.a.data
+        assert "2008-05" == form.a._value()
+
+        assert d == form.b.data
+
+    def test_failure(self):
+        form = self.F(DummyPostData(a=["2008-bb"]))
+
+        assert not form.validate()
+        assert 1 == len(form.a.process_errors)
+        assert 1 == len(form.a.errors)
+        assert "Not a valid date value" == form.a.process_errors[0]
+
+
 class TestTimeField:
     class F(Form):
         a = TimeField()