#1149368 khal: FTBFS against python 3.15rc2 (several date parsing errors)

Package:
src:khal
Source:
src:khal
Submitter:
Margarita Manterola
Date:
2026-09-29 17:31:02 UTC
Severity:
normal
Tags:
#1149368#5
Date:
2026-09-29 17:28:41 UTC
From:
To:
Hi!

While rebuilding the python related packages against the Python 3.15rc2
version we found that khal fails to build from source:
https://debusine.debian.net/debian/r-python-python3.15/artifact/4856282/

In the log, there are several tests failing, apparently due to date
parsing errors:

=================================== FAILURES
===================================
E       assert not SystemExit(1)
E        +  where SystemExit(1) = <Result SystemExit(1)>.exception

tests/cli_test.py:1069: AssertionError
------------------------------ Captured log call 
-------------------------------
CRITICAL khal:cli.py:273 Could not parse "01.06. 15:00 01.06. 15:30".
Please check your configuration or run `khal printformats` to see if
this does match your configured [long](date|time|datetime)format.
If you suspect a bug, please file an issue at
https://github.com/pimutils/khal/issues/

E       assert 'error parsing range' not in "calendar: two\nsummary: new
event\ndatetime range: now\ntimezone [Europe/Berlin]:
Europe/London\ndescription (or 'None'):
None\n/tmp/pytest-of-sbuild/pytest-0/test_issue_10560/calendar2/61UPW4BTXJL113QXECJKRR1NC1CYWGVHPNPJ.ics\nevent
saved\nEdit?  do[n]e  [s]ummary  [d]escription  da[t]etime range
re[p]eat  [l]ocation\n[c]ategories  [a]larm  [D]elete  [u]rl:
t\ndatetime range [01.06. 09:00 01.06. 10:00]: \nerror parsing
range\nEdit?  do[n]e  [s]ummary  [d]escription  da[t]etime range
re[p]eat  [l]ocation\n[c]ategories  [a]larm  [D]elete  [u]rl: n\n"
E
E         'error parsing range' is contained here:
E           calendar: two
E           summary: new event
E           datetime range: now
E           timezone [Europe/Berlin]: Europe/London
E           description (or 'None'): None
E
/tmp/pytest-of-sbuild/pytest-0/test_issue_10560/calendar2/61UPW4BTXJL113QXECJKRR1NC1CYWGVHPNPJ.ics
E           event saved
E           Edit?  do[n]e  [s]ummary  [d]escription  da[t]etime range
re[p]eat  [l]ocation
E           [c]ategories  [a]larm  [D]elete  [u]rl: t
E           datetime range [01.06. 09:00 01.06. 10:00]:
E           error parsing range
E           Edit?  do[n]e  [s]ummary  [d]escription  da[t]etime range
re[p]eat  [l]ocation
E           [c]ategories  [a]larm  [D]elete  [u]rl: n

tests/cli_test.py:1092: AssertionError

E           AssertionError: assert b'BEGIN:VEVENT\r\nSUMMARY:16.08.
\xc3\x84wes\xc3\xb6me
Event\r\nDTSTART;VALUE=DATE:20140815\r\nDTEND;VALUE=DATE:20140816\r\nDTSTAMP:20140216T120000Z\r\nUID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA\r\nEND:VEVENT\r\n'
== b'BEGIN:VEVENT\r\nSUMMARY:\xc3\x84wes\xc3\xb6me
Event\r\nDTSTART;VALUE=DATE:20140815\r\nDTEND;VALUE=DATE:20140817\r\nDTSTAMP:20140216T120000Z\r\nUID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA\r\nEND:VEVENT\r\n'
E
E             At index 22 diff: b'1' != b'\xc3'
E
E             Full diff:
E             - (b'BEGIN:VEVENT\r\nSUMMARY:\xc3\x84wes\xc3\xb6me
Event\r\nDTSTART;VALUE=DATE:20'
E             ?
                -------
E             + (b'BEGIN:VEVENT\r\nSUMMARY:16.08. \xc3\x84wes\xc3\xb6me
Event\r\nDTSTART;VALUE='
E             ?                            +++++++
E             -
b'140815\r\nDTEND;VALUE=DATE:20140817\r\nDTSTAMP:20140216T120000Z\r\nUID:E41'
E             ?                                      ^
              -------
E             +
b'DATE:20140815\r\nDTEND;VALUE=DATE:20140816\r\nDTSTAMP:20140216T120000Z\r\n'
E             ?    +++++++                                  ^
E             -  b'JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA\r\nEND:VEVENT\r\n')
E             +
b'UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA\r\nEND:VEVENT\r\n')
E             ?    +++++++

tests/parse_datetime_test.py:468: AssertionError


tests/parse_datetime_test.py:526:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ _
tests/parse_datetime_test.py:47: in _construct_event
     info = eventinfofstr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ _

info_string = '15.06. Äwesöme Event'
locale = {'default_timezone': <DstTzInfo 'Europe/Berlin' LMT+0:53:00
STD>, 'local_timezone': <DstTzInfo 'Europe/Berlin' LMT+0:53:00 STD>,
'dateformat': '%d.%m.', 'longdateformat': '%d.%m.%Y', ...}
default_event_duration = datetime.timedelta(seconds=3600)
default_dayevent_duration = datetime.timedelta(days=1),
adjust_reasonably = True

     def eventinfofstr(
         info_string: str,
         locale: LocaleConfiguration,
         default_event_duration: dt.timedelta,
         default_dayevent_duration: dt.timedelta,
         adjust_reasonably: bool = False,
     ) -> dict[str, Any]:
         """parses a string of the form START [END | DELTA] [TIMEZONE]
[SUMMARY] [::
         DESCRIPTION] into a dictionary with keys: dtstart, dtend,
timezone, allday,
         summary, description
         """
         description = None
         if " :: " in info_string:
             info_string, description = info_string.split(" :: ")

         parts = info_string.split(" ")
         summary = None
         start: dt.datetime | dt.date | None = None
         end: dt.datetime | dt.date | None = None
         tz: pytz.BaseTzInfo | None = None
         allday: bool = False
         for i in reversed(range(1, len(parts) + 1)):
             try:
                 start, end, allday = guessrangefstr(
                     " ".join(parts[0:i]),
                     locale,
                     default_timedelta_datetime=default_event_duration,
                     default_timedelta_date=default_dayevent_duration,
                     adjust_reasonably=adjust_reasonably,
                 )
             except (ValueError, DateTimeParseError):
                 continue
             if start is not None and end is not None:
                 try:
                     # next element is a valid Olson db timezone string
                     tz = pytz.timezone(parts[i])
                     i += 1
                 except (pytz.UnknownTimeZoneError, UnicodeDecodeError,
IndexError):
                     tz = None
                 summary = " ".join(parts[i:])
                 break

         if start is None or end is None:
                 f'Could not parse "{info_string}".\nPlease check your '
                 "configuration or run `khal printformats` to see if this
does "
                 "match your configured
[long](date|time|datetime)format.\nIf you "
                 "suspect a bug, please file an issue at "
                 "https://github.com/pimutils/khal/issues/ "
             )
E           khal.exceptions.DateTimeParseError: Could not parse "15.06.
Äwesöme Event".
E           Please check your configuration or run `khal printformats`
to see if this does match your configured
[long](date|time|datetime)format.
E           If you suspect a bug, please file an issue at
https://github.com/pimutils/khal/issues/

khal/parse_datetime.py:492: DateTimeParseError


tests/parse_datetime_test.py:546:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ _
tests/parse_datetime_test.py:47: in _construct_event
     info = eventinfofstr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ _

info_string = '29.02. Äwesöme Event'
locale = {'default_timezone': <DstTzInfo 'Europe/Berlin' LMT+0:53:00
STD>, 'local_timezone': <DstTzInfo 'Europe/Berlin' LMT+0:53:00 STD>,
'dateformat': '%d.%m.', 'longdateformat': '%d.%m.%Y', ...}
default_event_duration = datetime.timedelta(seconds=3600)
default_dayevent_duration = datetime.timedelta(days=1),
adjust_reasonably = True

     def eventinfofstr(
         info_string: str,
         locale: LocaleConfiguration,
         default_event_duration: dt.timedelta,
         default_dayevent_duration: dt.timedelta,
         adjust_reasonably: bool = False,
     ) -> dict[str, Any]:
         """parses a string of the form START [END | DELTA] [TIMEZONE]
[SUMMARY] [::
         DESCRIPTION] into a dictionary with keys: dtstart, dtend,
timezone, allday,
         summary, description
         """
         description = None
         if " :: " in info_string:
             info_string, description = info_string.split(" :: ")

         parts = info_string.split(" ")
         summary = None
         start: dt.datetime | dt.date | None = None
         end: dt.datetime | dt.date | None = None
         tz: pytz.BaseTzInfo | None = None
         allday: bool = False
         for i in reversed(range(1, len(parts) + 1)):
             try:
                 start, end, allday = guessrangefstr(
                     " ".join(parts[0:i]),
                     locale,
                     default_timedelta_datetime=default_event_duration,
                     default_timedelta_date=default_dayevent_duration,
                     adjust_reasonably=adjust_reasonably,
                 )
             except (ValueError, DateTimeParseError):
                 continue
             if start is not None and end is not None:
                 try:
                     # next element is a valid Olson db timezone string
                     tz = pytz.timezone(parts[i])
                     i += 1
                 except (pytz.UnknownTimeZoneError, UnicodeDecodeError,
IndexError):
                     tz = None
                 summary = " ".join(parts[i:])
                 break

         if start is None or end is None:
                 f'Could not parse "{info_string}".\nPlease check your '
                 "configuration or run `khal printformats` to see if this
does "
                 "match your configured
[long](date|time|datetime)format.\nIf you "
                 "suspect a bug, please file an issue at "
                 "https://github.com/pimutils/khal/issues/ "
             )
E           khal.exceptions.DateTimeParseError: Could not parse "29.02.
Äwesöme Event".
E           Please check your configuration or run `khal printformats`
to see if this does match your configured
[long](date|time|datetime)format.
E           If you suspect a bug, please file an issue at
https://github.com/pimutils/khal/issues/

khal/parse_datetime.py:492: DateTimeParseError


tests/parse_datetime_test.py:582:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ _
tests/parse_datetime_test.py:47: in _construct_event
     info = eventinfofstr(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ _

info_string = '15.06. Äwesöme Event'
locale = {'default_timezone': <DstTzInfo 'Europe/Berlin' LMT+0:53:00
STD>, 'local_timezone': <DstTzInfo 'Europe/Berlin' LMT+0:53:00 STD>,
'dateformat': '%d.%m.', 'longdateformat': '%d.%m.%Y', ...}
default_event_duration = datetime.timedelta(seconds=3600)
default_dayevent_duration = datetime.timedelta(days=1),
adjust_reasonably = True

     def eventinfofstr(
         info_string: str,
         locale: LocaleConfiguration,
         default_event_duration: dt.timedelta,
         default_dayevent_duration: dt.timedelta,
         adjust_reasonably: bool = False,
     ) -> dict[str, Any]:
         """parses a string of the form START [END | DELTA] [TIMEZONE]
[SUMMARY] [::
         DESCRIPTION] into a dictionary with keys: dtstart, dtend,
timezone, allday,
         summary, description
         """
         description = None
         if " :: " in info_string:
             info_string, description = info_string.split(" :: ")

         parts = info_string.split(" ")
         summary = None
         start: dt.datetime | dt.date | None = None
         end: dt.datetime | dt.date | None = None
         tz: pytz.BaseTzInfo | None = None
         allday: bool = False
         for i in reversed(range(1, len(parts) + 1)):
             try:
                 start, end, allday = guessrangefstr(
                     " ".join(parts[0:i]),
                     locale,
                     default_timedelta_datetime=default_event_duration,
                     default_timedelta_date=default_dayevent_duration,
                     adjust_reasonably=adjust_reasonably,
                 )
             except (ValueError, DateTimeParseError):
                 continue
             if start is not None and end is not None:
                 try:
                     # next element is a valid Olson db timezone string
                     tz = pytz.timezone(parts[i])
                     i += 1
                 except (pytz.UnknownTimeZoneError, UnicodeDecodeError,
IndexError):
                     tz = None
                 summary = " ".join(parts[i:])
                 break

         if start is None or end is None:
                 f'Could not parse "{info_string}".\nPlease check your '
                 "configuration or run `khal printformats` to see if this
does "
                 "match your configured
[long](date|time|datetime)format.\nIf you "
                 "suspect a bug, please file an issue at "
                 "https://github.com/pimutils/khal/issues/ "
             )
E           khal.exceptions.DateTimeParseError: Could not parse "15.06.
Äwesöme Event".
E           Please check your configuration or run `khal printformats`
to see if this does match your configured
[long](date|time|datetime)format.
E           If you suspect a bug, please file an issue at
https://github.com/pimutils/khal/issues/

khal/parse_datetime.py:492: DateTimeParseError

Please note that while we ran into this error during the rebuild against
Python 3.15, that doesn't mean that the root cause for the failure is
necessarily the new Python version.

Setting the severity to important for now. Once Python 3.15 is released
and later added to python3-defaults, this bug will become release
critical.