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
31 changes: 19 additions & 12 deletions benedict/serializers/toml.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
try:
import toml

toml_installed = True
except ModuleNotFoundError: # pragma: no cover
toml_installed = False

try:
# python >= 3.11
import tomllib
Expand All @@ -13,6 +6,20 @@
except ImportError:
tomllib_available = False

try:
import tomli

tomli_installed = True
except ModuleNotFoundError:
tomli_installed = False

try:
import tomli_w

tomli_w_installed = True
except ModuleNotFoundError:
tomli_w_installed = False

from typing import Any

from benedict.extras import require_toml
Expand All @@ -35,11 +42,11 @@ def decode(self, s: str, **kwargs: Any) -> Any:
if tomllib_available:
data = tomllib.loads(s, **kwargs)
else:
require_toml(installed=toml_installed)
data = toml.loads(s, **kwargs)
require_toml(installed=tomli_installed)
data = tomli.loads(s, **kwargs)
return data

def encode(self, d: Any, **kwargs: Any) -> str:
require_toml(installed=toml_installed)
data = toml.dumps(dict(d), **kwargs)
return data
require_toml(installed=tomli_w_installed)
result: str = tomli_w.dumps(dict(d), **kwargs)
return result
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ schema = [
"pydantic >= 2.0.0, < 3.0.0",
]
toml = [
"toml >= 0.10.2, < 1.0.0",
"tomli >= 2.0.0, < 3.0.0; python_version < '3.11'",
"tomli-w >= 1.0.0, < 2.0.0",
]
xls = [
"openpyxl >= 3.0.0, < 4.0.0",
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ python-fsutil == 0.16.1
python-slugify == 8.0.4
pyyaml == 6.0.3
requests == 2.33.1
toml == 0.10.2
tomli == 2.0.2
tomli-w == 1.2.0
typing_extensions >= 4.14.1
urllib3 >= 2.6.3
xlrd == 2.0.2
Expand Down
6 changes: 3 additions & 3 deletions tests/dicts/io/test_io_dict_toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ def test_from_toml_with_valid_data(self) -> None:

@unittest.skipIf(
tomllib_available,
"standard tomlib is available, exception will not be raised",
"standard tomllib is available, exception will not be raised",
)
@patch("benedict.serializers.toml.toml_installed", False)
@patch("benedict.serializers.toml.tomli_installed", False)
def test_from_toml_with_valid_data_but_toml_extra_not_installed(self) -> None:
j = """
a = 1
Expand Down Expand Up @@ -184,7 +184,7 @@ def test_to_toml_file(self) -> None:
self.assertFileExists(filepath)
self.assertEqual(d, IODict.from_toml(filepath))

@patch("benedict.serializers.toml.toml_installed", False)
@patch("benedict.serializers.toml.tomli_w_installed", False)
def test_to_toml_with_extra_not_installed(self) -> None:
d = IODict(
{
Expand Down
79 changes: 72 additions & 7 deletions tests/serializers/test_toml_serializer.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,82 @@
import unittest

# from benedict.serializers import TOMLSerializer
from benedict import benedict
from benedict.dicts.io import IODict
from benedict.serializers import TOMLSerializer


class toml_serializer_test_case(unittest.TestCase):
"""
This class describes a toml serializer test case.

Regression coverage for issue #439 — the uiri/toml encoder crashes
on certain strings. These tests pin the encode path to a library
that handles them correctly and guard against regression.
"""

def test_decode_toml(self) -> None:
# TODO
pass
def test_encode_ansi_control_character(self) -> None:
"""Scenario 1 — falsification clause #1.

`benedict({"color": "\\033[31m"}).to_toml()` must not raise. On
baseline (uiri/toml) this raises IndexError in the encoder.
"""
payload = {"color": "\033[31m"}
encoded = benedict(payload).to_toml()
self.assertIsInstance(encoded, str)
self.assertGreater(len(encoded), 0)
# Round-trip: decoded value must equal the original string.
decoded = IODict.from_toml(encoded)
self.assertEqual(decoded["color"], "\033[31m")

def test_encode_issue_439_literal_examples(self) -> None:
"""Scenario 2 — regression guard for issue #439's cited examples.

These pass on baseline (literal backslashes, not control chars).
Kept so the encoder swap does not silently regress them.
"""
payload = {
"reset": "\\033\\[00;00m",
"lightblue": "\\033\\[01;30m",
}
encoded = benedict(payload).to_toml()
self.assertIsInstance(encoded, str)
decoded = IODict.from_toml(encoded)
self.assertEqual(decoded["reset"], "\\033\\[00;00m")
self.assertEqual(decoded["lightblue"], "\\033\\[01;30m")

def test_roundtrip_control_chars_and_unicode(self) -> None:
"""Scenario 4 — round-trip integrity across tricky values."""
payload = {
"ansi_red": "\033[31m",
"ansi_reset": "\033[0m",
"bell": "\x07",
"tab_and_newline": "a\tb\nc",
"unicode_emoji": "benedict 🎩",
"backslash": "path\\to\\file",
"quotes": 'he said "hi"',
}
encoded = benedict(payload).to_toml()
decoded = IODict.from_toml(encoded)
for key, value in payload.items():
self.assertEqual(decoded[key], value, f"round-trip mismatch for {key!r}")

def test_encode_nested_dict(self) -> None:
"""Structural coverage — nested dicts still encode correctly."""
payload = {
"section": {
"key": "value",
"control": "\033[31m",
}
}
encoded = benedict(payload).to_toml()
decoded = IODict.from_toml(encoded)
self.assertEqual(decoded["section"]["key"], "value")
self.assertEqual(decoded["section"]["control"], "\033[31m")

def test_encode_toml(self) -> None:
# TODO
pass
def test_serializer_decode_roundtrip(self) -> None:
"""Direct serializer-level round-trip (bypasses IODict convenience layer)."""
serializer = TOMLSerializer()
payload = {"color": "\033[31m", "count": 42}
encoded = serializer.encode(payload)
decoded = serializer.decode(encoded)
self.assertEqual(decoded, payload)