From eef7ce5c807cc5701e88dff4182690fda907d974 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:26:20 -0700 Subject: [PATCH] fix: render non-date DateTimeField data without AttributeError Initial data= or obj string values for date/time fields used to crash in _value via a missing strftime. Render the string for the widget instead, including DateTimeLocalField. --- CHANGES.rst | 4 ++++ src/wtforms/fields/datetime.py | 11 +++++++++-- tests/fields/test_date.py | 11 +++++++++++ tests/fields/test_datetimelocal.py | 8 ++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 423e04b8..5dc673d0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,10 @@ Version 3.3.0b4 Unreleased +- :meth:`~fields.DateTimeField._value` and + :meth:`~fields.DateTimeLocalField._value` render plain string ``data`` / + ``obj`` values instead of raising ``AttributeError``. + - :class:`~datalist.DataList` ``choices`` accepts the shorthand ``{value: label}`` dict syntax. :issue:`886` - ``choices`` callables on fields nested in a :class:`~fields.FieldList` are diff --git a/src/wtforms/fields/datetime.py b/src/wtforms/fields/datetime.py index 4158dd3b..e98ce545 100644 --- a/src/wtforms/fields/datetime.py +++ b/src/wtforms/fields/datetime.py @@ -57,8 +57,13 @@ def __init__( def _value(self): if self.raw_data: return " ".join(self.raw_data) + if not self.data: + return "" format = self.format[0] - return self.data and self.data.strftime(format) or "" + # data=/obj may supply a plain string; still render for the widget. + if not hasattr(self.data, "strftime"): + return str(self.data) + return self.data.strftime(format) def process_formdata(self, valuelist): if not valuelist: @@ -279,8 +284,10 @@ def _value(self): return "" value = self.data + if not hasattr(value, "strftime"): + return str(value) tz = self._resolve_tz() - if tz is not None and value.tzinfo is not None: + if tz is not None and getattr(value, "tzinfo", None) is not None: value = value.astimezone(tz).replace(tzinfo=None) return value.strftime(self.format[0]) diff --git a/tests/fields/test_date.py b/tests/fields/test_date.py index ecc729cd..ab4488c1 100644 --- a/tests/fields/test_date.py +++ b/tests/fields/test_date.py @@ -38,3 +38,14 @@ class G(Form): form = G(DummyPostData(a=["bogus"])) assert not form.validate() assert form.a.errors == ["Enter a date as YYYY-MM-DD."] + + +def test_initial_string_data_renders(): + form = F(data={"a": "2020-01-02"}) + assert form.a.data == "2020-01-02" + assert form.a._value() == "2020-01-02" + + +def test_initial_non_date_obj_attribute_renders(): + form = F(obj=type("O", (), {"a": "not-a-date"})()) + assert form.a._value() == "not-a-date" diff --git a/tests/fields/test_datetimelocal.py b/tests/fields/test_datetimelocal.py index 1b8ed747..d3390cf9 100644 --- a/tests/fields/test_datetimelocal.py +++ b/tests/fields/test_datetimelocal.py @@ -139,3 +139,11 @@ def test_tz_naive_data_rendered_unchanged(): F = make_form(a=DateTimeLocalField(tz=paris)) form = F(a=datetime(2026, 5, 6, 16, 0)) assert form.a._value() == "2026-05-06 16:00:00" + + +def test_initial_string_data_renders(): + class G(Form): + a = DateTimeLocalField() + + form = G(data={"a": "2020-01-02T03:04"}) + assert form.a._value() == "2020-01-02T03:04"