Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 9 additions & 2 deletions benedict/serializers/toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
except ModuleNotFoundError:
toml_installed = False

try:
import tomli_w

tomli_w_installed = True
except ModuleNotFoundError:
tomli_w_installed = False

try:
# python >= 3.11
import tomllib
Expand Down Expand Up @@ -40,6 +47,6 @@ def decode(self, s: str, **kwargs: Any) -> Any:
return data

def encode(self, d: Any, **kwargs: Any) -> str:
require_toml(installed=toml_installed)
data = toml.dumps(dict(d), **kwargs)
require_toml(installed=tomli_w_installed)
data = tomli_w.dumps(dict(d), **kwargs)
return data
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ s3 = [
"boto3 >= 1.24.89, < 2.0.0",
]
toml = [
"toml >= 0.10.2, < 1.0.0",
"toml >= 0.10.2, < 1.0.0; python_version < '3.11'",
"tomli-w >= 1.0.0, < 2.0.0",
]
xls = [
"openpyxl >= 3.0.0, < 4.0.0",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ python-slugify == 8.0.4
pyyaml == 6.0.3
requests == 2.33.1
toml == 0.10.2
tomli-w == 1.2.0
typing_extensions >= 4.14.1
urllib3 >= 2.6.3
useful-types == 0.2.1
Expand Down
2 changes: 1 addition & 1 deletion tests/dicts/io/test_io_dict_toml.py
Original file line number Diff line number Diff line change
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):
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test methods in this file omit the -> None return type annotation, while the other serializer test modules consistently include it (e.g., tests/serializers/test_abstract_serializer.py:12). For consistency (and to keep type-checking expectations uniform), please add -> None to these new test methods.

Copilot uses AI. Check for mistakes.
"""Scenario 1 — falsification clause #1.

`benedict({"color": "\033[31m"}).to_toml()` must not raise. On
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring includes "\033[31m" inside a Python string literal, so \033 will be interpreted and the docstring will contain a real ESC control character at runtime. This can lead to invisible/control characters in test output and makes the docstring harder to read/copy. Consider escaping it as \\033[31m (or writing it as \x1b[31m) in the docstring example so the docstring remains printable text.

Suggested change
`benedict({"color": "\033[31m"}).to_toml()` must not raise. On
`benedict({"color": "\\033[31m"}).to_toml()` must not raise. On

Copilot uses AI. Check for mistakes.
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):
"""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):
"""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):
"""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):
"""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)
Loading