Skip to content

Write datetimes with sub-minute UTC offsets as valid TOML - #701

Open
mmporong wants to merge 1 commit into
python-poetry:masterfrom
mmporong:fix-subminute-utc-offset
Open

mmporong wants to merge 1 commit into
python-poetry:masterfrom
mmporong:fix-subminute-utc-offset

Conversation

@mmporong

@mmporong mmporong commented Nov 3, 2026

Copy link
Copy Markdown

Summary

item() and dumps() render a datetime with isoformat(). When the UTC offset is not a whole number of minutes, isoformat() adds a seconds field (-00:44:30), and for an offset such as +00:00:30 the "+00:00" to "Z" replacement in item() turns it into Z:30. TOML offsets are HH:MM, so neither tomlkit nor tomllib can read the output back. zoneinfo gives these offsets (local mean time) for dates before a zone's first transition:

>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> import tomlkit, tomllib
>>> s = tomlkit.dumps({"d": datetime(1960, 1, 1, 12, tzinfo=ZoneInfo("Africa/Monrovia"))})
>>> s
'd = 1960-01-01T12:00:00-00:44:30\n'
>>> tomllib.loads(s)
tomllib.TOMLDecodeError: Expected newline or end of document after a statement (at line 1, column 30)

This PR adds _format_datetime(), which writes such a value as the same instant in UTC (1960-01-01T12:44:30Z above) and leaves whole-minute offsets unchanged. item(), DateTime() without raw, and DateTime._new() (used by replace(), astimezone() and arithmetic) all go through it. The item keeps its original fields and tzinfo; only the written text changes, and the parsed value compares equal to the original. This is in the same spirit as #613, which made tz-aware time values render as valid TOML. If you would rather raise ValueError for these offsets, I can switch to that.

Tests:

  • New tests in tests/test_items.py cover offsets of +30 s, +1 h 0 min 1 s, -44 min 30 s and 1 microsecond through item(), dumps(), astimezone() and replace(), and check that the item keeps its fields. All 5 fail on master and pass with this change.
  • Full suite with the toml-test submodule: 1229 passed (1224 before, plus the 5 new tests), Python 3.11 only.
  • The Africa/Monrovia example above, read back with tomllib, now gives a value equal to the original.
  • ruff 0.17.2 (check and format --check): clean. mypy 1.19.1 (the pinned version): the same 33 errors before and after, none in the changed lines.

No linked issue.

Duplicate Check

Agent Drafting Metadata

  • Agent: Claude Code
  • Model: Claude Opus 5.5
  • Notes: The defect was found by an automated round-trip check (dumps() then tomllib.loads()) in an AI-assisted scan of open-source projects. The agent wrote the fix, the tests, the changelog entry and this description, and ran the checks listed above.

isoformat() adds a seconds field for a UTC offset that is not a whole
number of minutes (e.g. the LMT offsets zoneinfo gives for historical
dates), and the '+00:00' -> 'Z' replacement in item() could also hit
the start of such an offset ('Z:30'). TOML offsets are HH:MM, so the
output could not be parsed by tomlkit or tomllib. Write such values as
the same instant in UTC in item(), DateTime() and DateTime._new().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant