From 7caff0d0c3758723be6792c280bbfa4f9163f159 Mon Sep 17 00:00:00 2001 From: Kushal Dave Date: Tue, 10 Nov 2026 14:37:07 +0000 Subject: [PATCH] Allow table setdefault calls without an explicit default --- CHANGELOG.md | 2 ++ tests/test_items.py | 32 ++++++++++++++++++++++++++++++++ tomlkit/items.py | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df615099..eda39f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ ### Fixed +- Allow `Table.setdefault()` and `InlineTable.setdefault()` to retrieve existing values without supplying a default, matching document and mapping behavior. + - 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. diff --git a/tests/test_items.py b/tests/test_items.py index 13853fb2..71fbb534 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -1901,3 +1901,35 @@ def test_out_of_order_table_proxy_membership() -> None: _ = 123 in table # membership must not resolve values or mutate the document assert doc.as_string() == content + + +@pytest.mark.parametrize("inline", [False, True]) +@pytest.mark.parametrize("literal, expected", [("0", 0), ("false", False), ('""', "")]) +def test_table_setdefault_existing_key_without_default( + inline: bool, literal: str, expected: int | bool | str +) -> None: + if inline: + source = f"settings = {{ value = {literal} }} # preserve formatting\n" + else: + source = f"[settings] # preserve formatting\nvalue = {literal}\n" + document = parse(source) + settings = document["settings"] + assert isinstance(settings, (Table, InlineTable)) + + assert settings.setdefault("value") == expected + assert api.dumps(document) == source + + +@pytest.mark.parametrize("inline", [False, True]) +def test_table_setdefault_missing_key_without_default_preserves_document( + inline: bool, +) -> None: + from tomlkit.exceptions import ConvertError + + source = "settings = { value = 1 }\n" if inline else "[settings]\nvalue = 1\n" + document = parse(source) + settings = document["settings"] + assert isinstance(settings, (Table, InlineTable)) + with pytest.raises(ConvertError): + settings.setdefault("missing") + assert api.dumps(document) == source diff --git a/tomlkit/items.py b/tomlkit/items.py index 4d56f564..8198e617 100644 --- a/tomlkit/items.py +++ b/tomlkit/items.py @@ -1909,7 +1909,7 @@ def remove(self: AT, key: Key | str) -> AT: def item(self, key: Key | str) -> Item | OutOfOrderTableProxy: return self._value.item(key) - def setdefault(self, key: Key | str, default: Any) -> Any: # type: ignore[override] + def setdefault(self, key: Key | str, default: Any = None) -> Any: super().setdefault(key, default) return self[key]