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 == ""