Skip to content

Commit f3358e7

Browse files
authored
Swap uiri/toml encoder for tomli_w. #439 (#566)
1 parent a423e15 commit f3358e7

5 files changed

Lines changed: 98 additions & 24 deletions

File tree

benedict/serializers/toml.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
try:
2-
import toml
3-
4-
toml_installed = True
5-
except ModuleNotFoundError: # pragma: no cover
6-
toml_installed = False
7-
81
try:
92
# python >= 3.11
103
import tomllib
@@ -13,6 +6,20 @@
136
except ImportError:
147
tomllib_available = False
158

9+
try:
10+
import tomli
11+
12+
tomli_installed = True
13+
except ModuleNotFoundError: # pragma: no cover
14+
tomli_installed = False
15+
16+
try:
17+
import tomli_w
18+
19+
tomli_w_installed = True
20+
except ModuleNotFoundError: # pragma: no cover
21+
tomli_w_installed = False
22+
1623
from typing import Any
1724

1825
from benedict.extras import require_toml
@@ -35,11 +42,11 @@ def decode(self, s: str, **kwargs: Any) -> Any:
3542
if tomllib_available:
3643
data = tomllib.loads(s, **kwargs)
3744
else:
38-
require_toml(installed=toml_installed)
39-
data = toml.loads(s, **kwargs)
45+
require_toml(installed=tomli_installed)
46+
data = tomli.loads(s, **kwargs)
4047
return data
4148

4249
def encode(self, d: Any, **kwargs: Any) -> str:
43-
require_toml(installed=toml_installed)
44-
data = toml.dumps(dict(d), **kwargs)
45-
return data
50+
require_toml(installed=tomli_w_installed)
51+
result: str = tomli_w.dumps(dict(d), **kwargs)
52+
return result

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ schema = [
141141
"pydantic >= 2.0.0, < 3.0.0",
142142
]
143143
toml = [
144-
"toml >= 0.10.2, < 1.0.0",
144+
"tomli >= 2.0.0, < 3.0.0; python_version < '3.11'",
145+
"tomli-w >= 1.0.0, < 2.0.0",
145146
]
146147
xls = [
147148
"openpyxl >= 3.0.0, < 4.0.0",

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ python-fsutil == 0.16.1
1212
python-slugify == 8.0.4
1313
pyyaml == 6.0.3
1414
requests == 2.33.1
15-
toml == 0.10.2
15+
tomli == 2.0.2
16+
tomli-w == 1.2.0
1617
typing_extensions >= 4.14.1
1718
urllib3 >= 2.6.3
1819
xlrd == 2.0.2

tests/dicts/io/test_io_dict_toml.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ def test_from_toml_with_valid_data(self) -> None:
5050

5151
@unittest.skipIf(
5252
tomllib_available,
53-
"standard tomlib is available, exception will not be raised",
53+
"standard tomllib is available, exception will not be raised",
5454
)
55-
@patch("benedict.serializers.toml.toml_installed", False)
55+
@patch("benedict.serializers.toml.tomli_installed", False)
5656
def test_from_toml_with_valid_data_but_toml_extra_not_installed(self) -> None:
5757
j = """
5858
a = 1
@@ -184,7 +184,7 @@ def test_to_toml_file(self) -> None:
184184
self.assertFileExists(filepath)
185185
self.assertEqual(d, IODict.from_toml(filepath))
186186

187-
@patch("benedict.serializers.toml.toml_installed", False)
187+
@patch("benedict.serializers.toml.tomli_w_installed", False)
188188
def test_to_toml_with_extra_not_installed(self) -> None:
189189
d = IODict(
190190
{
Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,82 @@
11
import unittest
22

3-
# from benedict.serializers import TOMLSerializer
3+
from benedict import benedict
4+
from benedict.dicts.io import IODict
5+
from benedict.serializers import TOMLSerializer
46

57

68
class toml_serializer_test_case(unittest.TestCase):
79
"""
810
This class describes a toml serializer test case.
11+
12+
Regression coverage for issue #439 — the uiri/toml encoder crashes
13+
on certain strings. These tests pin the encode path to a library
14+
that handles them correctly and guard against regression.
915
"""
1016

11-
def test_decode_toml(self) -> None:
12-
# TODO
13-
pass
17+
def test_encode_ansi_control_character(self) -> None:
18+
"""Scenario 1 — falsification clause #1.
19+
20+
`benedict({"color": "\\033[31m"}).to_toml()` must not raise. On
21+
baseline (uiri/toml) this raises IndexError in the encoder.
22+
"""
23+
payload = {"color": "\033[31m"}
24+
encoded = benedict(payload).to_toml()
25+
self.assertIsInstance(encoded, str)
26+
self.assertGreater(len(encoded), 0)
27+
# Round-trip: decoded value must equal the original string.
28+
decoded = IODict.from_toml(encoded)
29+
self.assertEqual(decoded["color"], "\033[31m")
30+
31+
def test_encode_issue_439_literal_examples(self) -> None:
32+
"""Scenario 2 — regression guard for issue #439's cited examples.
33+
34+
These pass on baseline (literal backslashes, not control chars).
35+
Kept so the encoder swap does not silently regress them.
36+
"""
37+
payload = {
38+
"reset": "\\033\\[00;00m",
39+
"lightblue": "\\033\\[01;30m",
40+
}
41+
encoded = benedict(payload).to_toml()
42+
self.assertIsInstance(encoded, str)
43+
decoded = IODict.from_toml(encoded)
44+
self.assertEqual(decoded["reset"], "\\033\\[00;00m")
45+
self.assertEqual(decoded["lightblue"], "\\033\\[01;30m")
46+
47+
def test_roundtrip_control_chars_and_unicode(self) -> None:
48+
"""Scenario 4 — round-trip integrity across tricky values."""
49+
payload = {
50+
"ansi_red": "\033[31m",
51+
"ansi_reset": "\033[0m",
52+
"bell": "\x07",
53+
"tab_and_newline": "a\tb\nc",
54+
"unicode_emoji": "benedict 🎩",
55+
"backslash": "path\\to\\file",
56+
"quotes": 'he said "hi"',
57+
}
58+
encoded = benedict(payload).to_toml()
59+
decoded = IODict.from_toml(encoded)
60+
for key, value in payload.items():
61+
self.assertEqual(decoded[key], value, f"round-trip mismatch for {key!r}")
62+
63+
def test_encode_nested_dict(self) -> None:
64+
"""Structural coverage — nested dicts still encode correctly."""
65+
payload = {
66+
"section": {
67+
"key": "value",
68+
"control": "\033[31m",
69+
}
70+
}
71+
encoded = benedict(payload).to_toml()
72+
decoded = IODict.from_toml(encoded)
73+
self.assertEqual(decoded["section"]["key"], "value")
74+
self.assertEqual(decoded["section"]["control"], "\033[31m")
1475

15-
def test_encode_toml(self) -> None:
16-
# TODO
17-
pass
76+
def test_serializer_decode_roundtrip(self) -> None:
77+
"""Direct serializer-level round-trip (bypasses IODict convenience layer)."""
78+
serializer = TOMLSerializer()
79+
payload = {"color": "\033[31m", "count": 42}
80+
encoded = serializer.encode(payload)
81+
decoded = serializer.decode(encoded)
82+
self.assertEqual(decoded, payload)

0 commit comments

Comments
 (0)