From d1a51bf96913567cd6c3e8d0a31555e068c38685 Mon Sep 17 00:00:00 2001 From: Dylan Pulver Date: Tue, 1 Sep 2026 22:03:16 +0300 Subject: [PATCH] Reject trailing characters in key_value() tomlkit.key_value() parsed a single key/value pair and returned it without checking that the input was consumed, so anything after the pair was silently discarded: >>> tomlkit.key_value("a = 1 b = 2 c = 3") (, 1) TOML 1.0.0 requires a newline or EOF after a key/value pair, and parse() already enforces that -- tests/examples/invalid/key_value_with_trailing_chars.toml exists for exactly this rule and is asserted against parse() alone. Parse the comment trail (as the document parser does for a top-level pair) and then require end of input, mirroring the guard value() already has. Trailing whitespace, a comment and a final newline stay valid; real trailing input now raises UnexpectedCharError. --- tests/test_api.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ tomlkit/api.py | 9 ++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index b5f31bc3..dca95122 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -320,6 +320,50 @@ def test_key_value() -> None: assert isinstance(i, Integer) +@pytest.mark.parametrize( + "src", + ["foo = 12", "foo = 12\n", " foo = 12", "foo = 12 ", "foo = 12 # comment\n"], +) +def test_key_value_allows_trailing_whitespace_and_comment(src: str) -> None: + k, i = tomlkit.key_value(src) + + assert k.key == "foo" + assert i == 12 + + +@pytest.mark.parametrize( + "src", + [ + "foo = 12 junk", + "foo = 12 = 13", + "foo = 12]]]", + ], +) +def test_key_value_raises_on_trailing_chars(src: str) -> None: + # parse() rejects each of these; key_value() must agree. + with pytest.raises(UnexpectedCharError): + parse(src) + + with pytest.raises(UnexpectedCharError): + tomlkit.key_value(src) + + +def test_key_value_raises_on_a_second_pair() -> None: + # Two pairs are a valid document, but key_value() parses a single pair, + # so the second one is trailing input rather than a silently dropped value. + assert dict(parse("foo = 12\nbar = 13")) == {"foo": 12, "bar": 13} + + with pytest.raises(UnexpectedCharError): + tomlkit.key_value("foo = 12\nbar = 13") + + +def test_key_value_raises_on_invalid_example( + invalid_example: Callable[[str], str], +) -> None: + with pytest.raises(UnexpectedCharError): + tomlkit.key_value(invalid_example("key_value_with_trailing_chars")) + + def test_string() -> None: s = tomlkit.string('foo "') diff --git a/tomlkit/api.py b/tomlkit/api.py index b0f8cd6d..c7f528cc 100644 --- a/tomlkit/api.py +++ b/tomlkit/api.py @@ -290,12 +290,19 @@ def value(raw: str) -> _Item: def key_value(src: str) -> tuple[Key, _Item]: """Parse a key-value pair from a string. + Anything other than whitespace or a comment after the pair is an error, + as it is when the same text is given to :func:`parse`. + :Example: >>> key_value("foo = 1") (Key('foo'), 1) """ - return Parser(src)._parse_key_value() + parser = Parser(src) + k, v = parser._parse_key_value(parse_comment=True) + if not parser.end(): + raise parser.parse_error(UnexpectedCharError, char=parser._current) + return k, v def ws(src: str) -> Whitespace: