Skip to content

Commit 927a025

Browse files
committed
fix(unparse): handle non-string #text with attributes; unify value conversion
- Fix TypeError when #text contains non-string values (int, float, bool) with attributes - Add _convert_value_to_string() helper to eliminate duplicated conversion logic - Ensure consistent boolean conversion (lowercase) for both plain and #text values - Add comprehensive test coverage for GitHub issue #366 Fixes #366
1 parent ab4c86f commit 927a025

File tree

2 files changed

+62
-4
lines changed

2 files changed

+62
-4
lines changed

tests/test_dicttoxml.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,51 @@ def test_empty_dict_vs_empty_list_behavior(self):
460460

461461
# They should be different
462462
self.assertNotEqual(compact_dict, compact_list)
463+
464+
def test_non_string_text_with_attributes(self):
465+
"""Test that non-string #text values work when tag has attributes.
466+
467+
This test covers GitHub issue #366: Tag value (#text) must be a string
468+
when tag has additional parameters - unparse.
469+
470+
Also tests that plain values and explicit #text values are treated
471+
consistently (both go through the same conversion logic).
472+
"""
473+
# Test cases for explicit #text values with attributes
474+
self.assertEqual(unparse({"a": {"@param": "test", "#text": 1}}, full_document=False),
475+
'<a param="test">1</a>')
476+
477+
self.assertEqual(unparse({"a": {"@param": 42, "#text": 3.14}}, full_document=False),
478+
'<a param="42">3.14</a>')
479+
480+
self.assertEqual(unparse({"a": {"@param": "flag", "#text": True}}, full_document=False),
481+
'<a param="flag">true</a>')
482+
483+
self.assertEqual(unparse({"a": {"@param": "test", "#text": None}}, full_document=False),
484+
'<a param="test">None</a>')
485+
486+
self.assertEqual(unparse({"a": {"@param": "test", "#text": "string"}}, full_document=False),
487+
'<a param="test">string</a>')
488+
489+
self.assertEqual(unparse({"a": {"@attr1": "value1", "@attr2": 2, "#text": 100}}, full_document=False),
490+
'<a attr1="value1" attr2="2">100</a>')
491+
492+
# Test cases for plain values (should be treated the same as #text)
493+
self.assertEqual(unparse({"a": 1}, full_document=False), '<a>1</a>')
494+
self.assertEqual(unparse({"a": 3.14}, full_document=False), '<a>3.14</a>')
495+
self.assertEqual(unparse({"a": True}, full_document=False), '<a>true</a>')
496+
self.assertEqual(unparse({"a": "hello"}, full_document=False), '<a>hello</a>')
497+
self.assertEqual(unparse({"a": None}, full_document=False), '<a></a>')
498+
499+
# Consistency tests: plain values should match explicit #text values
500+
self.assertEqual(unparse({"a": 42}, full_document=False),
501+
unparse({"a": {"#text": 42}}, full_document=False))
502+
503+
self.assertEqual(unparse({"a": 3.14}, full_document=False),
504+
unparse({"a": {"#text": 3.14}}, full_document=False))
505+
506+
self.assertEqual(unparse({"a": True}, full_document=False),
507+
unparse({"a": {"#text": True}}, full_document=False))
508+
509+
self.assertEqual(unparse({"a": "hello"}, full_document=False),
510+
unparse({"a": {"#text": "hello"}}, full_document=False))

xmltodict.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,18 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False,
374374
return handler.item
375375

376376

377+
def _convert_value_to_string(value):
378+
"""Convert a value to its string representation for XML output.
379+
380+
Handles boolean values consistently by converting them to lowercase.
381+
"""
382+
if isinstance(value, (str, bytes)):
383+
return value
384+
if isinstance(value, bool):
385+
return "true" if value else "false"
386+
return str(value)
387+
388+
377389
def _has_angle_brackets(value):
378390
"""Return True if value (a str) contains '<' or '>'.
379391
@@ -463,21 +475,19 @@ def _emit(key, value, content_handler,
463475
raise ValueError('document with multiple roots')
464476
if v is None:
465477
v = _dict()
466-
elif isinstance(v, bool):
467-
v = 'true' if v else 'false'
468478
elif not isinstance(v, (dict, str)):
469479
if expand_iter and hasattr(v, '__iter__'):
470480
v = _dict(((expand_iter, v),))
471481
else:
472-
v = str(v)
482+
v = _convert_value_to_string(v)
473483
if isinstance(v, str):
474484
v = _dict(((cdata_key, v),))
475485
cdata = None
476486
attrs = _dict()
477487
children = []
478488
for ik, iv in v.items():
479489
if ik == cdata_key:
480-
cdata = iv
490+
cdata = _convert_value_to_string(iv)
481491
continue
482492
if isinstance(ik, str) and ik.startswith(attr_prefix):
483493
ik = _process_namespace(ik, namespaces, namespace_separator,

0 commit comments

Comments
 (0)