From ba35e3f71d24ec2e8d2fb8cb72e6df2807159749 Mon Sep 17 00:00:00 2001 From: agu2347 Date: Sat, 18 Jul 2026 10:24:05 +0000 Subject: [PATCH] Don't crash on blank PO-Revision-Date / POT-Creation-Date headers Some tools (e.g. Poedit) can leave the PO-Revision-Date header blank in a generated .po file instead of omitting it or using the conventional 'YEAR-MO-DA HO:MI+ZONE' placeholder. Parsing such a file crashed with: ValueError: time data '' does not match format '%Y-%m-%d %H:%M' because _parse_datetime_header() was called unconditionally with the blank value. Guard both call sites (po-revision-date and, for the same reason, pot-creation-date) to skip parsing when the header value is blank, keeping the previous/default value instead -- the same treatment already given to the 'YEAR-MO-DA HO:MI+ZONE' placeholder value just above, and to a blank Language header elsewhere in this same method. Added a regression test that parses a message block with both headers blank and asserts: no exception is raised, the catalog's creation_date/revision_date fall back to their defaults rather than being corrupted, and no literal 'None' leaks into the round-tripped mime_headers output. Confirmed the test fails with the original ValueError without the fix, and passes with it. Also verified end-to-end with an actual .po file parse + write round-trip (using an installed release build with CLDR data, since a shallow clone here can't build that data), and confirmed normal (non-blank) header values are completely unaffected. Ran the full existing test_catalog.py and test_pofile.py suites: all pass, no regressions. Fixes #1219 --- babel/messages/catalog.py | 12 +++++++++--- tests/messages/test_catalog.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 9a9739a72..e42d34c5b 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -582,10 +582,16 @@ def _set_mime_headers(self, headers: Iterable[tuple[str, str]]) -> None: self._num_plurals = int(params.get('nplurals', 2)) self._plural_expr = params.get('plural', '(n != 1)') elif name == 'pot-creation-date': - self.creation_date = _parse_datetime_header(value) + # Some tools (e.g. Poedit) may leave this header blank. + # Keep the previous/default value rather than crashing. + if value.strip(): + self.creation_date = _parse_datetime_header(value) elif name == 'po-revision-date': - # Keep the value if it's not the default one - if 'YEAR' not in value: + # Keep the value if it's not the default one. + # Some tools (e.g. Poedit) may also leave this header + # blank; keep the previous/default value in that case + # too, rather than crashing. + if 'YEAR' not in value and value.strip(): self.revision_date = _parse_datetime_header(value) mime_headers = property( diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 191a2a498..522d9a625 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -346,6 +346,36 @@ def test_catalog_stores_datetime_correctly(): assert value == '2009-03-09 15:47-0700' +def test_catalog_handles_blank_revision_and_creation_date(): + """Regression test for + https://github.com/python-babel/babel/issues/1219 + + Some tools (e.g. Poedit) can leave PO-Revision-Date and/or + POT-Creation-Date blank in the header. Parsing such a header must + not raise, and must not silently store an unparseable value that + would corrupt the header on write-out. + """ + cat = catalog.Catalog() + default_creation_date = cat.creation_date + default_revision_date = cat.revision_date + + cat[''] = catalog.Message( + '', + "POT-Creation-Date: \n" + "PO-Revision-Date: \n", + ) + + # The blank headers should not have overwritten the defaults with + # an unparseable value (e.g. the raw empty string) or crashed. + assert cat.creation_date == default_creation_date + assert cat.revision_date == default_revision_date + + for key, value in cat.mime_headers: + if key in ('POT-Creation-Date', 'PO-Revision-Date'): + assert value is not None + assert 'None' not in str(value) + + def test_catalog_mime_headers_contain_same_information_as_attributes(): cat = catalog.Catalog() cat[''] = catalog.Message('',