diff --git a/.coverage b/.coverage
index 401fa9c..cad90cb 100644
Binary files a/.coverage and b/.coverage differ
diff --git a/dev.py b/dev.py
index ff74e89..cf89b93 100644
--- a/dev.py
+++ b/dev.py
@@ -10,7 +10,7 @@ def run_command(cmd: list[str], description: str) -> bool:
"""Run a command and return True if successful."""
print(f"\nš {description}...")
try:
- result = subprocess.run(cmd, check=True, cwd=Path(__file__).parent)
+ subprocess.run(cmd, check=True, cwd=Path(__file__).parent)
print(f"ā
{description} passed!")
return True
except subprocess.CalledProcessError as e:
@@ -32,7 +32,7 @@ def main() -> None:
if command in ("test", "all"):
success &= run_command([
- "pytest", "--cov=json2xml", "--cov-report=term",
+ "pytest", "--cov=json2xml", "--cov-report=term",
"-xvs", "tests", "-n", "auto"
], "Tests")
@@ -50,11 +50,11 @@ def main() -> None:
return
if not success:
- print(f"\nā Some checks failed!")
+ print("\nā Some checks failed!")
sys.exit(1)
else:
- print(f"\nš All checks passed!")
+ print("\nš All checks passed!")
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py
index 00d62a5..af32da4 100644
--- a/json2xml/dicttoxml.py
+++ b/json2xml/dicttoxml.py
@@ -127,7 +127,7 @@ def make_attrstring(attr: dict[str, Any]) -> str:
Returns:
str: The string of XML attributes.
"""
- attrstring = " ".join([f'{k}="{v}"' for k, v in attr.items()])
+ attrstring = " ".join([f'{k}="{escape_xml(v)}"' for k, v in attr.items()])
return f'{" " if attrstring != "" else ""}{attrstring}'
diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py
index 8db1c28..df770a9 100644
--- a/tests/test_dict2xml.py
+++ b/tests/test_dict2xml.py
@@ -1062,3 +1062,84 @@ def test_convert_dict_with_falsy_value_line_400(self) -> None:
# None should trigger the "elif not val:" branch and result in an empty element
assert "" == result
+
+ def test_attrs_xml_escaping(self) -> None:
+ """Test that @attrs values are properly XML-escaped."""
+ # Test the specific case from the user's bug report
+ info_dict = {
+ 'Info': {
+ "@attrs": {
+ "Name": "systemSpec",
+ "HelpText": "spec version "
+ }
+ }
+ }
+ result = dicttoxml.dicttoxml(info_dict, attr_type=False, item_wrap=False, root=False).decode('utf-8')
+ expected = ''
+ assert expected == result
+
+ def test_attrs_comprehensive_xml_escaping(self) -> None:
+ """Test comprehensive XML escaping in attributes."""
+ data = {
+ 'Element': {
+ "@attrs": {
+ "ampersand": "Tom & Jerry",
+ "less_than": "value < 10",
+ "greater_than": "value > 5",
+ "quotes": 'He said "Hello"',
+ "single_quotes": "It's working",
+ "mixed": "Tom & Jerry < 10 > 5 \"quoted\" 'apostrophe'"
+ },
+ "@val": "content"
+ }
+ }
+ result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8')
+
+ # Check that all special characters are properly escaped in attributes
+ assert 'ampersand="Tom & Jerry"' in result
+ assert 'less_than="value < 10"' in result
+ assert 'greater_than="value > 5"' in result
+ assert 'quotes="He said "Hello""' in result
+ assert 'single_quotes="It's working"' in result
+ assert 'mixed="Tom & Jerry < 10 > 5 "quoted" 'apostrophe'"' in result
+
+ # Verify the element content is also properly escaped
+ assert ">content<" in result
+
+ def test_attrs_empty_and_none_values(self) -> None:
+ """Test attribute handling with empty and None values."""
+ data = {
+ 'Element': {
+ "@attrs": {
+ "empty": "",
+ "zero": 0,
+ "false": False
+ }
+ }
+ }
+ result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8')
+
+ assert 'empty=""' in result
+ assert 'zero="0"' in result
+ assert 'false="False"' in result
+
+ def test_make_attrstring_function_directly(self) -> None:
+ """Test the make_attrstring function directly."""
+ from json2xml.dicttoxml import make_attrstring
+
+ # Test basic escaping
+ attrs = {
+ "test": "value ",
+ "ampersand": "Tom & Jerry",
+ "quotes": 'Say "hello"'
+ }
+ result = make_attrstring(attrs)
+
+ assert 'test="value <here>"' in result
+ assert 'ampersand="Tom & Jerry"' in result
+ assert 'quotes="Say "hello""' in result
+
+ # Test empty attributes
+ empty_attrs: dict[str, Any] = {}
+ result = make_attrstring(empty_attrs)
+ assert result == ""