Skip to content
Merged
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
Binary file modified .coverage
Binary file not shown.
10 changes: 5 additions & 5 deletions dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")

Expand All @@ -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()
main()
2 changes: 1 addition & 1 deletion json2xml/dicttoxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'


Expand Down
81 changes: 81 additions & 0 deletions tests/test_dict2xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<none_key></none_key>" == 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 <here>"
}
}
}
result = dicttoxml.dicttoxml(info_dict, attr_type=False, item_wrap=False, root=False).decode('utf-8')
expected = '<Info Name="systemSpec" HelpText="spec version &lt;here&gt;"></Info>'
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 &amp; Jerry"' in result
assert 'less_than="value &lt; 10"' in result
assert 'greater_than="value &gt; 5"' in result
assert 'quotes="He said &quot;Hello&quot;"' in result
assert 'single_quotes="It&apos;s working"' in result
assert 'mixed="Tom &amp; Jerry &lt; 10 &gt; 5 &quot;quoted&quot; &apos;apostrophe&apos;"' 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 <here>",
"ampersand": "Tom & Jerry",
"quotes": 'Say "hello"'
}
result = make_attrstring(attrs)

assert 'test="value &lt;here&gt;"' in result
assert 'ampersand="Tom &amp; Jerry"' in result
assert 'quotes="Say &quot;hello&quot;"' in result

# Test empty attributes
empty_attrs: dict[str, Any] = {}
result = make_attrstring(empty_attrs)
assert result == ""
Loading