From 5f860f8d3866cea262f34b6a37895464aa034e26 Mon Sep 17 00:00:00 2001 From: agu2347 <94227848+agu2347@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:34:47 +0530 Subject: [PATCH] Ignore blank PO-Revision-Date/POT-Creation-Date header values _parse_datetime_header() unconditionally passed the header value to datetime.strptime(), so a blank date (left empty by tools such as Poedit instead of the conventional 'YEAR-MO-DA HO:MI+ZONE' placeholder or omitting the header) raised an unhandled ValueError, crashing `pybabel update`/`compile` while parsing an otherwise valid catalog. Make _parse_datetime_header() return None for a blank (or whitespace-only) value, and skip assigning creation_date/revision_date in that case instead of raising, matching the existing handling of the literal 'YEAR-MO-DA HO:MI+ZONE' placeholder for PO-Revision-Date. Fixes #1219 --- babel/messages/catalog.py | 20 ++++++++++++++++---- tests/messages/test_catalog.py | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 5686b3141..adae85f47 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -103,10 +103,18 @@ def _has_python_brace_format(string: str) -> bool: return field_name_seen -def _parse_datetime_header(value: str) -> datetime.datetime: +def _parse_datetime_header(value: str) -> datetime.datetime | None: match = re.match(r'^(?P.*?)(?P[+-]\d{4})?$', value) - dt = datetime.datetime.strptime(match.group('datetime'), '%Y-%m-%d %H:%M') + dt_str = match.group('datetime').strip() + if not dt_str: + # Some tools (e.g. Poedit) leave the PO-Revision-Date/POT-Creation-Date + # header value blank instead of using the conventional + # 'YEAR-MO-DA HO:MI+ZONE' placeholder or omitting the header + # altogether. There's nothing meaningful to parse in that case. + return None + + dt = datetime.datetime.strptime(dt_str, '%Y-%m-%d %H:%M') # Separate the offset into a sign component, hours, and # minutes tzoffset = match.group('tzoffset') @@ -591,11 +599,15 @@ 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) + parsed = _parse_datetime_header(value) + if parsed is not None: + self.creation_date = parsed elif name == 'po-revision-date': # Keep the value if it's not the default one if 'YEAR' not in value: - self.revision_date = _parse_datetime_header(value) + parsed = _parse_datetime_header(value) + if parsed is not None: + self.revision_date = parsed @property def mime_headers(self) -> list[tuple[str, str]]: diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 4a60208c8..8723b0ab9 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -557,6 +557,29 @@ def test_datetime_parsing(): assert val2.tzinfo is None +def test_datetime_parsing_blank_value(): + # Some tools (e.g. Poedit) leave the PO-Revision-Date/POT-Creation-Date + # header value blank instead of using the conventional + # 'YEAR-MO-DA HO:MI+ZONE' placeholder or omitting the header + # altogether; this used to raise a ValueError. See GH issue #1219. + assert catalog._parse_datetime_header('') is None + assert catalog._parse_datetime_header(' ') is None + + +def test_set_mime_headers_ignores_blank_dates(): + cat = catalog.Catalog() + default_creation_date = cat.creation_date + default_revision_date = cat.revision_date + + cat._set_mime_headers([ + ('POT-Creation-Date', ''), + ('PO-Revision-Date', ''), + ]) + + assert cat.creation_date == default_creation_date + assert cat.revision_date == default_revision_date + + def test_update_catalog_comments(): # Based on https://web.archive.org/web/20100710131029/http://babel.edgewall.org/attachment/ticket/163/cat-update-comments.py