Skip to content

Commit 61eb72b

Browse files
authored
Merge branch 'master' into fix/542-aot-captures-dotted-sibling
2 parents b39439a + e23a254 commit 61eb72b

8 files changed

Lines changed: 125 additions & 12 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ repos:
1515
args: [--py39-plus]
1616

1717
- repo: https://github.com/astral-sh/ruff-pre-commit
18-
rev: v0.15.18
18+
rev: v0.15.21
1919
hooks:
2020
- id: ruff
2121
args: [--fix, --exit-non-zero-on-fix, --show-fixes]

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
- Out-of-order value-vs-table and dotted-key-vs-table redefinitions are now rejected at parse time instead of being silently accepted or raising only on access. The parser also detects when a non-dotted key is a prefix of an existing dotted key, matching the stdlib `tomllib` behaviour. ([#523](https://github.com/python-poetry/tomlkit/issues/523))
3636
- Reject tables inserted into inline tables instead of serializing invalid TOML. ([#531](https://github.com/python-poetry/tomlkit/issues/531))
3737
- Fix assigning an array of tables over a dotted key (e.g. `doc["a"] = aot(...)` where `a` came from `a.b = ...`): the new `[[a]]` header kept the dotted key's inline position and swallowed the following dotted sibling on round-trip. The array of tables now renders past the inline entries it would otherwise capture, mirroring the table fix for [#513](https://github.com/python-poetry/tomlkit/issues/513). ([#542](https://github.com/python-poetry/tomlkit/issues/542))
38+
- Fix invalid serialization with a duplicated `[table]` header when adding a key to an out-of-order table whose concrete header is declared after its sub-tables; the new key now lands in the existing concrete part instead of giving the header-less super part a second header. ([#545](https://github.com/python-poetry/tomlkit/pull/545))
39+
- Fix a table's display name (its exact header spelling, including whitespace and quoting) being normalised when the table is assigned onto itself, e.g. `doc[k] = doc[k]` rewriting `[keys .'a'.'c']` to `[keys.a.'c']`. ([#291](https://github.com/python-poetry/tomlkit/issues/291))
40+
- Fix missing newlines when appending a key after a dotted inline table, including when the original document has no trailing newline. ([#533](https://github.com/python-poetry/tomlkit/pull/533))
41+
- Preserve trailing whitespace when replacing a super table, including assigning it onto itself. ([#534](https://github.com/python-poetry/tomlkit/pull/534))
42+
- Fix `str()` and `repr()` of out-of-order table proxies to show their merged values. ([#536](https://github.com/python-poetry/tomlkit/pull/536))
43+
- Reject decimal integer literals that exceed Python's integer-string conversion limit instead of coercing them to infinity. ([#538](https://github.com/python-poetry/tomlkit/pull/538))
3844

3945
## [0.15.0] - 2026-05-10
4046

‎docs/requirements.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ requests==2.33.0
3232
# via sphinx
3333
snowballstemmer==2.2.0
3434
# via sphinx
35-
soupsieve==2.5
35+
soupsieve==2.8.4
3636
# via beautifulsoup4
3737
sphinx==7.4.7
3838
# via

‎poetry.lock‎

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/test_parser.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import sys
2+
13
import pytest
24

35
from tomlkit.exceptions import EmptyTableNameError
46
from tomlkit.exceptions import InternalParserError
7+
from tomlkit.exceptions import InvalidNumberError
58
from tomlkit.exceptions import InvalidUnicodeValueError
69
from tomlkit.exceptions import ParseError
710
from tomlkit.exceptions import UnexpectedCharError
11+
from tomlkit.items import Integer
812
from tomlkit.items import StringType
913
from tomlkit.parser import Parser
1014

@@ -214,3 +218,20 @@ def test_parser_rejects_aot_header_missing_second_bracket(content: str) -> None:
214218
parser = Parser(content)
215219
with pytest.raises(ParseError):
216220
parser.parse()
221+
222+
223+
@pytest.mark.skipif(
224+
not hasattr(sys, "get_int_max_str_digits"),
225+
reason="requires the int-from-string digit limit (3.9.14+/3.10.7+/3.11+)",
226+
)
227+
def test_parser_rejects_overlong_decimal_integer() -> None:
228+
# A decimal integer with more digits than Python's int-from-string limit
229+
# raises ValueError in int(); it must be reported as an invalid number, not
230+
# silently fall through to float() and become inf.
231+
parser = Parser("a = " + "9" * 4301)
232+
with pytest.raises(InvalidNumberError):
233+
parser.parse()
234+
235+
# the value just under the limit is still a normal integer
236+
value = Parser("a = " + "9" * 4300).parse()["a"]
237+
assert isinstance(value, Integer)

‎tests/test_toml_document.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,20 @@ def test_valid_out_of_order_independent_tables() -> None:
643643
assert doc.as_string() == "[a]\nx=1\n[zz]\n[a.b]\nc=1\n"
644644

645645

646+
def test_set_value_on_out_of_order_table_with_empty_concrete_part() -> None:
647+
# A super table defined after its sub-table (the "defining a super-table
648+
# afterward is ok" spec example) leaves an empty concrete `[x]` part.
649+
# Adding a plain value must land in that concrete part, not turn the
650+
# header-less super part into a second `[x]` header -- which produced
651+
# output with a duplicate header that no longer parsed.
652+
doc = parse("[x.y.z.w]\n\n[x]\n")
653+
doc["x"]["c"] = 3
654+
655+
assert doc["x"]["c"] == 3
656+
assert doc.as_string() == "[x.y.z.w]\n\n[x]\nc = 3\n"
657+
assert parse(doc.as_string()).unwrap() == {"x": {"y": {"z": {"w": {}}}, "c": 3}}
658+
659+
646660
def test_out_of_order_table_merges_aot_fragments() -> None:
647661
# https://github.com/python-poetry/tomlkit/issues/505
648662
content = """\
@@ -887,6 +901,17 @@ def test_repr() -> None:
887901
assert repr(doc["namespace"]) == "{'key1': 'value1', 'key2': 'value2'}"
888902

889903

904+
def test_repr_out_of_order_table_proxy() -> None:
905+
doc = parse("""\
906+
a.b.c = "d"
907+
a.b.e = "f"
908+
""")
909+
expected = "{'b': {'c': 'd', 'e': 'f'}}"
910+
911+
assert repr(doc["a"]) == expected
912+
assert str(doc["a"]) == expected
913+
914+
890915
def test_deepcopy() -> None:
891916
content = """
892917
[tool]
@@ -1006,6 +1031,38 @@ def test_replace_preserve_sep() -> None:
10061031
)
10071032

10081033

1034+
def test_replace_super_table_preserves_whitespace() -> None:
1035+
content = """\
1036+
[env.pro1.rst]
1037+
name = "x7"
1038+
1039+
[env2]
1040+
name = 2
1041+
1042+
[env3]
1043+
name = 3
1044+
"""
1045+
doc = parse(content)
1046+
1047+
doc["env"] = doc["env"]
1048+
1049+
assert doc.as_string() == content
1050+
1051+
1052+
def test_replace_table_with_itself_preserves_display_name() -> None:
1053+
content = """\
1054+
[keys.a]
1055+
[keys .'a'.'c']
1056+
'd' = 'e'
1057+
"""
1058+
doc = parse(content)
1059+
1060+
for mode in doc["keys"]:
1061+
doc["keys"][mode] = doc["keys"][mode]
1062+
1063+
assert doc.as_string() == content
1064+
1065+
10091066
def test_replace_with_table_of_nested() -> None:
10101067
example = """\
10111068
[a]

‎tomlkit/container.py‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,7 @@ def _replace_at(
930930
value.trivia.trail = v.trivia.trail
931931
self._body[idx] = (new_key, value)
932932

933-
if hasattr(value, "invalidate_display_name"):
933+
if value is not v and hasattr(value, "invalidate_display_name"):
934934
value.invalidate_display_name()
935935

936936
if isinstance(value, Table):
@@ -1125,6 +1125,12 @@ def unwrap(self) -> dict[str, Any]:
11251125
def value(self) -> dict[str, Any]:
11261126
return self._internal_container.value
11271127

1128+
def __str__(self) -> str:
1129+
return str(self.value)
1130+
1131+
def __repr__(self) -> str:
1132+
return repr(self.value)
1133+
11281134
def __contains__(self, key: object) -> bool:
11291135
# Native membership test. The inherited ``MutableMapping.__contains__``
11301136
# resolves the value via ``__getitem__`` (and builds a ``NonExistentKey``
@@ -1170,7 +1176,18 @@ def _is_table_or_aot(it: Any) -> bool:
11701176
table[key] = value
11711177
break
11721178
else:
1173-
self._tables[0][key] = value
1179+
# No part holds a plain value yet, so the chosen part must
1180+
# start rendering its own ``[key]`` header. Prefer a part
1181+
# that is not a super table (it already renders that header):
1182+
# turning a header-less super part concrete here would emit a
1183+
# second, duplicate header next to an existing concrete part
1184+
# and produce TOML that no longer parses.
1185+
for table in self._tables:
1186+
if not table.is_super_table():
1187+
table[key] = value
1188+
break
1189+
else:
1190+
self._tables[0][key] = value
11741191
else:
11751192
self._tables[0][key] = value
11761193
else:
@@ -1220,9 +1237,12 @@ def ends_with_whitespace(it: Any) -> bool:
12201237
"""Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object
12211238
ending with a ``Whitespace``.
12221239
"""
1223-
return (
1224-
isinstance(it, Table) and isinstance(it.value._previous_item(), Whitespace)
1225-
) or (isinstance(it, AoT) and len(it) > 0 and isinstance(it[-1], Whitespace))
1240+
if isinstance(it, Whitespace):
1241+
return True
1242+
if isinstance(it, Table):
1243+
previous = it.value._previous_item()
1244+
return previous is not None and ends_with_whitespace(previous)
1245+
return isinstance(it, AoT) and len(it) > 0 and ends_with_whitespace(it[-1])
12261246

12271247

12281248
def _equal_with_nan(left: Any, right: Any) -> bool:

‎tomlkit/parser.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,11 +783,20 @@ def _parse_number(self, raw: str, trivia: Trivia) -> Item | None:
783783
try:
784784
return Integer(int(sign + clean, base), trivia, sign + raw)
785785
except ValueError:
786+
pass
787+
788+
# Only fall back to float for an actual float literal (a fractional
789+
# dot, a base-10 exponent, or inf/nan). A decimal integer whose digit
790+
# count exceeds Python's int-from-string conversion limit also raises
791+
# ValueError above; it must be rejected, not silently coerced to inf.
792+
if base == 10 and ("." in clean or "e" in clean or clean in ("inf", "nan")):
786793
try:
787794
return Float(float(sign + clean), trivia, sign + raw)
788795
except ValueError:
789796
return None
790797

798+
return None
799+
791800
def _parse_literal_string(self) -> String:
792801
with self._state:
793802
return self._parse_string(StringType.SLL)

0 commit comments

Comments
 (0)