Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Fixed

- Preserve valid serialization for dates and times created through standard-library factory methods.

- Fix `item()` and `dumps()` writing a `datetime` whose UTC offset is not a whole number of minutes (e.g. the historical LMT offsets `zoneinfo` gives, such as `-00:44:30`) as invalid TOML like `1900-01-01T12:00:00Z:30`, which no parser accepts. Such a value is now written as the same instant in UTC; this also covers `DateTime.replace()` and `astimezone()`.
- Allow `DateTime.astimezone()` to use the local timezone when no timezone argument is supplied, matching `datetime.datetime`.
- Arithmetic and `replace()` results for `Integer`, `Float`, `String`, `DateTime`, `Date` and `Time` no longer share the original item's `Trivia`, so changing the result's comment or indentation no longer rewrites the item still stored in the document.
Expand Down
92 changes: 92 additions & 0 deletions tests/test_temporal_factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from __future__ import annotations

import sys

from datetime import date
from datetime import datetime
from datetime import time
from typing import Any

import pytest

from tomlkit import dumps
from tomlkit import parse
from tomlkit.items import Date
from tomlkit.items import DateTime
from tomlkit.items import Time


def assert_temporal_round_trip(value: date | time, expected: date | time) -> None:
rendered = dumps({"value": value})
if sys.version_info >= (3, 11):
import tomllib

assert tomllib.loads(rendered) == {"value": expected}
assert parse(rendered).unwrap() == {"value": expected}


@pytest.mark.parametrize(
"method, args",
[
("fromisoformat", ("2026-11-09",)),
("fromordinal", (date(2026, 11, 9).toordinal(),)),
("fromtimestamp", (1720000000,)),
("fromisocalendar", (2026, 46, 1)),
],
)
def test_date_factories_serialize(method: str, args: tuple[Any, ...]) -> None:
value = getattr(Date, method)(*args)
expected = getattr(date, method)(*args)

assert isinstance(value, Date)
assert_temporal_round_trip(value, expected)


def test_date_today_serializes() -> None:
before = date.today()
value = Date.today()
after = date.today()

assert isinstance(value, Date)
assert value.unwrap() in (before, after)
assert_temporal_round_trip(value, value.unwrap())


@pytest.mark.parametrize(
"text",
[
"00:00:00",
"12:34",
"12:34:56",
"12:34:56.123456",
"12:34:56.000001",
"12:34:56+02:00",
],
)
def test_time_fromisoformat_serializes(text: str) -> None:
value = Time.fromisoformat(text)
expected = time.fromisoformat(text)

assert isinstance(value, Time)
assert value.tzinfo == expected.tzinfo
assert_temporal_round_trip(value, expected.replace(tzinfo=None))


@pytest.mark.parametrize(
"source",
[
"value = 2026-11-09 # date\n",
"value = 12:34:56.120000 # time\n",
"value = 12:34:56.000000 # whole second\n",
],
)
def test_parsed_temporal_items_keep_original_format(source: str) -> None:
assert dumps(parse(source)) == source


def test_datetime_factory_serialization_is_unchanged() -> None:
text = "2026-11-09T12:34:56.123456"
value = DateTime.fromisoformat(text)

assert isinstance(value, DateTime)
assert_temporal_round_trip(value, datetime.fromisoformat(text))
4 changes: 2 additions & 2 deletions tomlkit/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -1302,7 +1302,7 @@ def __init__(
) -> None:
super().__init__(trivia or Trivia())

self._raw = raw
self._raw = raw or date.isoformat(self)

def unwrap(self) -> date:
(year, month, day, _, _) = self._getstate()
Expand Down Expand Up @@ -1387,7 +1387,7 @@ def __init__(
) -> None:
super().__init__(trivia or Trivia())

self._raw = raw
self._raw = raw or _format_time(self)

def unwrap(self) -> time:
(hour, minute, second, microsecond, tzinfo, _, _) = self._getstate()
Expand Down
Loading