From dad7725843d6f0428a7565f7817b6292ec84356e Mon Sep 17 00:00:00 2001 From: agu2347 Date: Sat, 18 Jul 2026 10:24:05 +0000 Subject: [PATCH 1/2] 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 145954a64..35ce0d012 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -592,10 +592,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) @property diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index bf41c1976..0a8c2368d 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -343,6 +343,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('', From 3735f07b3c51de21e15898fb1a88872212d3c912 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 4 Nov 2026 12:55:29 +0200 Subject: [PATCH 2/2] Adjust comments --- babel/messages/catalog.py | 9 +++------ tests/messages/test_catalog.py | 11 +---------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 35ce0d012..21af7dfe1 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -592,15 +592,12 @@ 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': - # Some tools (e.g. Poedit) may leave this header blank. - # Keep the previous/default value rather than crashing. + # Keep last value instead of crashing on blank values 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. - # Some tools (e.g. Poedit) may also leave this header - # blank; keep the previous/default value in that case - # too, rather than crashing. + # Keep the value if it's not the default one + # and don't crash trying to parse blank values if 'YEAR' not in value and value.strip(): self.revision_date = _parse_datetime_header(value) diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 0a8c2368d..caa7562c8 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -344,14 +344,6 @@ def test_catalog_stores_datetime_correctly(): 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 @@ -362,8 +354,7 @@ def test_catalog_handles_blank_revision_and_creation_date(): "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. + # We didn't crash and we retained the original values assert cat.creation_date == default_creation_date assert cat.revision_date == default_revision_date